Files
2026-06-18 22:21:38 -04:00

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
08-gitea-ci 04 execute 4
08-03
.gitea/workflows/ci.yml
false
CI-02
service why env_vars
gitea-registry-pat Publish job authenticates to the Gitea container registry; created in Plan 01
name source
GITEA_REGISTRY_PAT Repo secret created in Plan 01 (write:package scope)
truths artifacts key_links
A merge (push) to main triggers a publish job that builds the API Docker production image and pushes it to the Gitea container registry
The image is pushed under two tags: :latest and :<milestone>-<shortsha> (e.g. v1.1-<7charsha>)
Registry authentication uses docker login --password-stdin with the PAT piped from a repo secret — the token never appears in plaintext in the CI log
The publish job runs only on push to main, never on pull_request, and never carries DEV_AUTH_BYPASS
path provides contains
.gitea/workflows/ci.yml push-to-main publish job (CI-02) docker push
from to via pattern
.gitea/workflows/ci.yml (publish job) git.bergerhouse.net registry docker login --password-stdin + docker build --target production + docker push --password-stdin
Add the publish job to `.gitea/workflows/ci.yml`: on merge (push) to `main`, build the API Docker `production` image and push it to the Gitea container registry under `:latest` and `:-`, authenticating with the operator PAT via `--password-stdin` so the credential never hits the log. This delivers CI-02 (ROADMAP criteria 5 + 6) and Pitfall 13 (--password-stdin).

Purpose: Every merge to main produces an immutable, traceable image (D-04) plus a moving :latest pointer, with zero credential exposure (ROADMAP criterion 6 is a hard requirement).

Output: a publish job in ci.yml gated on push → main.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-gitea-ci/08-RESEARCH.md @.planning/research/PITFALLS.md @.planning/phases/08-gitea-ci/08-01-SUMMARY.md @apps/api/Dockerfile

<artifacts_this_phase_produces>

  • .gitea/workflows/ci.yml (EXTENDED — adds the publish job; closes the phase) </artifacts_this_phase_produces>

<interface_context> Confirmed facts (do not re-derive):

  • Git remote: https://git.bergerhouse.net/luckberg/familysync.git → registry host git.bergerhouse.net, owner luckberg. Image: git.bergerhouse.net/luckberg/familysync-api (08-RESEARCH §Registry Details).
  • Dockerfile is multi-stage with a production target that builds API + PWA and serves both on :3000. It MUST be built from the REPO ROOT with -f apps/api/Dockerfile . (the Dockerfile header and 08-RESEARCH §Dockerfile Build Context say so — building from apps/api/ fails because it copies the root workspace manifest + lockfile).
  • Milestone = v1.1 (PROJECT.md "Current Milestone"). Per D-04, expose it as the workflow-level env.MILESTONE (already added in Plan 02) rather than hardcoding inline; update at milestone boundaries.
  • Short SHA = ${GITHUB_SHA:0:7} (CONFIRMED available in Gitea Actions; 08-RESEARCH). If 08-01-SUMMARY found GITHUB_SHA unavailable, fall back to git rev-parse --short=7 HEAD.
  • Registry auth: PAT with write:package in repo secret GITEA_REGISTRY_PAT (created Plan 01). GITHUB_TOKEN/GITEA_TOKEN CANNOT push packages (08-RESEARCH; Gitea forum) — must use the PAT. Username = luckberg.
  • Pitfall 13: NEVER docker login -p $TOKEN (token leaks to the log / process list). ALWAYS echo "$PAT" | docker login git.bergerhouse.net -u luckberg --password-stdin. </interface_context>
Task 1: Add the publish job (build + tag + login --password-stdin + push) .gitea/workflows/ci.yml - .planning/phases/08-gitea-ci/08-RESEARCH.md (§Pattern 6 Docker publish; §Docker Registry Push; §Image Tag Strategy D-04; Pitfall on GITHUB_TOKEN) - .planning/research/PITFALLS.md (Pitfall 13 --password-stdin) - apps/api/Dockerfile (production target; build-from-root requirement) - .planning/phases/08-gitea-ci/08-01-SUMMARY.md (docker socket access confirmed; GITHUB_SHA availability) Add a `publish` job to ci.yml: `runs-on: self-hosted`, guarded `if: github.event_name == 'push' && github.ref == 'refs/heads/main'` (push-to-main ONLY — never pull_request; D-03). It runs independently of the PR jobs (those are pull_request-gated and won't fire on push). Do NOT set DEV_AUTH_BYPASS anywhere in this job (T-08-06 boundary).
Steps:
  1. `uses: actions/checkout@v4`.
  2. Compute tags (id: tags). Derive `SHORT_SHA=${GITHUB_SHA:0:7}` (fallback `git rev-parse --short=7 HEAD` if 08-01 flagged GITHUB_SHA missing). Use the workflow-level `${{ env.MILESTONE }}` (= v1.1). Emit two outputs:
     `latest=git.bergerhouse.net/luckberg/familysync-api:latest`
     `sha_tag=git.bergerhouse.net/luckberg/familysync-api:${MILESTONE}-${SHORT_SHA}`
     (write to `$GITHUB_OUTPUT`).
  3. Docker login via stdin (Pitfall 13 — the load-bearing security step):
     `echo "${{ secrets.GITEA_REGISTRY_PAT }}" | docker login git.bergerhouse.net --username luckberg --password-stdin`
     NEVER use `-p`/`--password` with the token as an argument. Do not `echo` the secret anywhere else; do not set it as a plain env var.
  4. Build + push from REPO ROOT:
     `docker build --target production -f apps/api/Dockerfile -t <latest> -t <sha_tag> .`
     then `docker push <latest>` and `docker push <sha_tag>`.
  5. Final `if: always()` step: `docker logout git.bergerhouse.net || true` to drop the stored credential from the runner after push.

Use `docker/login-action`/`docker/build-push-action` ONLY if 08-01-SUMMARY confirmed they resolve AND you prefer them; the shell `docker login --password-stdin` + `docker build`/`docker push` form is the safer first iteration (08-RESEARCH §Pattern 6 note) and is the recommended path.
grep -q "github.event_name == 'push'" .gitea/workflows/ci.yml && grep -q "refs/heads/main" .gitea/workflows/ci.yml && grep -q -- "--password-stdin" .gitea/workflows/ci.yml && ! grep -E "docker login.*(-p |--password )[^-]" .gitea/workflows/ci.yml && grep -q "docker build --target production" .gitea/workflows/ci.yml && grep -q "familysync-api:latest" .gitea/workflows/ci.yml && grep -q 'familysync-api:${MILESTONE}' .gitea/workflows/ci.yml && grep -q "docker push" .gitea/workflows/ci.yml && ! grep -qi "DEV_AUTH_BYPASS" <(awk '/publish:/,0' .gitea/workflows/ci.yml) && echo PUBLISH_OK The publish job runs only on push to main, logs in with --password-stdin (never -p), builds the production target from repo root, pushes :latest and :${MILESTONE}-, logs out, and never sets DEV_AUTH_BYPASS. Task 2: Merge, audit the publish log, and verify both tags The publish job (Task 1). Verifying it requires merging the PR to main and auditing the resulting CI log + registry — the executor cannot merge a protected branch. 1. Merge the PR (all PR jobs green) into `main` — the push triggers the publish job. 2. In Gitea → Actions, open the publish job log and AUDIT it line by line: the PAT must NOT appear in plaintext anywhere (ROADMAP criterion 6 — hard requirement). The `docker login` line should show `--password-stdin`, never the token. If the token is visible, STOP — rotate the PAT and fix before anything else. 3. Confirm the build used `--target production -f apps/api/Dockerfile .` and succeeded. 4. In Gitea → repo → Packages, confirm `familysync-api` exists with BOTH tags: `latest` and `v1.1-<7charsha>` matching the merge commit. 5. (Optional) `docker pull git.bergerhouse.net/luckberg/familysync-api:latest` from a machine with registry access to confirm the image is pullable. Type "publish verified" once both tags exist in the registry AND the log audit confirms no plaintext PAT, or describe the failure.

<threat_model>

Trust Boundaries

Boundary Description
Repo secret → docker login PAT crosses into the job; the single highest-value secret in this phase
publish job → registry Authenticated push to the package registry

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-08-PAT Information Disclosure GITEA_REGISTRY_PAT in publish job mitigate docker login --password-stdin exclusively — token piped via stdin, never an -p/--password argument (Pitfall 13). Token referenced only as ${{ secrets.GITEA_REGISTRY_PAT }} (Gitea masks registered secrets in logs); never echoed elsewhere; docker logout after push. Grep gate forbids -p/--password forms. Checkpoint requires a line-by-line log audit (ROADMAP criterion 6). This is the load-bearing mitigation for the phase.
T-08-09 Spoofing DEV_AUTH_BYPASS bleed into publish mitigate Publish job never sets DEV_AUTH_BYPASS (grep gate scoped to the publish: block); the bypass is confined to the harness job (Plan 03).
T-08-10 Tampering wrong build context mitigate Build from repo root with -f apps/api/Dockerfile . (Dockerfile requires root context for the workspace manifest + lockfile); building from apps/api/ would fail or produce a broken image.

</threat_model>

- ci.yml passes the Task grep gate (push-to-main guard, --password-stdin, no -p, production target from root, both tags, no DEV_AUTH_BYPASS in publish block). - After merge: both tags present in the Gitea registry; log audit shows no plaintext PAT.

<success_criteria>

  • CI-02: on merge to main, the API production image is built and pushed to the Gitea registry under :latest + :v1.1- (ROADMAP criterion 5; D-04).
  • Registry credentials never appear in plaintext in the CI logs (ROADMAP criterion 6; Pitfall 13) — the load-bearing security outcome of the phase.
  • Publish runs only on push to main; DEV_AUTH_BYPASS never bleeds into it. </success_criteria>
Create `.planning/phases/08-gitea-ci/08-04-SUMMARY.md` when done. Record: the final image name + both tags pushed, confirmation the log audit found no plaintext PAT, and whether the shell or docker/* action form was used.