Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 62 additions & 11 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,16 +1,67 @@
node_modules
.next
dist
# Docker matches these patterns against paths relative to the build context ROOT,
# so a bare `node_modules` excludes only ./node_modules. apps/*/node_modules,
# apps/*/dist and apps/*/.env all stayed in the context. Every pattern naming a
# build artifact or a secret is therefore `**/`-prefixed; `**/` matches zero or
# more leading directories, so the root-level copy is still covered.

# Dependencies. Host installs are platform-specific: a darwin node_modules that
# survives into out/full/ is copied over the linux install in the installer stage.
**/node_modules

# Build outputs
**/dist
**/.next
**/.turbo
**/coverage
**/*.tsbuildinfo

# Secrets. `.env*` alone matched only the root, so apps/*/.env, which holds
# live tokens on any developer machine, sat inside the build context of every
# image here. `*.env` (staging.env, prod.env) is a separate shape the
# dot-prefixed patterns do not match.
#
# This block mirrors every secret-bearing entry in .gitignore; keep the two in
# step. settings.local.json is unanchored there on purpose (nested worktrees,
# editor .bak copies), so it is unanchored here too rather than relying on the
# .claude exclusion below to cover it.
**/.env
**/.env.*
**/*.env
!**/.env.example
!**/.env.*.example

# Credentials and keys
**/*.pem
**/*.key
**/*.p12
**/*.pfx
**/settings.local.json
**/settings.local.json.*
**/.railway-config-pull-*
**/.chalk
**/*.log
**/.DS_Store
**/Thumbs.db

# A local `turbo prune` leaves a full monorepo copy here, and a `next export` in
# any app leaves one there. Depth-matching for the same reason as everything
# above: `/out` anchored to the root reintroduced the exact bug this file's
# header describes, one line below the header.
**/out
**/.nyc_output

# Repo and editor metadata not needed by any build
.git
.github
.claude
*.md
!README.md
.env*
!.env.example
docker-compose.yml
.turbo
coverage
.vscode
.idea
**/.vscode
**/.idea

# Docs site is not built from these images
apps/docs

# Top-level docs only, deliberately not `**/*.md`: nested README files stay in the
# context because packages may read or ship them.
*.md
!README.md
44 changes: 44 additions & 0 deletions apps/release-bot/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Required. Discord application (dev portal -> your app -> Bot -> Reset Token).
# The bot needs Send Messages, View Channel and Read Message History: reading the
# channel is how it knows what it has already announced. Embed Links is needed
# only for YouTube announcements, where Discord's unfurl is the video player.
# Release announcements wrap their URL in angle brackets to suppress it.
DISCORD_BOT_TOKEN=

# Required. Release notes are never posted unsummarized. Without this no release
# is announced and the run exits non-zero - it does not fall back to announcing
# releases with an empty body, because the watermark would then advance past
# every one of them and no later fix could recover them. YouTube announcements
# are unaffected either way: videos run first and need neither OpenAI nor GitHub,
# so a key that is missing or rejected stops releases only. A transient failure
# stops that source for the run and is retried next.
OPENAI_API_KEY=

# Required, for reading releases and the commits between them. Needs no scopes
# beyond public repository read. Unauthenticated requests are rate limited to 60
# an hour, which one run can exhaust.
GITHUB_TOKEN=

# Where each source posts. A source with no channel is skipped, so these can be
# filled in one at a time.
AGUI_CHANNEL_ID=
CPK_CHANNEL_ID=
# Optional: OpenBot posts to CPK_CHANNEL_ID unless given its own channel.
OPENBOT_CHANNEL_ID=
YOUTUBE_CHANNEL_DISCORD_ID=

# The CopilotKit YouTube channel.
YOUTUBE_CHANNEL_ID=UCbC2DjohfqaUcXK_XmXBVUg

# Optional. Unset means announcements are silent, which is the default.
AGUI_PING_ROLE_ID=
CPK_PING_ROLE_ID=
# Optional. Falls back to CPK_PING_ROLE_ID only while OPENBOT_CHANNEL_ID is
# unset. Setting that - even to the same id as CPK_CHANNEL_ID - turns the
# fallback off, so an unset role here then means silent rather than pinging
# CopilotKit's role.
OPENBOT_PING_ROLE_ID=
YOUTUBE_PING_ROLE_ID=

# Optional. Defaults to gpt-5.4.
OPENAI_MODEL=
84 changes: 84 additions & 0 deletions apps/release-bot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# ── Stage 1: prune the monorepo to only what @copilotkit/outpost-release-bot needs ──
# Pinned to a minor, like turbo and pnpm below. A floating `node:22-alpine` is
# not a watchPattern input, so a rebuild triggered by any watched file could
# move the runtime version underneath an otherwise identical image.
FROM node:22.20-alpine AS pruner
RUN apk add --no-cache libc6-compat
WORKDIR /app

# No pnpm in this stage: pruning runs on turbo alone.
#
# This literal is the version pnpm-lock.yaml resolves for the root `turbo`
# devDependency (the root declares the range ^2.3.0, which resolves to 2.9.6).
# Nothing enforces the match: npm resolves this pin from the registry, not from
# the lockfile, so the two drift silently and the pruner can run a different
# turbo than every local `turbo run` does. Bump this literal in the same commit
# that bumps the root devDependency, and re-read the resolved version out of
# pnpm-lock.yaml rather than copying the declared range.
RUN npm install -g turbo@2.9.6

COPY . .
# `turbo prune --docker` uses the SCM file list when .git is present. .dockerignore
# excludes .git, so it falls back to a filesystem walk and would rake in whatever
# the host left lying around. .dockerignore keeps node_modules out of the context
# in the first place; this scrub is the second line of defence, because a host
# node_modules reaching out/full/ would be copied over the linux install in the
# stage below and produce an image whose native modules are darwin binaries.
RUN turbo prune @copilotkit/outpost-release-bot --docker \
&& find out/full -name node_modules -type d -prune -exec rm -rf {} +

# ── Stage 2: install dependencies and build ──────────────────────────────────
FROM node:22.20-alpine AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Matches the root package.json's packageManager field; corepack honours that
# field, so a different pin here would be silently ignored at best.
#
# The Node pin has to stay at 22.14 or later for this line to work at all. npm
# rotated its registry signing keys after 22.12 shipped, and the corepack
# bundled with 22.12 and 22.13 (0.29.4 and 0.30.0) does not carry the new key:
# this exact command fails there with `Cannot find matching keyid`, so the image
# never builds. 0.31.0, in Node 22.14, is the first that works.
RUN corepack enable && corepack prepare pnpm@10.33.4 --activate

# Manifests and the pruned lockfile first, so this install layer is reused on any
# change that touches only sources. Keep this ordering: it is the whole point of
# the two-step pruned copy.
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile

# Sources second. out/full/ is scrubbed of node_modules in the pruner stage above,
# so this COPY merges sources over the install rather than overwriting it.
COPY --from=pruner /app/out/full/ .
COPY --from=pruner /app/tsconfig.json ./tsconfig.json
RUN pnpm turbo run build --filter=@copilotkit/outpost-release-bot \
&& rm -f apps/release-bot/dist/*.tsbuildinfo

# ── Stage 3: production image ────────────────────────────────────────────────
# No Prisma, no database, no health server: this service talks to GitHub, YouTube,
# OpenAI and Discord over HTTPS, announces what is new, and exits.
FROM node:22.20-alpine AS runner

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 outpost && \
adduser --system --uid 1001 outpost

WORKDIR /app

# No node_modules. This package declares no dependencies - every import in src/
# is relative or a Node builtin (`node:fs`, `node:url`) - and the installer stage runs a plain `pnpm install`
# without --prod, so copying either tree would ship turbo, typescript, eslint,
# prettier, vitest and tsx into a container that runs one script and exits.
#
# Adding the first runtime dependency means adding the copy back, and pnpm's
# isolated layout puts an app's own deps in apps/release-bot/node_modules
# symlinked into the root store, so it is both trees plus a --prod install.
COPY --from=installer --chown=outpost:outpost /app/apps/release-bot/dist ./apps/release-bot/dist
# "type": "module" lives here and is load-bearing for ESM resolution of ./x.js.
COPY --from=installer --chown=outpost:outpost /app/apps/release-bot/package.json ./apps/release-bot/package.json

USER outpost

CMD ["node", "apps/release-bot/dist/index.js"]
Loading
Loading