# CD-01 — Signed staging release artifacts from main Status: INTEGRATED (PR #158, PR #159) / LOG_CLEAN_RUN_PENDING Validation: local PASS; run 40494565772 released and verified; landing setup changed to the preinstalled Node so runner cache paths do not appear in logs Scope: staging continuous-deploy plan, Phase 1 only. No VPS, k8s, nginx, production, release tag or Phase 2 work is included. ## Delivered - `.github/workflows/release-staging.yml` builds and publishes `api`, `worker` and `migrate` from `infra/docker/Dockerfile` with `REVISION=` to `ghcr.io//abastevo-:sha-`. It also builds the landing for `https://teste.abastevo.com.br` and uploads `landing-dist.tar.gz`. - Each image gets an SPDX JSON SBOM and a build-provenance attestation bound to its digest. The workflow records image references and digests. - `release-manifest.json` (schema `abastevo-release-manifest-v1`) contains the commit SHA, creation time, one `image@digest` per role, landing bundle SHA-256, highest migration version and the triggering workflow-run URL. It is signed keyless with cosign and published as a workflow artifact and as the OCI artifact `ghcr.io//abastevo-release:sha-` plus `staging`. `schema_min_supported` is deferred to Phase 3, where the runtime schema contract is introduced; publishing a placeholder would misstate compatibility. - The final workflow step prints the exact `cosign verify-blob` command with the workflow certificate identity at `refs/heads/main` and the GitHub OIDC issuer. - `scripts/check-release-workflow.sh` rejects unpinned actions, `pull_request_target`, `pull_request`, non-`GITHUB_TOKEN` secrets, `set -x`, environment dumps, echo of variables and `latest` tags. It runs from Quick. - `.github/CODEOWNERS` explicitly assigns `.github/workflows/` and `infra/deploy/` to the owner. The default owner entry remains. - `backend.yml` path selection includes the release workflow and the guard so the backend gate runs when this pipeline changes. ## Release gate semantics - Automatic runs listen to completed `quick`, `CI` and `backend` runs on `main`. The gate requires a successful `push` run for all three workflows for the same `head_sha` before any build job starts. - Optional path-filtered workflows that did not run for a commit leave the release gate closed with no artifact; the gate does not fabricate success. A run that exists but is not successful closes the gate with an error. - Manual dispatch requires `refs/heads/main` and a full 40-hex SHA that is an ancestor of the checked-out `main`; it does not use an environment or a secret. - Minimal permissions are declared per job: read-only by default, with `packages: write`, `id-token: write` and `attestations: write` only where image publication/signing/attestation needs them. `actions: read` is limited to the gate query. No secret beyond the automatic `GITHUB_TOKEN` is used. ## Local validation performed - `actionlint v1.7.12` (downloaded from the pinned release asset and verified against the published SHA-256 checksum) passes for the release workflow and for all workflows. - The guard passes the delivered workflow and rejects synthetic fixtures for each forbidden pattern: unpinned action, `pull_request_target`, `pull_request`, non-token secret, `set -x`, variable echo, `env`/`printenv` dump and `latest` tag. - `bash -n` passes for the guard and `quick-verify.sh`; `git diff --check` passes. - A local manifest fixture generated with the workflow's `jq` expression and validation predicate produced the expected schema, all three digest-pinned image references, a valid landing SHA-256 and `migrations_max` `000132`. - `actionlint` also passes `release-staging.yml` after `shellcheck` analysis of its embedded scripts. ## Known constraints recorded before merge - Branch protection currently requires only `Quick verification`. `CI` and `backend` are path-selected workflow signals, not protection-required checks. The release gate requires all three when they run; a commit that does not trigger one of them does not produce a release. - CODEOWNERS is present, but the repository setting `require_code_owner_reviews` is false. Enforcing owner approval is a repository-settings decision outside this change. - GHCR package visibility is not changed by the workflow. Anonymous pulls and `gh attestation verify` for the published artifacts are verified after the first real `main` run; a package still private is reported, not worked around with a personal token. ## Post-merge run 1 PR #158 merged as `ef86cc93`. `quick`, `CI` and `backend` succeeded on that `main` commit. The three `release-staging` runs created by their completion (`40485613772`, `40486612237`, `40487056660`) failed in the gate's `oras-project/setup-oras` step before any artifact was published: `setup-oras` v2.0.2 only bundles ORAS releases through 1.3.4, so the requested 1.3.6 was rejected. The fix pins the CLI to the bundled 1.3.4 release; the workflow logic and permissions are unchanged. ## Post-merge run 2 PR #159 merged as `2cd8f0ac`. `quick`, `CI` and `backend` succeeded on that `main` commit, and the automatic gate built and published the release in run `40494565772`: - Manifest SHA `2cd8f0ac20da6f04e79885d9b5b03f8ceb79cb19`; landing SHA-256 `957d1a49a56aca588a5c83ff21dc9b016b5a0b37b2e5c002b7117017c1c3c213`; `migrations_max` `000132`. - Images: api `sha256:044222cf...`, worker `sha256:e64e482e...`, migrate `sha256:074ccf44...` (full references in the signed manifest). - `cosign verify-blob` with the workflow identity at `refs/heads/main` and the GitHub OIDC issuer: `Verified OK`. - `gh attestation verify` passed for the manifest and for all three image digests. - Anonymous `docker manifest inspect` passed for all three images, and an anonymous `oras pull` of `abastevo-release:sha-` returned the manifest and Sigstore bundle. The log scan found only GitHub runner cache/tool metadata and Docker loopback network setup; no operator host, deployment address, secret or variable dump. The landing job's setup-node step was replaced by a preinstalled-Node version check so its runner cache paths no longer appear; that log-clean rerun is pending. ## Post-merge verification checklist Run against the real `main` release artifacts (results to be reported on the delivery PR, not inferred from local state): ```sh cosign verify-blob \ --bundle release-manifest.sigstore.json \ --certificate-identity "https://github.com/AlexandreZanata/Abastevo/.github/workflows/release-staging.yml@refs/heads/main" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ release-manifest.json gh attestation verify release-manifest.json --repo AlexandreZanata/Abastevo gh attestation verify oci://ghcr.io/alexandrezanata/abastevo-api@ --repo AlexandreZanata/Abastevo gh attestation verify oci://ghcr.io/alexandrezanata/abastevo-worker@ --repo AlexandreZanata/Abastevo gh attestation verify oci://ghcr.io/alexandrezanata/abastevo-migrate@ --repo AlexandreZanata/Abastevo docker pull ghcr.io/alexandrezanata/abastevo-api@ ``` The run must also show no host, IP, operator path or variable dump in its logs. Phase 2 (deploy agent, VPS access, rollout) is not started.