Backbone sub-project #1, CI half (verified with actionlint; the runtime proof is
the first rc/stable tag push — these jobs cannot run locally).
- ci-staging.yml:
- new `images` job (needs: test): buildx builds clusev-app (Dockerfile) +
clusev-terminal (docker/terminal/Dockerfile) → private GHCR, tagged by
version; then a framework smoke-boot (`php artisan route:list`) so a class
stripped by export-ignore that is still reachable on boot fails before promote.
- `deploy-staging` widened to any tag; pulls the CI-built private image
(CLUSEV_IMAGE/CLUSEV_TERMINAL_IMAGE + CLUSEV_PULL=1) instead of building.
- new gated `promote` job (needs: [test, images], if success + stable vX.Y.Z,
no -rc): copy the exact image digests to the public GHCR, run promote.sh
(leak-guard + export-ignore tree), write release-images.lock, push code+tag.
- promote-public.yml: repurposed as a guarded manual fallback mirroring the auto
promote (image copy + lock + guard). Dropped the "beta channel" naming.
- promote-stable.yml: removed (no beta→stable step in a single-channel model).
Deploy inputs are GitHub vars/secrets (GHCR_OWNER, PUBLIC_GHCR_OWNER,
PUBLIC_REPO_SLUG, STAGING_*, GHCR_TOKEN, PUBLIC_REPO_TOKEN, PROMOTE_FORBIDDEN) —
never hardcoded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GitHub deprecated the Node 20 action runtime, so checkout@v4 and setup-node@v4 were
force-run on Node 24 with a warning. v5 of both targets Node 24 natively; their inputs
are unchanged (ref/repository/token/fetch-depth/path; the build's node-version stays
'20', that's the project runtime not the action runtime). setup-php@v2 is the current
major and unaffected. ci-staging runs only on tag pushes, so the warning clears on the
next tagged release.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PromotionService::publicTags() is called on every Release render incl. the 5s
page poll; the anonymous GitHub tag API is rate-limited (60/h per IP). Cache it
120s so the panel never gets rate-limited to empty. (Test flushes cache in setUp.)
- yank.yml: replace the loose shell-glob tag guard with an anchored grep regex so
a garbage/space/metachar tag dispatched straight from the GitHub UI is rejected
before reaching git push --delete (the panel already validates tag ∈ publicTags).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Full-page tests render @vite layouts; without public/build/manifest.json (gitignored,
absent on a fresh CI checkout) they 500. Move npm ci + npm run build ahead of the
test step so the manifest exists. Fixes the red CI on every v* tag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- promote.sh: fail fast before mutating the public tree if the tag was already
promoted (avoids a half-commit left on the runner on re-dispatch).
- ci-staging.yml: pass STAGING_PROJECT_DIR via env + :? guard instead of a
${{ }} expression in run: (clears the template-injection lint, fails loudly
on misconfig).
- README: PUBLIC_REPO_URL was never read by any workflow — keep only the slug.
- set-repository-url.test.sh: chmod +x for mode parity with promote.test.sh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Promotion workflows for the release pipeline. promote-public publishes a tag to
the public repo's beta channel; promote-stable re-tags a finalised beta as a
stable vX.Y.Z. Both bound to the 'production' environment (required reviewer),
reading PUBLIC_REPO_SLUG/PUBLIC_REPO_TOKEN. Tree copy via scripts/promote.sh
excludes .git/.github so private CI never reaches the public repo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>