From eb4b4d590c8a7a80894086c4ca89b6f0e456f3f1 Mon Sep 17 00:00:00 2001 From: plutoless Date: Thu, 11 Jun 2026 02:58:00 -0700 Subject: [PATCH 1/6] docs: spec for server-only docker image --- ...26-06-11-quickstart-docker-image-design.md | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md diff --git a/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md b/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md new file mode 100644 index 0000000..71a5f2d --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md @@ -0,0 +1,145 @@ +# agent-quickstart-python — Server-Only Docker Image Design + +**Date:** 2026-06-11 +**Status:** Approved +**Repo:** `agent-quickstart-python` +**Branch:** `ci/docker-image` off `main` +**Relation:** Sub-project 2 of 3 (test suite → **Docker image** → nightly). Ported from the +`recipe-agent-custom-llm` Docker work, but deliberately reduced to a **server-only** image. + +## Goal + +Add a Docker image for the FastAPI backend plus a GitHub Actions workflow that builds and +smoke-tests it on every push/PR and publishes it to GHCR on `v*` tags. The image contains +**only** the Python `server/` backend — no web frontend, no `llm/` (this repo has none). + +## Why server-only (the key decision) + +The combined web+server image considered first would have required bundling the Next.js +frontend, which in turn requires Next's `output: 'standalone'` packaging mode and a +production-code change to `web/next.config.ts`. Dropping the web frontend from the image +removes that requirement entirely: + +- No bun/Next build stage. +- No `output: 'standalone'`, no `DOCKER_BUILD` type-check seam — `web/next.config.ts` is + **untouched**. +- No Node in the runtime — the base image is a single `python:3.12-slim-bookworm`. +- One process (the FastAPI server), so no `entrypoint.sh` / `wait -n` juggling — a plain + `CMD`. + +The frontend still runs the normal way (`bun run dev`, or a Next deploy); it is simply not +part of this container image. The image is a deploy-shaped artifact for the backend half. + +## Layout + +``` +Dockerfile # single-stage python:3.12-slim, server only +.dockerignore # exclude venv, caches, env, tests, docs, git, markdown +.github/workflows/docker.yml # build -> smoke (:8000) -> push to GHCR on v* tags +``` + +No changes to `web/`, `server/src/`, or any docs (CI-only, per the locked decision). + +## Components + +### `Dockerfile` (single stage) + +```dockerfile +# syntax=docker/dockerfile:1 +FROM python:3.12-slim-bookworm AS runtime +WORKDIR /app + +# Python dependencies for the FastAPI backend. +COPY server/requirements.txt /tmp/server-req.txt +RUN pip install --no-cache-dir -r /tmp/server-req.txt + +# Backend source. +COPY server/src /app/server/src + +# server.py reads $PORT (default 8000) and binds 0.0.0.0. +EXPOSE 8000 +CMD ["python", "/app/server/src/server.py"] +``` + +Notes: +- `server/src/server.py` already ends with `uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT","8000")))`, + so the `CMD` needs no wrapper. Binding `0.0.0.0` is what makes the mapped port reachable. +- `python:3.12-slim-bookworm` is within the documented floor (≥ 3.10); the app is + version-agnostic across 3.10–3.13, so a stable slim base is all that matters here. + +### `.dockerignore` + +``` +**/venv +**/node_modules +**/__pycache__ +*.env.local +**/.env.local +**/tests +docs/ +.github/ +.git/ +*.md +``` + +(Same generic content as the custom-llm source; `web/.next` is dropped since web is not in +the build context that matters, but keeping a superset is harmless. We keep the list minimal +and backend-relevant.) + +### `.github/workflows/docker.yml` + +Triggers: `push` (all branches + `v*` tags), `pull_request`, and `workflow_call` (so the +later nightly sub-project can reuse it without edits). Permissions: `contents: read`, +`packages: write`. + +One `docker` job on `ubuntu-latest`: +1. `actions/checkout@v4` +2. `docker/setup-buildx-action@v3` +3. `docker/metadata-action@v5` → tags: `type=sha`, `type=ref,event=pr`, + `type=semver,pattern={{version}}`, `type=semver,pattern={{major}}.{{minor}}`, + `type=raw,value=latest,enable=${{ startsWith(github.ref,'refs/tags/') }}`, + images `ghcr.io/${{ github.repository }}`. +4. `docker/build-push-action@v6` with `context: .`, `platforms: linux/amd64`, `load: true`, + `push: false`, `cache-from/to: type=gha`. +5. **Smoke test** — run the image with fake creds, poll until ready, fail on any miss: + ```bash + IMAGE=$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -n1) + docker run -d --name smoke -p 8000:8000 \ + -e AGORA_APP_ID=0123456789abcdef0123456789abcdef \ + -e AGORA_APP_CERTIFICATE=fedcba9876543210fedcba9876543210 \ + "$IMAGE" + # poll http://localhost:8000/get_config (up to ~40s); print docker logs on failure + docker rm -f smoke + ``` + `/get_config` is the right probe: it exercises real Token007 generation from the fake + 32-hex creds (no Agora cloud call), proving the app booted and the route works. +6. **Log in to GHCR** + **Push tags** — both gated on `startsWith(github.ref,'refs/tags/')`, + using `docker/login-action@v3` with `secrets.GITHUB_TOKEN`. + +## Out of scope + +- No web frontend in the image; no `web/next.config.ts` change. +- No README / `docs/ai/` changes (CI-only). Because `docs/ai/` is untouched, **no L0 + `Last Reviewed` bump** is needed. +- No `nightly.yml` (sub-project 3). The `workflow_call:` trigger is included now so the + nightly can call this workflow later with no edit. + +## Verification + +- **Local:** `docker build -t qs-server .` then run with the two fake AGORA envs and + `curl -fsS localhost:8000/get_config` returns a JSON envelope with a non-empty token. +- **No regression:** `bun run verify` / `verify:local` unaffected (no app or web changes). +- **CI:** the `docker` job is green on the PR (build + smoke); push steps are skipped on a + branch (only run on `v*` tags). + +## Risks / Notes + +- **`server.py` as a script:** the `CMD` runs `python /app/server/src/server.py`, relying on + the module's `__main__` uvicorn launch. Verified present at `server/src/server.py:200-204`. +- **No `/health` route:** unlike the custom-llm `llm/` service, the backend has no `/health`; + `/get_config` is the liveness probe and additionally validates token generation. +- **Image size:** slim Python base + one `pip install`, no Node/bun layers — small. +- **Next cycle:** sub-project 3 (nightly) adds `.github/workflows/nightly.yml` calling + `ci.yml` + `docker.yml` on a daily schedule; this spec's `workflow_call:` trigger is the + seam it will use. +``` From fcb3f42fafea483389907a673581a8a666334f95 Mon Sep 17 00:00:00 2001 From: plutoless Date: Thu, 11 Jun 2026 03:09:11 -0700 Subject: [PATCH 2/6] docs: fold grill decisions into server-only docker spec (non-root, amd64, tag-push) --- ...26-06-11-quickstart-docker-image-design.md | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md b/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md index 71a5f2d..9cb43fb 100644 --- a/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md +++ b/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md @@ -30,6 +30,17 @@ removes that requirement entirely: The frontend still runs the normal way (`bun run dev`, or a Next deploy); it is simply not part of this container image. The image is a deploy-shaped artifact for the backend half. +## Locked decisions (grill 2026-06-11) + +1. **Server-only image** — no web frontend, so no `web/next.config.ts` change. +2. **amd64-only** (`platforms: linux/amd64`) — the `load: true` smoke path requires a single + arch; arm64 users run under emulation. No multi-arch push. +3. **Non-root `USER app`** — strictly better default for a fresh image; a few extra Dockerfile + lines, no runtime cost. +4. **Keep GHCR tag-push** — build+smoke on every push/PR, publish only on `v*` tags. The image + is undocumented (CI-only) but still produced on releases. +5. **CI-only, no docs** — no README / `docs/ai/` changes; therefore no L0 `Last Reviewed` bump. + ## Layout ``` @@ -47,14 +58,21 @@ No changes to `web/`, `server/src/`, or any docs (CI-only, per the locked decisi ```dockerfile # syntax=docker/dockerfile:1 FROM python:3.12-slim-bookworm AS runtime + +# Run as a non-root user (created before any COPY so --chown can reference it). +RUN useradd --create-home --uid 10001 app WORKDIR /app -# Python dependencies for the FastAPI backend. +# Python dependencies for the FastAPI backend (installed as root into the +# system site-packages, which is world-readable for the app user at runtime). COPY server/requirements.txt /tmp/server-req.txt RUN pip install --no-cache-dir -r /tmp/server-req.txt -# Backend source. -COPY server/src /app/server/src +# Backend source, owned by the runtime user. +COPY --chown=app:app server/src /app/server/src + +# Drop privileges for the running process. +USER app # server.py reads $PORT (default 8000) and binds 0.0.0.0. EXPOSE 8000 @@ -66,6 +84,9 @@ Notes: so the `CMD` needs no wrapper. Binding `0.0.0.0` is what makes the mapped port reachable. - `python:3.12-slim-bookworm` is within the documented floor (≥ 3.10); the app is version-agnostic across 3.10–3.13, so a stable slim base is all that matters here. +- **Non-root:** `pip install` runs as root (writes to `/usr/local`, world-readable), then + `USER app` drops privileges before `CMD`. Ports 8000 (> 1024) need no privilege, so the + unprivileged user binds fine. ### `.dockerignore` From 7ce8045bc1c0c4b4cafa489591a9fe0c31552c97 Mon Sep 17 00:00:00 2001 From: plutoless Date: Thu, 11 Jun 2026 03:14:05 -0700 Subject: [PATCH 3/6] docs: implementation plan for server-only docker image --- .../2026-06-11-quickstart-docker-image.md | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-11-quickstart-docker-image.md diff --git a/docs/superpowers/plans/2026-06-11-quickstart-docker-image.md b/docs/superpowers/plans/2026-06-11-quickstart-docker-image.md new file mode 100644 index 0000000..e0c9ab1 --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-quickstart-docker-image.md @@ -0,0 +1,264 @@ +# Quickstart Server-Only Docker Image Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a single-stage, non-root Docker image for the FastAPI `server/` backend plus a GitHub Actions workflow that builds + smoke-tests it on every push/PR and publishes to GHCR on `v*` tags. + +**Architecture:** One `python:3.12-slim-bookworm` stage installs `server/requirements.txt`, copies `server/src`, drops to a non-root user, and runs `python server.py` (which reads `$PORT`, default 8000, binds `0.0.0.0`). No web frontend, no `llm/`, no `next.config.ts` change. The CI workflow mirrors the custom-llm `docker.yml` but smoke-probes only `:8000/get_config`. + +**Tech Stack:** Docker (single-stage), GitHub Actions (`docker/build-push-action`, `docker/metadata-action`), GHCR. + +**Spec:** `docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md` + +**Repo & branch:** `agent-quickstart-python` (`/Users/zhangqianze/Documents/agent-quickstart-python`), branch `ci/docker-image` (already created off `main`; the spec + grilled spec are committed there). + +--- + +## Conventions + +- Conventional Commits, lowercase after prefix, present tense. **No AI attribution / no `Co-Authored-By`. No `--no-verify`. No git config changes.** If a commit fails on git identity, prefix with `git -c user.email="qianze.zhang@hotmail.com"`. +- This is infrastructure: the "tests" are a local `docker build` + container smoke and dependency-free `grep` validations. A failure is a real finding — surface it, don't weaken the check. +- Requires a working Docker daemon for the local build/smoke steps. If Docker is unavailable in the execution environment, complete the file creation + `grep` validations and report DONE_WITH_CONCERNS noting the local build was deferred to CI (the CI `docker` job is the authoritative gate). + +--- + +## Task 1: Dockerfile + .dockerignore (build + local smoke) + +**Files:** +- Create: `Dockerfile`, `.dockerignore` + +- [ ] **Step 1: Create `.dockerignore`** + +``` +**/venv +**/node_modules +**/__pycache__ +*.env.local +**/.env.local +**/tests +docs/ +.github/ +.git/ +*.md +``` + +- [ ] **Step 2: Create `Dockerfile`** + +```dockerfile +# syntax=docker/dockerfile:1 +FROM python:3.12-slim-bookworm AS runtime + +# Run as a non-root user (created before any COPY so --chown can reference it). +RUN useradd --create-home --uid 10001 app +WORKDIR /app + +# Python dependencies for the FastAPI backend (installed as root into the +# system site-packages, world-readable for the app user at runtime). +COPY server/requirements.txt /tmp/server-req.txt +RUN pip install --no-cache-dir -r /tmp/server-req.txt + +# Backend source, owned by the runtime user. +COPY --chown=app:app server/src /app/server/src + +# Drop privileges for the running process. +USER app + +# server.py reads $PORT (default 8000) and binds 0.0.0.0. +EXPOSE 8000 +CMD ["python", "/app/server/src/server.py"] +``` + +- [ ] **Step 3: Build the image** + +Run: +```bash +cd /Users/zhangqianze/Documents/agent-quickstart-python +docker build -t qs-server:test . +``` +Expected: build succeeds; final stage installs fastapi/uvicorn/agora-agents and copies `server/src`. (No web/Node layers, so no type-check OOM risk.) + +- [ ] **Step 4: Smoke the container locally** + +Run: +```bash +docker rm -f qs-smoke 2>/dev/null || true +docker run -d --name qs-smoke -p 8000:8000 \ + -e AGORA_APP_ID=0123456789abcdef0123456789abcdef \ + -e AGORA_APP_CERTIFICATE=fedcba9876543210fedcba9876543210 \ + qs-server:test +# poll up to ~40s +for i in $(seq 1 40); do curl -fsS http://localhost:8000/get_config -o /tmp/qs_cfg.json && break; sleep 1; done +cat /tmp/qs_cfg.json; echo +docker rm -f qs-smoke +``` +Expected: `/get_config` returns a JSON envelope `{"code":0,"msg":"success","data":{...}}` with a non-empty `token`. (Token007 is generated offline from the fake 32-hex creds — no Agora cloud call.) If the container exits or `/get_config` never responds, run `docker logs qs-smoke` and report it — a real finding. + +- [ ] **Step 5: Confirm the process runs as non-root** + +Run: +```bash +docker run --rm --entrypoint sh qs-server:test -c "id -un" +``` +Expected: `app` (not `root`). + +- [ ] **Step 6: Commit** + +```bash +cd /Users/zhangqianze/Documents/agent-quickstart-python +git add Dockerfile .dockerignore +git commit -m "build: add non-root server-only docker image" +``` + +--- + +## Task 2: CI workflow (`.github/workflows/docker.yml`) + +**Files:** +- Create: `.github/workflows/docker.yml` + +- [ ] **Step 1: Create `.github/workflows/docker.yml`** + +```yaml +name: docker + +on: + push: + branches: ["**"] + tags: ["v*"] + pull_request: + workflow_call: + +permissions: + contents: read + packages: write + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} + + - name: Build (load locally, no push) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + load: true + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Smoke test + run: | + IMAGE=$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -n1) + echo "Smoke-testing $IMAGE" + docker run -d --name smoke -p 8000:8000 \ + -e AGORA_APP_ID=0123456789abcdef0123456789abcdef \ + -e AGORA_APP_CERTIFICATE=fedcba9876543210fedcba9876543210 \ + "$IMAGE" + set +e + fail=0 + for url in http://localhost:8000/get_config; do + ok="" + for i in $(seq 1 40); do + if curl -fsS "$url" -o /dev/null; then ok=1; echo "OK $url"; break; fi + sleep 1 + done + if [ -z "$ok" ]; then echo "FAIL $url"; fail=1; fi + done + if [ "$fail" -ne 0 ]; then docker logs smoke; fi + docker rm -f smoke + exit $fail + + - name: Log in to GHCR + if: startsWith(github.ref, 'refs/tags/') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push tags + if: startsWith(github.ref, 'refs/tags/') + run: | + printf '%s\n' "${{ steps.meta.outputs.tags }}" | while read -r tag; do + [ -n "$tag" ] && docker push "$tag" + done +``` + +- [ ] **Step 2: Structural validation** + +Run: +```bash +cd /Users/zhangqianze/Documents/agent-quickstart-python +grep -nE "name: docker|workflow_call:|platforms: linux/amd64|-p 8000:8000|/get_config|startsWith\(github.ref, 'refs/tags/'\)" .github/workflows/docker.yml +grep -nP "\t" .github/workflows/docker.yml && echo "HAS TABS (bad)" || echo "no tabs" +grep -c ":3000\|8001" .github/workflows/docker.yml +``` +Expected: the matched lines print; `no tabs`; the last `grep -c` prints `0` (no web/llm ports leaked in from the custom-llm source). + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/docker.yml +git commit -m "ci: build + smoke-test the docker image, push to GHCR on tags" +``` + +--- + +## Task 3: No-regression check + push + PR + +**Files:** none (git only). + +- [ ] **Step 1: Confirm no app/web files changed** + +Run: +```bash +cd /Users/zhangqianze/Documents/agent-quickstart-python +git diff --name-only main...ci/docker-image +``` +Expected: only `Dockerfile`, `.dockerignore`, `.github/workflows/docker.yml`, and the two `docs/superpowers/...docker...` files. **No** `web/`, `server/src/`, `package.json`, or `next.config.ts` changes. + +- [ ] **Step 2: Push** + +```bash +git push -u origin ci/docker-image +``` + +- [ ] **Step 3: Open the PR** (REST — the GraphQL `gh pr create` path 401s under the lapsed SSO session) + +```bash +REPO=AgoraIO-Conversational-AI/agent-quickstart-python +gh api -X POST "repos/$REPO/pulls" \ + -f title="ci: add server-only docker image + workflow" \ + -f head="ci/docker-image" -f base="main" \ + -f body="Adds a single-stage, non-root python:3.12-slim Docker image for the FastAPI server/ backend, plus a docker workflow that builds and smoke-tests it (probe :8000/get_config with fake AGORA creds) on every push/PR and publishes to GHCR on v* tags. Server-only: no web frontend, no llm/, no next.config.ts change. amd64-only (the load+smoke path needs single arch). (Sub-project 2 of 3; nightly follows and reuses this via workflow_call.)" \ + --jq '{number, url: .html_url, state}' +``` +Expected: JSON with the new PR number + URL. + +--- + +## Self-Review notes (for the implementer) + +- **No web in the image** — if a step references `web/`, `next.config.ts`, `bun`, or port `3000`/`8001`, it's wrong; this image is server-only. +- **Smoke probe is `/get_config`**, not `/health` (the backend has no `/health`). It doubles as a token-generation check and needs the two fake `AGORA_*` envs. +- **Non-root** — `pip install` is root, `USER app` precedes `CMD`; the `id -un` check must print `app`. +- **`workflow_call:` is intentionally present** in this *new* `docker.yml` so sub-project 3 (nightly) can reuse it with no edit. (The quickstart `ci.yml` from sub-project 1 does not yet have `workflow_call`; the nightly cycle adds it there.) +- **Docker may be unavailable** in the sandbox — if so, do the file + grep work, report the deferred local build clearly, and let CI be the gate. Do not fake a green smoke. +``` From 17315f957ca395844514839c3c829ba8bd7ba0e7 Mon Sep 17 00:00:00 2001 From: plutoless Date: Thu, 11 Jun 2026 03:17:41 -0700 Subject: [PATCH 4/6] build: add non-root server-only docker image --- .dockerignore | 10 ++++++++++ Dockerfile | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8fa99a6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +**/venv +**/node_modules +**/__pycache__ +*.env.local +**/.env.local +**/tests +docs/ +.github/ +.git/ +*.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4e70769 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 +FROM python:3.12-slim-bookworm AS runtime + +# Run as a non-root user (created before any COPY so --chown can reference it). +RUN useradd --create-home --uid 10001 app +WORKDIR /app + +# Python dependencies for the FastAPI backend (installed as root into the +# system site-packages, world-readable for the app user at runtime). +COPY server/requirements.txt /tmp/server-req.txt +RUN pip install --no-cache-dir -r /tmp/server-req.txt + +# Backend source, owned by the runtime user. +COPY --chown=app:app server/src /app/server/src + +# Drop privileges for the running process. +USER app + +# server.py reads $PORT (default 8000) and binds 0.0.0.0. +EXPOSE 8000 +CMD ["python", "/app/server/src/server.py"] From c7cdf7c65015b5f15eb6fa36cdf3e151bfd37f08 Mon Sep 17 00:00:00 2001 From: plutoless Date: Thu, 11 Jun 2026 03:19:15 -0700 Subject: [PATCH 5/6] ci: build + smoke-test the docker image, push to GHCR on tags --- .github/workflows/docker.yml | 80 ++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..8787514 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,80 @@ +name: docker + +on: + push: + branches: ["**"] + tags: ["v*"] + pull_request: + workflow_call: + +permissions: + contents: read + packages: write + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} + + - name: Build (load locally, no push) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + load: true + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Smoke test + run: | + IMAGE=$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -n1) + echo "Smoke-testing $IMAGE" + docker run -d --name smoke -p 8000:8000 \ + -e AGORA_APP_ID=0123456789abcdef0123456789abcdef \ + -e AGORA_APP_CERTIFICATE=fedcba9876543210fedcba9876543210 \ + "$IMAGE" + set +e + fail=0 + for url in http://localhost:8000/get_config; do + ok="" + for i in $(seq 1 40); do + if curl -fsS "$url" -o /dev/null; then ok=1; echo "OK $url"; break; fi + sleep 1 + done + if [ -z "$ok" ]; then echo "FAIL $url"; fail=1; fi + done + if [ "$fail" -ne 0 ]; then docker logs smoke; fi + docker rm -f smoke + exit $fail + + - name: Log in to GHCR + if: startsWith(github.ref, 'refs/tags/') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push tags + if: startsWith(github.ref, 'refs/tags/') + run: | + printf '%s\n' "${{ steps.meta.outputs.tags }}" | while read -r tag; do + [ -n "$tag" ] && docker push "$tag" + done From dd06312d38049b4f5268af3b1cdd77a353af4ede Mon Sep 17 00:00:00 2001 From: plutoless Date: Thu, 11 Jun 2026 03:43:57 -0700 Subject: [PATCH 6/6] chore: stop tracking superpowers docs and gitignore the folder --- .gitignore | 4 +- .../2026-06-11-quickstart-docker-image.md | 264 ------------------ ...26-06-11-quickstart-docker-image-design.md | 166 ----------- 3 files changed, 3 insertions(+), 431 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-11-quickstart-docker-image.md delete mode 100644 docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md diff --git a/.gitignore b/.gitignore index 1a07908..bb0325d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -/.vscode \ No newline at end of file +/.vscode +# Superpowers workflow docs (specs/plans) — local only, not tracked +docs/superpowers/ diff --git a/docs/superpowers/plans/2026-06-11-quickstart-docker-image.md b/docs/superpowers/plans/2026-06-11-quickstart-docker-image.md deleted file mode 100644 index e0c9ab1..0000000 --- a/docs/superpowers/plans/2026-06-11-quickstart-docker-image.md +++ /dev/null @@ -1,264 +0,0 @@ -# Quickstart Server-Only Docker Image Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a single-stage, non-root Docker image for the FastAPI `server/` backend plus a GitHub Actions workflow that builds + smoke-tests it on every push/PR and publishes to GHCR on `v*` tags. - -**Architecture:** One `python:3.12-slim-bookworm` stage installs `server/requirements.txt`, copies `server/src`, drops to a non-root user, and runs `python server.py` (which reads `$PORT`, default 8000, binds `0.0.0.0`). No web frontend, no `llm/`, no `next.config.ts` change. The CI workflow mirrors the custom-llm `docker.yml` but smoke-probes only `:8000/get_config`. - -**Tech Stack:** Docker (single-stage), GitHub Actions (`docker/build-push-action`, `docker/metadata-action`), GHCR. - -**Spec:** `docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md` - -**Repo & branch:** `agent-quickstart-python` (`/Users/zhangqianze/Documents/agent-quickstart-python`), branch `ci/docker-image` (already created off `main`; the spec + grilled spec are committed there). - ---- - -## Conventions - -- Conventional Commits, lowercase after prefix, present tense. **No AI attribution / no `Co-Authored-By`. No `--no-verify`. No git config changes.** If a commit fails on git identity, prefix with `git -c user.email="qianze.zhang@hotmail.com"`. -- This is infrastructure: the "tests" are a local `docker build` + container smoke and dependency-free `grep` validations. A failure is a real finding — surface it, don't weaken the check. -- Requires a working Docker daemon for the local build/smoke steps. If Docker is unavailable in the execution environment, complete the file creation + `grep` validations and report DONE_WITH_CONCERNS noting the local build was deferred to CI (the CI `docker` job is the authoritative gate). - ---- - -## Task 1: Dockerfile + .dockerignore (build + local smoke) - -**Files:** -- Create: `Dockerfile`, `.dockerignore` - -- [ ] **Step 1: Create `.dockerignore`** - -``` -**/venv -**/node_modules -**/__pycache__ -*.env.local -**/.env.local -**/tests -docs/ -.github/ -.git/ -*.md -``` - -- [ ] **Step 2: Create `Dockerfile`** - -```dockerfile -# syntax=docker/dockerfile:1 -FROM python:3.12-slim-bookworm AS runtime - -# Run as a non-root user (created before any COPY so --chown can reference it). -RUN useradd --create-home --uid 10001 app -WORKDIR /app - -# Python dependencies for the FastAPI backend (installed as root into the -# system site-packages, world-readable for the app user at runtime). -COPY server/requirements.txt /tmp/server-req.txt -RUN pip install --no-cache-dir -r /tmp/server-req.txt - -# Backend source, owned by the runtime user. -COPY --chown=app:app server/src /app/server/src - -# Drop privileges for the running process. -USER app - -# server.py reads $PORT (default 8000) and binds 0.0.0.0. -EXPOSE 8000 -CMD ["python", "/app/server/src/server.py"] -``` - -- [ ] **Step 3: Build the image** - -Run: -```bash -cd /Users/zhangqianze/Documents/agent-quickstart-python -docker build -t qs-server:test . -``` -Expected: build succeeds; final stage installs fastapi/uvicorn/agora-agents and copies `server/src`. (No web/Node layers, so no type-check OOM risk.) - -- [ ] **Step 4: Smoke the container locally** - -Run: -```bash -docker rm -f qs-smoke 2>/dev/null || true -docker run -d --name qs-smoke -p 8000:8000 \ - -e AGORA_APP_ID=0123456789abcdef0123456789abcdef \ - -e AGORA_APP_CERTIFICATE=fedcba9876543210fedcba9876543210 \ - qs-server:test -# poll up to ~40s -for i in $(seq 1 40); do curl -fsS http://localhost:8000/get_config -o /tmp/qs_cfg.json && break; sleep 1; done -cat /tmp/qs_cfg.json; echo -docker rm -f qs-smoke -``` -Expected: `/get_config` returns a JSON envelope `{"code":0,"msg":"success","data":{...}}` with a non-empty `token`. (Token007 is generated offline from the fake 32-hex creds — no Agora cloud call.) If the container exits or `/get_config` never responds, run `docker logs qs-smoke` and report it — a real finding. - -- [ ] **Step 5: Confirm the process runs as non-root** - -Run: -```bash -docker run --rm --entrypoint sh qs-server:test -c "id -un" -``` -Expected: `app` (not `root`). - -- [ ] **Step 6: Commit** - -```bash -cd /Users/zhangqianze/Documents/agent-quickstart-python -git add Dockerfile .dockerignore -git commit -m "build: add non-root server-only docker image" -``` - ---- - -## Task 2: CI workflow (`.github/workflows/docker.yml`) - -**Files:** -- Create: `.github/workflows/docker.yml` - -- [ ] **Step 1: Create `.github/workflows/docker.yml`** - -```yaml -name: docker - -on: - push: - branches: ["**"] - tags: ["v*"] - pull_request: - workflow_call: - -permissions: - contents: read - packages: write - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: docker/setup-buildx-action@v3 - - - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=sha - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }} - - - name: Build (load locally, no push) - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64 - load: true - push: false - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Smoke test - run: | - IMAGE=$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -n1) - echo "Smoke-testing $IMAGE" - docker run -d --name smoke -p 8000:8000 \ - -e AGORA_APP_ID=0123456789abcdef0123456789abcdef \ - -e AGORA_APP_CERTIFICATE=fedcba9876543210fedcba9876543210 \ - "$IMAGE" - set +e - fail=0 - for url in http://localhost:8000/get_config; do - ok="" - for i in $(seq 1 40); do - if curl -fsS "$url" -o /dev/null; then ok=1; echo "OK $url"; break; fi - sleep 1 - done - if [ -z "$ok" ]; then echo "FAIL $url"; fail=1; fi - done - if [ "$fail" -ne 0 ]; then docker logs smoke; fi - docker rm -f smoke - exit $fail - - - name: Log in to GHCR - if: startsWith(github.ref, 'refs/tags/') - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Push tags - if: startsWith(github.ref, 'refs/tags/') - run: | - printf '%s\n' "${{ steps.meta.outputs.tags }}" | while read -r tag; do - [ -n "$tag" ] && docker push "$tag" - done -``` - -- [ ] **Step 2: Structural validation** - -Run: -```bash -cd /Users/zhangqianze/Documents/agent-quickstart-python -grep -nE "name: docker|workflow_call:|platforms: linux/amd64|-p 8000:8000|/get_config|startsWith\(github.ref, 'refs/tags/'\)" .github/workflows/docker.yml -grep -nP "\t" .github/workflows/docker.yml && echo "HAS TABS (bad)" || echo "no tabs" -grep -c ":3000\|8001" .github/workflows/docker.yml -``` -Expected: the matched lines print; `no tabs`; the last `grep -c` prints `0` (no web/llm ports leaked in from the custom-llm source). - -- [ ] **Step 3: Commit** - -```bash -git add .github/workflows/docker.yml -git commit -m "ci: build + smoke-test the docker image, push to GHCR on tags" -``` - ---- - -## Task 3: No-regression check + push + PR - -**Files:** none (git only). - -- [ ] **Step 1: Confirm no app/web files changed** - -Run: -```bash -cd /Users/zhangqianze/Documents/agent-quickstart-python -git diff --name-only main...ci/docker-image -``` -Expected: only `Dockerfile`, `.dockerignore`, `.github/workflows/docker.yml`, and the two `docs/superpowers/...docker...` files. **No** `web/`, `server/src/`, `package.json`, or `next.config.ts` changes. - -- [ ] **Step 2: Push** - -```bash -git push -u origin ci/docker-image -``` - -- [ ] **Step 3: Open the PR** (REST — the GraphQL `gh pr create` path 401s under the lapsed SSO session) - -```bash -REPO=AgoraIO-Conversational-AI/agent-quickstart-python -gh api -X POST "repos/$REPO/pulls" \ - -f title="ci: add server-only docker image + workflow" \ - -f head="ci/docker-image" -f base="main" \ - -f body="Adds a single-stage, non-root python:3.12-slim Docker image for the FastAPI server/ backend, plus a docker workflow that builds and smoke-tests it (probe :8000/get_config with fake AGORA creds) on every push/PR and publishes to GHCR on v* tags. Server-only: no web frontend, no llm/, no next.config.ts change. amd64-only (the load+smoke path needs single arch). (Sub-project 2 of 3; nightly follows and reuses this via workflow_call.)" \ - --jq '{number, url: .html_url, state}' -``` -Expected: JSON with the new PR number + URL. - ---- - -## Self-Review notes (for the implementer) - -- **No web in the image** — if a step references `web/`, `next.config.ts`, `bun`, or port `3000`/`8001`, it's wrong; this image is server-only. -- **Smoke probe is `/get_config`**, not `/health` (the backend has no `/health`). It doubles as a token-generation check and needs the two fake `AGORA_*` envs. -- **Non-root** — `pip install` is root, `USER app` precedes `CMD`; the `id -un` check must print `app`. -- **`workflow_call:` is intentionally present** in this *new* `docker.yml` so sub-project 3 (nightly) can reuse it with no edit. (The quickstart `ci.yml` from sub-project 1 does not yet have `workflow_call`; the nightly cycle adds it there.) -- **Docker may be unavailable** in the sandbox — if so, do the file + grep work, report the deferred local build clearly, and let CI be the gate. Do not fake a green smoke. -``` diff --git a/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md b/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md deleted file mode 100644 index 9cb43fb..0000000 --- a/docs/superpowers/specs/2026-06-11-quickstart-docker-image-design.md +++ /dev/null @@ -1,166 +0,0 @@ -# agent-quickstart-python — Server-Only Docker Image Design - -**Date:** 2026-06-11 -**Status:** Approved -**Repo:** `agent-quickstart-python` -**Branch:** `ci/docker-image` off `main` -**Relation:** Sub-project 2 of 3 (test suite → **Docker image** → nightly). Ported from the -`recipe-agent-custom-llm` Docker work, but deliberately reduced to a **server-only** image. - -## Goal - -Add a Docker image for the FastAPI backend plus a GitHub Actions workflow that builds and -smoke-tests it on every push/PR and publishes it to GHCR on `v*` tags. The image contains -**only** the Python `server/` backend — no web frontend, no `llm/` (this repo has none). - -## Why server-only (the key decision) - -The combined web+server image considered first would have required bundling the Next.js -frontend, which in turn requires Next's `output: 'standalone'` packaging mode and a -production-code change to `web/next.config.ts`. Dropping the web frontend from the image -removes that requirement entirely: - -- No bun/Next build stage. -- No `output: 'standalone'`, no `DOCKER_BUILD` type-check seam — `web/next.config.ts` is - **untouched**. -- No Node in the runtime — the base image is a single `python:3.12-slim-bookworm`. -- One process (the FastAPI server), so no `entrypoint.sh` / `wait -n` juggling — a plain - `CMD`. - -The frontend still runs the normal way (`bun run dev`, or a Next deploy); it is simply not -part of this container image. The image is a deploy-shaped artifact for the backend half. - -## Locked decisions (grill 2026-06-11) - -1. **Server-only image** — no web frontend, so no `web/next.config.ts` change. -2. **amd64-only** (`platforms: linux/amd64`) — the `load: true` smoke path requires a single - arch; arm64 users run under emulation. No multi-arch push. -3. **Non-root `USER app`** — strictly better default for a fresh image; a few extra Dockerfile - lines, no runtime cost. -4. **Keep GHCR tag-push** — build+smoke on every push/PR, publish only on `v*` tags. The image - is undocumented (CI-only) but still produced on releases. -5. **CI-only, no docs** — no README / `docs/ai/` changes; therefore no L0 `Last Reviewed` bump. - -## Layout - -``` -Dockerfile # single-stage python:3.12-slim, server only -.dockerignore # exclude venv, caches, env, tests, docs, git, markdown -.github/workflows/docker.yml # build -> smoke (:8000) -> push to GHCR on v* tags -``` - -No changes to `web/`, `server/src/`, or any docs (CI-only, per the locked decision). - -## Components - -### `Dockerfile` (single stage) - -```dockerfile -# syntax=docker/dockerfile:1 -FROM python:3.12-slim-bookworm AS runtime - -# Run as a non-root user (created before any COPY so --chown can reference it). -RUN useradd --create-home --uid 10001 app -WORKDIR /app - -# Python dependencies for the FastAPI backend (installed as root into the -# system site-packages, which is world-readable for the app user at runtime). -COPY server/requirements.txt /tmp/server-req.txt -RUN pip install --no-cache-dir -r /tmp/server-req.txt - -# Backend source, owned by the runtime user. -COPY --chown=app:app server/src /app/server/src - -# Drop privileges for the running process. -USER app - -# server.py reads $PORT (default 8000) and binds 0.0.0.0. -EXPOSE 8000 -CMD ["python", "/app/server/src/server.py"] -``` - -Notes: -- `server/src/server.py` already ends with `uvicorn.run(app, host="0.0.0.0", port=int(os.getenv("PORT","8000")))`, - so the `CMD` needs no wrapper. Binding `0.0.0.0` is what makes the mapped port reachable. -- `python:3.12-slim-bookworm` is within the documented floor (≥ 3.10); the app is - version-agnostic across 3.10–3.13, so a stable slim base is all that matters here. -- **Non-root:** `pip install` runs as root (writes to `/usr/local`, world-readable), then - `USER app` drops privileges before `CMD`. Ports 8000 (> 1024) need no privilege, so the - unprivileged user binds fine. - -### `.dockerignore` - -``` -**/venv -**/node_modules -**/__pycache__ -*.env.local -**/.env.local -**/tests -docs/ -.github/ -.git/ -*.md -``` - -(Same generic content as the custom-llm source; `web/.next` is dropped since web is not in -the build context that matters, but keeping a superset is harmless. We keep the list minimal -and backend-relevant.) - -### `.github/workflows/docker.yml` - -Triggers: `push` (all branches + `v*` tags), `pull_request`, and `workflow_call` (so the -later nightly sub-project can reuse it without edits). Permissions: `contents: read`, -`packages: write`. - -One `docker` job on `ubuntu-latest`: -1. `actions/checkout@v4` -2. `docker/setup-buildx-action@v3` -3. `docker/metadata-action@v5` → tags: `type=sha`, `type=ref,event=pr`, - `type=semver,pattern={{version}}`, `type=semver,pattern={{major}}.{{minor}}`, - `type=raw,value=latest,enable=${{ startsWith(github.ref,'refs/tags/') }}`, - images `ghcr.io/${{ github.repository }}`. -4. `docker/build-push-action@v6` with `context: .`, `platforms: linux/amd64`, `load: true`, - `push: false`, `cache-from/to: type=gha`. -5. **Smoke test** — run the image with fake creds, poll until ready, fail on any miss: - ```bash - IMAGE=$(printf '%s\n' "${{ steps.meta.outputs.tags }}" | head -n1) - docker run -d --name smoke -p 8000:8000 \ - -e AGORA_APP_ID=0123456789abcdef0123456789abcdef \ - -e AGORA_APP_CERTIFICATE=fedcba9876543210fedcba9876543210 \ - "$IMAGE" - # poll http://localhost:8000/get_config (up to ~40s); print docker logs on failure - docker rm -f smoke - ``` - `/get_config` is the right probe: it exercises real Token007 generation from the fake - 32-hex creds (no Agora cloud call), proving the app booted and the route works. -6. **Log in to GHCR** + **Push tags** — both gated on `startsWith(github.ref,'refs/tags/')`, - using `docker/login-action@v3` with `secrets.GITHUB_TOKEN`. - -## Out of scope - -- No web frontend in the image; no `web/next.config.ts` change. -- No README / `docs/ai/` changes (CI-only). Because `docs/ai/` is untouched, **no L0 - `Last Reviewed` bump** is needed. -- No `nightly.yml` (sub-project 3). The `workflow_call:` trigger is included now so the - nightly can call this workflow later with no edit. - -## Verification - -- **Local:** `docker build -t qs-server .` then run with the two fake AGORA envs and - `curl -fsS localhost:8000/get_config` returns a JSON envelope with a non-empty token. -- **No regression:** `bun run verify` / `verify:local` unaffected (no app or web changes). -- **CI:** the `docker` job is green on the PR (build + smoke); push steps are skipped on a - branch (only run on `v*` tags). - -## Risks / Notes - -- **`server.py` as a script:** the `CMD` runs `python /app/server/src/server.py`, relying on - the module's `__main__` uvicorn launch. Verified present at `server/src/server.py:200-204`. -- **No `/health` route:** unlike the custom-llm `llm/` service, the backend has no `/health`; - `/get_config` is the liveness probe and additionally validates token generation. -- **Image size:** slim Python base + one `pip install`, no Node/bun layers — small. -- **Next cycle:** sub-project 3 (nightly) adds `.github/workflows/nightly.yml` calling - `ci.yml` + `docker.yml` on a daily schedule; this spec's `workflow_call:` trigger is the - seam it will use. -```