diff --git a/.gitea/workflows/backend-quality-gate.yml b/.gitea/workflows/backend-quality-gate.yml index fa643207..e0dbb8ff 100644 --- a/.gitea/workflows/backend-quality-gate.yml +++ b/.gitea/workflows/backend-quality-gate.yml @@ -1,4 +1,4 @@ -name: Staging Backend Quality Gate +name: Independent Staging Quality Gate on: pull_request: diff --git a/deploy/README.md b/deploy/README.md index 3093841e..150497c3 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -154,7 +154,7 @@ Staging is isolated from production: Gitea is the primary source repository and Actions control plane. Gitea automatically injects the per-job `${{ secrets.GITEA_TOKEN }}` token; its access is limited by each workflow's `permissions` block and it must not be configured as a repository secret. Configure repository Actions secrets `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`, and `STAGING_SSH_PRIVATE_KEY`, plus variables `STAGING_HOST`, `STAGING_PORT`, `STAGING_USER`, `STAGING_PATH`, `STAGING_URL`, and `STAGING_KNOWN_HOSTS`. `STAGING_SSH_PRIVATE_KEY` must be the private-key file encoded as one unwrapped base64 line (for example, `base64 < key | tr -d '\n'`), not a multiline PEM/OpenSSH value; staging workflows decode it only into a mode-`0600` temporary file and validate it with `ssh-keygen`. Staging is an independent test line and is not required to equal or remain inside `main` history. A push to `staging` runs the exact-SHA quality gate; its publish job creates immutable API/web image digests plus an allowlisted controller bundle from that same staging SHA, then explicitly dispatches `Deploy staging` from `refs/heads/staging`. The deploy workflow validates the source gate run, consumes only that gate-attested artifact, rejects stale normal releases, and never checks out or executes an untested branch controller. The staging key, database, Resend key, and model-provider keys must not be shared with production. Staging image publishing has no Supabase build variables. GitHub workflows are upstream/mirror fallback only, not the normal staging release path. -`Staging Backend Quality Gate` runs for relevant `pull_request` paths, pushes to `staging`, and `workflow_dispatch`. It validates the Python/database/frontend contract; only a successful push to `staging` publishes the API/web images and a run-bound artifact containing their `sha256` digests plus the allowlisted controller bundle. The publish job rechecks the current staging head and dispatches `.gitea/workflows/deploy-staging.yml` from `refs/heads/staging` with the exact SHA and source gate run ID. The deploy workflow waits for that gate's success, validates the artifact against the full 40-character commit, and deploys digest references rather than trusting discoverability tags. +`Independent Staging Quality Gate` runs for relevant `pull_request` paths, pushes to `staging`, and `workflow_dispatch`. It validates the Python/database/frontend contract; only a successful push to `staging` publishes the API/web images and a run-bound artifact containing their `sha256` digests plus the allowlisted controller bundle. The publish job rechecks the current staging head and dispatches `.gitea/workflows/deploy-staging.yml` from `refs/heads/staging` with the exact SHA and source gate run ID. The deploy workflow waits for that gate's success, validates the artifact against the full 40-character commit, and deploys digest references rather than trusting discoverability tags. The staging env file must include these non-secret selectors so Compose cannot fall back to production paths: @@ -174,13 +174,13 @@ After source sync and before `up`, the workflow validates `.env.staging` mode/se 1. Complete the server and Gitea bootstrap: create both mode-`0600` env files, preload the reviewed `postgres:17-alpine` image, and configure the listed Actions variables/secrets. No repository-level Supabase variables are required. Deployment and migration workflows use `--pull never` for PostgreSQL, so database image upgrades remain an explicit operator-controlled maintenance action rather than an application-deploy side effect. 2. Push the reviewed test revision directly to `staging`. It may advance independently of `main`; do not merge or reset `main` merely to satisfy staging release mechanics. -3. The `Staging Backend Quality Gate` runs for that push and, when successful, publishes API/web images plus an artifact binding the exact SHA to both immutable image digests and the allowlisted staging controller bundle. +3. The `Independent Staging Quality Gate` runs for that push and, when successful, publishes API/web images plus an artifact binding the exact SHA to both immutable image digests and the allowlisted staging controller bundle. 4. The successful publish job rechecks that `staging` still points at the exact SHA, then dispatches `Deploy staging` from `refs/heads/staging` with the source gate run ID. The deploy workflow waits for that gate's final success, validates the artifact, and performs the normal forward-only release under the shared staging host lock. 5. If environment validation fails, fix the server-side env files without committing or copying secrets, then manually rerun `Deploy staging` using **Use workflow from: staging**, the same successful SHA in `deploy_sha`, an empty `gate_run_id`, and `allow_rollback=false`; the workflow resolves a successful exact-SHA staging push gate before mutation. 6. If the read-only checker reports a pending migration, stop app deployment and run `Migrate Staging Database` manually using **Use workflow from: staging** with the same full SHA. Migration success does not dispatch deployment. 7. After migration succeeds, manually start `Deploy staging` from `staging` with that same exact SHA and `allow_rollback=false`, then confirm `https://staging.jyotisha.chat/api/health` reports it and private API health. -Application rollback uses the same workflow: manually dispatch `Deploy staging` using **Use workflow from: staging** with a previous known-good full SHA that has a successful `Staging Backend Quality Gate` push run, leave `gate_run_id` empty, and explicitly set `allow_rollback=true`. The requested SHA must be an ancestor of the current `staging` head. Normal deployments reject stale or divergent revisions. Rollback still consumes the selected gate run's digest and controller artifact and is supported only during that artifact's 30-day retention window; after expiry, stop and prepare a separately reviewed republish/recovery change rather than substituting a mutable tag or assuming the old run can still be rerun. Database migrations are separate and are not rolled back by an application deployment. Restore a staging database backup before running any destructive migration rehearsal. +Application rollback uses the same workflow: manually dispatch `Deploy staging` using **Use workflow from: staging** with a previous known-good full SHA that has a successful `Independent Staging Quality Gate` push run, leave `gate_run_id` empty, and explicitly set `allow_rollback=true`. The requested SHA must be an ancestor of the current `staging` head. Normal deployments reject stale or divergent revisions. Rollback still consumes the selected gate run's digest and controller artifact and is supported only during that artifact's 30-day retention window; after expiry, stop and prepare a separately reviewed republish/recovery change rather than substituting a mutable tag or assuming the old run can still be rerun. Database migrations are separate and are not rolled back by an application deployment. Restore a staging database backup before running any destructive migration rehearsal. Inspect staging without printing secrets: @@ -241,7 +241,7 @@ PostgreSQL is private: `deploy/docker-compose.postgres.yml` has no `ports` mappi Use this order for every staging revision: 1. Review the test change, then push its exact commit directly to `staging`; `main` may remain at a different SHA. -2. Wait for `Staging Backend Quality Gate` to pass and publish that exact full SHA's API/web digest and controller artifact. Its publish job dispatches the staging-ref deployment and refuses dispatch if `staging` already advanced. +2. Wait for `Independent Staging Quality Gate` to pass and publish that exact full SHA's API/web digest and controller artifact. Its publish job dispatches the staging-ref deployment and refuses dispatch if `staging` already advanced. 3. The dispatched `Deploy staging` workflow validates the source gate run and checks the exact SHA in read-only migration-check mode before changing API, web, or Caddy. If it reports pending or drifted migrations, stop; do not retry the application deployment as if it were a migration. 4. Open **Migrate Staging Database -> Run workflow**, select **Use workflow from: staging**, and enter the reported full lowercase 40-character SHA in `deploy_sha`. The workflow requires the current `staging` head and a successful exact-SHA staging gate, starts only PostgreSQL, and runs the digest-pinned migrator from the gate-attested controller bundle. 5. A successful migration rechecks that `staging` still points at the same exact SHA and prints the ordered migration ledger, but does not dispatch deployment. The operator must then open **Deploy staging -> Run workflow**, select **Use workflow from: staging**, enter the same exact SHA in `deploy_sha`, leave `gate_run_id` empty, and set `allow_rollback=false`. If `staging` advanced, stop rather than substituting a branch name, short SHA, or newer commit. diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index c443e628..e699a955 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -3046,7 +3046,7 @@ - 用户现象:staging 作为测试分支需要领先或偏离 main 时,旧部署工作流仍要求 main 与 staging 同一 SHA,且 `workflow_run` 从默认 main 加载控制器,导致 staging-only 变更无法按自身已测试工作流发布。 - 触发条件:`staging` 推送了尚未进入 `main` 的测试提交并完成 quality gate。 - 根因:staging 发布把 production 的 reviewed-main 收敛约束复用到了测试环境,同时依赖默认分支的 `workflow_run` controller;即使删除 SHA 相等检查,旧 main controller 仍可能继续执行旧门禁。 -- 修复:staging push gate 在发布同一 exact-SHA 的不可变镜像与 allowlisted controller bundle 后,显式从 `refs/heads/staging` dispatch `Deploy staging`,并传入源 gate run ID;deploy 等待并验证该 gate 最终成功,正常发布仍要求当前 staging HEAD,回滚仍要求当前 staging history 中的旧成功 gate SHA。staging migration 只要求当前 staging HEAD 和 exact-SHA gate artifact。main 与 production workflow 均不改动。 +- 修复:将 staging gate 重命名为 `Independent Staging Quality Gate`,使默认 main 上遗留的 `workflow_run` 监听器不再匹配并抢占 `staging-mutation` 并发组;staging push gate 在发布同一 exact-SHA 的不可变镜像与 allowlisted controller bundle 后,显式从 `refs/heads/staging` dispatch `Deploy staging`,并传入源 gate run ID。deploy 等待并验证该 gate 最终成功,正常发布仍要求当前 staging HEAD,回滚仍要求当前 staging history 中的旧成功 gate SHA。staging migration 只要求当前 staging HEAD 和 exact-SHA gate artifact。main 与 production workflow 均不改动。 - 验证:工作流契约测试锁定 staging-ref dispatch、源 gate run 证明、无 main 引用、当前 HEAD 防陈旧发布、同一 gate artifact 的 controller/digest 校验和回滚祖先限制;远端 gate/deploy 与运行时 SHA 待本次 staging 发布记录。 -- 防复发:测试环境的部署控制器必须来自被同一 quality gate 证明的 staging SHA;不得重新引入 `workflow_run` 默认分支控制器或 staging/main 相等门禁。production 继续保持独立的 main/staging 收敛要求。 +- 防复发:测试环境的部署控制器必须来自被同一 quality gate 证明的 staging SHA;staging gate 名称不得恢复为默认 main 遗留监听器匹配的 `Staging Backend Quality Gate`,也不得重新引入 `workflow_run` 默认分支控制器或 staging/main 相等门禁。production 继续保持独立的 main/staging 收敛要求。 - 修复版本:本次 staging workflow 提交 diff --git a/frontend/tests/staging-backend-workflows.test.ts b/frontend/tests/staging-backend-workflows.test.ts index 896ba5be..b2dc3b57 100644 --- a/frontend/tests/staging-backend-workflows.test.ts +++ b/frontend/tests/staging-backend-workflows.test.ts @@ -588,6 +588,8 @@ test("Gitea staging deployment is dispatched from staging after the exact push g const quality = read(giteaQualityWorkflow); const workflow = read(giteaDeployWorkflow); + assert.match(quality, /^name: Independent Staging Quality Gate$/m); + assert.doesNotMatch(quality, /^name: Staging Backend Quality Gate$/m); assert.match(workflow, /^on:\n\s+workflow_dispatch:/m); assert.doesNotMatch(workflow, /workflow_run:|read_ref_sha main|refs\/heads\/main|reviewed main/); assert.match(workflow, /gate_run_id:/);