diff --git a/.github/workflows/cd-staging.yml b/.github/workflows/cd-staging.yml index 922c4887..0a7f2cfb 100644 --- a/.github/workflows/cd-staging.yml +++ b/.github/workflows/cd-staging.yml @@ -4,8 +4,6 @@ name: Publish - Staging Candidate Image on: - push: - branches: [main] workflow_dispatch: permissions: @@ -21,6 +19,7 @@ concurrency: jobs: validate-candidate: name: Validate candidate and publish attested image + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 90 diff --git a/.github/workflows/verify-staging-provenance.yml b/.github/workflows/verify-staging-provenance.yml index 6eb2face..fdc3cdd4 100644 --- a/.github/workflows/verify-staging-provenance.yml +++ b/.github/workflows/verify-staging-provenance.yml @@ -1,4 +1,4 @@ -# Independently verifies the published OCI subject from a successful main push. +# Independently verifies the OCI subject from a successful manual main publish. # This workflow does not deploy staging and cannot authorize production. name: Verify - Staging Image Provenance @@ -19,7 +19,7 @@ jobs: name: Verify publisher identity and OCI subject if: >- github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.event == 'push' && + github.event.workflow_run.event == 'workflow_dispatch' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest diff --git a/data_agent/test_staging_candidate_evidence.py b/data_agent/test_staging_candidate_evidence.py index 8c13cbe4..7649492a 100644 --- a/data_agent/test_staging_candidate_evidence.py +++ b/data_agent/test_staging_candidate_evidence.py @@ -161,12 +161,18 @@ def test_staging_workflow_is_candidate_validation_not_fake_deployment(): text = path.read_text(encoding="utf-8") workflow = yaml.safe_load(text) jobs = workflow["jobs"] + triggers = workflow.get("on", workflow.get(True)) assert workflow["name"] == "Publish - Staging Candidate Image" + assert triggers == {"workflow_dispatch": None} assert set(jobs) == {"validate-candidate", "candidate-summary"} assert "deploy-staging" not in text assert "Ready for production" not in text assert "environment: staging" not in text + assert jobs["validate-candidate"]["if"] == ( + "github.event_name == 'workflow_dispatch' && " + "github.ref == 'refs/heads/main'" + ) validation_commands = "\n".join( step.get("run", "") for step in jobs["validate-candidate"]["steps"] diff --git a/data_agent/test_staging_provenance_evidence.py b/data_agent/test_staging_provenance_evidence.py index 978b8a82..3eede75d 100644 --- a/data_agent/test_staging_provenance_evidence.py +++ b/data_agent/test_staging_provenance_evidence.py @@ -274,7 +274,7 @@ def test_protected_workflow_verifies_and_attests_without_deploying(): assert job["environment"] == "staging-provenance" assert "workflow_run" in text assert 'workflows: ["Publish - Staging Candidate Image"]' in text - assert "github.event.workflow_run.event == 'push'" in job["if"] + assert "github.event.workflow_run.event == 'workflow_dispatch'" in job["if"] assert "github.event.workflow_run.head_branch == 'main'" in job["if"] assert "head_repository.full_name == github.repository" in job["if"] checkout = steps[0] diff --git a/docs/architecture-decisions/adr-033-protected-staging-provenance-verification.md b/docs/architecture-decisions/adr-033-protected-staging-provenance-verification.md index c0db5e56..781ad9d4 100644 --- a/docs/architecture-decisions/adr-033-protected-staging-provenance-verification.md +++ b/docs/architecture-decisions/adr-033-protected-staging-provenance-verification.md @@ -4,6 +4,8 @@ **Date**: 2026-07-26 +**Updated**: 2026-07-26 after ADR-035 mainline recovery + **Decision owners**: Platform Architecture, SRE, Security **Related decisions**: ADR-029、ADR-031、ADR-032 @@ -22,7 +24,7 @@ GitHub CLI 可以把 repository、signer workflow、source ref/digest、signer d - verifier 必须固定 source repository、`main`、完整 SHA 和 signer workflow,调用方不能放宽策略; - self-hosted publisher、错误 OIDC issuer、错误 predicate 或不同 OCI subject 必须 fail closed; - verification evidence 自身需要独立 workflow identity,但仍不能声明 staging deployment 或 production promotion; -- `workflow_run` 的高权限边界必须只接受本仓库 `main` push 的成功 publisher run。 +- `workflow_run` 的高权限边界必须只接受本仓库 `main` 上人工触发且成功的 publisher run。 ## Considered Options @@ -38,7 +40,7 @@ GitHub CLI 可以把 repository、signer workflow、source ref/digest、signer d 1. 新增 `data_agent.staging_provenance_evidence`。它先验证 registry evidence schema、fingerprint、source/candidate/local image ID、GHCR repository/digest 和所有非 promotion flag,再决定是否调用外部 verifier。 2. verifier 固定执行 `gh attestation verify`,同时约束:`--repo`、`--signer-workflow`、`--signer-digest`、`--source-ref refs/heads/main`、`--source-digest`、GitHub OIDC issuer、SLSA v1 predicate 和 `--deny-self-hosted-runners`。调用方只能提供本仓库 slug 与 publisher run SHA。 3. 命令必须成功且 JSON 中至少一个 verified statement 的 subject name/digest 精确匹配 `repository@sha256:`。原始 attestation bundle、证书和命令 stderr 不复制到 GDA evidence。 -4. 新增 `verify-staging-provenance.yml`,只响应 `Publish - Staging Candidate Image` 的成功、本仓库、`main`、`push` 类型 `workflow_run`。它 checkout publisher 的精确 SHA,并进入 `staging-provenance` environment。 +4. `Publish - Staging Candidate Image` 只允许 `workflow_dispatch`,且 publisher job 明确要求 `refs/heads/main`。`verify-staging-provenance.yml` 只响应该 workflow 的成功、本仓库、`main`、`workflow_dispatch` 类型 `workflow_run`。它 checkout publisher 的精确 SHA,并进入 `staging-provenance` environment。禁止通过普通 `main` push 隐式发布 candidate。 5. environment 必须设置 environment-level `GDA_STAGING_PROVENANCE_PROTECTED=true`,并在 GitHub 中配置 required reviewers、禁止无审核 bypass。变量只是显式启用开关,不能替代 GitHub environment protection rule。 6. verifier workflow 仅拥有 `actions: read`、`contents: read`、`packages: read`、`id-token: write` 和 `attestations: write`;验证结果由该独立 workflow 再生成 artifact attestation,成功后才上传。 7. `provenance_verified` 允许 `provenance_attestation_verified=true` 与 `registry_digest_verified=true`,但固定 `staging_deployed=false`、`live_cluster_verified=false` 和 `production_promotion_allowed=false`。release bundle 消费前仍须验证 provenance evidence artifact identity。 @@ -55,13 +57,13 @@ GitHub CLI 可以把 repository、signer workflow、source ref/digest、signer d - publisher 与 verifier 仍位于同一仓库;在独立 trusted builder/reusable workflow 建立前,安全性依赖 main branch protection、CODEOWNERS 和 environment reviewers; - GitHub environment protection 不能由仓库 YAML 自证,必须在远端配置并留存设置证据; -- 截至本决策,本机 GitHub 登录、仓库 admin/push 和 Actions 权限已恢复并验证,但本地分支未 push,environment 未配置,publisher/verifier 均未真实运行; +- `staging-provenance` environment、required reviewer、禁止 admin bypass 和 environment-level 显式变量已配置;publisher/verifier 尚未完成一次成功的真实运行; - verifier 只证明 OCI provenance,不证明 release manifest、Secret、目标集群、live config 或 golden slice。 ## Verification - 测试覆盖精确 command policy、成功 subject、registry/source/fingerprint 漂移、空与错误 subject、CLI 故障脱敏和非 promotion flags; -- workflow 合同测试固定 `workflow_run` 来源、main push、本仓库、environment、最小权限、artifact run-id、GHCR login、verification-attestation-upload 顺序和无部署命令; +- workflow 合同测试固定 publisher 只能手工从 main 触发,并固定 verifier 的 `workflow_run` 来源、main、`workflow_dispatch`、本仓库、environment、最小权限、artifact run-id、GHCR login、verification-attestation-upload 顺序和无部署命令; - GitHub CLI 2.92.0 help 与官方实现源码已复核,确认 signer/source digest、source ref、OIDC issuer 和 runner environment 分别映射到证书扩展; - 真实 GHCR subject 验证仍是未完成的外部验收项。 diff --git a/docs/architecture-decisions/adr-035-github-mainline-history-recovery.md b/docs/architecture-decisions/adr-035-github-mainline-history-recovery.md index d7c4625b..656c797f 100644 --- a/docs/architecture-decisions/adr-035-github-mainline-history-recovery.md +++ b/docs/architecture-decisions/adr-035-github-mainline-history-recovery.md @@ -1,6 +1,6 @@ # ADR-035:GitHub Mainline 历史恢复与受保护主线切换 -**Status**: Proposed +**Status**: Accepted **Date**: 2026-07-26 @@ -40,26 +40,87 @@ | force push 当前 HEAD 到 `main` | 最快 | 覆盖默认分支、破坏可恢复性与审计边界 | 拒绝 | | `--allow-unrelated-histories` 合并并保留当前 tree | 两条历史在图上相连 | PR 表现为 4,859 文件/500 万行替换,merge resolution 不可可信评审 | 拒绝 | | 重写 645 个活跃提交,把新 root graft 到旧 `main` | 得到单一线性祖先 | 所有 commit SHA 变化,现有 branches/artifacts/文档引用失效 | 拒绝 | -| 归档旧 `main`,保护两条分支后把活跃 `feat/v12` lineage 改名为 canonical `main` | 不重写、不丢历史;当前 25 提交可在真实共同祖先上评审 | 主线存在一次显式 lineage discontinuity,需要仓库管理员分阶段切换 | 推荐,待批准 | +| 归档旧 `main`,保护两条分支后把活跃 `feat/v12` lineage 改名为 canonical `main` | 不重写、不丢历史;变更可在真实共同祖先上评审 | 主线存在一次显式 lineage discontinuity,需要仓库管理员分阶段切换 | 已采用 | -## Proposed Decision +## Decision 采用“归档旧主线 + 提升活跃 lineage”,禁止 force push、synthetic unrelated merge 和全历史重写。 1. 先建立同时覆盖现有 `main`、`feat/v12-extensible-platform` 和未来 `main` 的 repository ruleset:禁止 force push/delete,要求 PR,限制 bypass;required checks 在确认新主线 CI check name 后收紧。 2. 创建 `staging-provenance` environment,配置 required reviewers、禁止未经审核 bypass,并仅设置 environment-level `GDA_STAGING_PROVENANCE_PROTECTED=true`。在主线切换完成前不运行 publisher。 3. 在远端创建不可变审计锚点:`archive/main-2026-04-10` 和 annotated tag 指向 `f339e13`;另为切换前的 active lineage 创建 archive ref。不得删除旧对象。 -4. 将当前 25 个提交 push 到独立 review branch,并以 `feat/v12-extensible-platform` 为 base 评审;因为两者共享 `ebd99f8`,该 PR 是真实增量,不涉及 unrelated history。 +4. 将初始 25 个提交 push 到独立 review branch,并以 `feat/v12-extensible-platform` 为 base 评审;因为两者共享 `ebd99f8`,该 PR 是真实增量,不涉及 unrelated history。评审期间增加 7 个 CI、依赖和兼容修复,最终 PR 固定为 32 个提交。 5. 合入并验证后,把旧 `main` 改名为 `legacy/main-2026-04-10`,再把已验证的 `feat/v12-extensible-platform` 改名为 `main`,确认 GitHub default branch、ruleset target、workflow source ref 和本地 remote-tracking ref 全部指向新 canonical branch。 6. 先运行只读 CI/合同验证,再按 ADR-032 至 ADR-034 的顺序进行首次 candidate publish、protected provenance verify 和 release materialization。未取得真实 artifact 前不得部署 staging。 7. 将 lineage discontinuity、旧/新 root、切换人、时间、GitHub audit log 和最终 SHA 追加到本 ADR;完成复核后才能把状态改为 Accepted。 +## Execution Record + +Repository owner `zhouning` approved and completed the recovery on +2026-07-26. The externally verifiable record is: + +- ruleset `19761373` protects only `refs/heads/main`, prohibits deletion and + non-fast-forward updates, requires a PR with resolved review threads, and + requires `Platform Required Tests`, `Frontend Build`, and + `Route Evaluation Contract (PR)` with strict branch freshness; +- ruleset `19761446` prohibits deletion and non-fast-forward updates for + `archive/main-2026-04-10`, + `archive/active-lineage-pre-ar1-2026-07-26`, and + `legacy/main-2026-04-10`; ruleset `19761509` similarly protects the archive + tag; +- `staging-provenance` environment `18764854504` requires reviewer + `zhouning`, disables admin bypass, restricts deployment to protected + branches, and holds environment-level + `GDA_STAGING_PROVENANCE_PROTECTED=true`; +- `archive/main-2026-04-10` and `legacy/main-2026-04-10` both point to old + main `f339e132cd9563d2016fb0e5d4d00cdc85f8b8ba`; annotated tag + `pre-mainline-recovery-2026-07-26` resolves to the same commit; +- `archive/active-lineage-pre-ar1-2026-07-26` and the review merge base both + point to `ebd99f81664c8db850bbd4f48c551b4572746b10`; +- PR [#2](https://github.com/zhouning/gisdataagent/pull/2) reviewed 32 commits + on that shared ancestry. CI run + [30199394234](https://github.com/zhouning/gisdataagent/actions/runs/30199394234) + passed all required checks at head + `df7a17acf5361ed0fb5ea3db88aa214a5e82e9ca`; +- GitHub created merge commit + `eaa626649f067220ebd02d629a04613dd40e218f` with parents `ebd99f8` and + `df7a17a`. No squash, rebase, unrelated-history merge, force push, or history + rewrite was used; +- old `main` was renamed to `legacy/main-2026-04-10`, the merged + `feat/v12-extensible-platform` branch was renamed to `main`, and the GitHub + default branch now resolves to `main@eaa6266`; +- post-switch CI run + [30199837779](https://github.com/zhouning/gisdataagent/actions/runs/30199837779) + passed `Frontend Build`, `Platform Required Tests`, and the credentialed + `Agent Evaluation (Full)`. Its artifact reports 4/4 pipeline files passed; + optimization, governance, general, and planner each recorded a `1.0` pass + rate against thresholds `0.6`, `0.6`, `0.7`, and `0.5` respectively; +- the local remote fetch refspec was restored from the obsolete single-branch + refspec to the standard all-branches refspec, and `origin/HEAD` now resolves + to `origin/main`. + +GitHub emitted `push` events during the branch renames. Publisher run +[30199822852](https://github.com/zhouning/gisdataagent/actions/runs/30199822852) +for the old SHA created no jobs. Publisher run +[30199837791](https://github.com/zhouning/gisdataagent/actions/runs/30199837791) +for the new SHA was cancelled during dependency installation: candidate +credentials, database bootstrap, application image build, GHCR login, image +push, registry binding, and attestation were all skipped. GitHub deployments +remained at zero. The follow-up protected change therefore makes candidate +publication manual-only and updates the verifier to accept only a successful +`workflow_dispatch` run from `main`. + +The resulting state is **configured but not published and not deployed**. +Candidate publication, protected provenance verification, release +materialization, staging deployment, and production promotion remain separate +future approval gates. + ## Consequences 正面影响: - 两套 Git 对象都保留,旧主线可查、可恢复; -- AR-0/AR-1 的 25 个提交能在共同祖先上正常评审; +- AR-0/AR-1 及评审修复的 32 个提交已在共同祖先上正常评审; - workflow 中固定的 `refs/heads/main` 最终对应真实活跃代码; - ruleset 和 environment 先于首次发布建立,不再依赖当前无保护状态。 @@ -73,12 +134,16 @@ ## Verification Plan - 切换前记录 `default_branch`、两个 root SHA、old/new head SHA、ruleset、environment 和 workflow 清单; -- 证明 review branch 与 `feat/v12-extensible-platform` 的 merge base 是 `ebd99f8`,且只包含预期 25 个提交; +- 证明 review branch 与 `feat/v12-extensible-platform` 的 merge base 是 `ebd99f8`,且最终只包含预期 32 个提交; - 证明 archive branch/tag 精确指向 `f339e13`,且禁止删除/force update; - 切换后确认 `main` 是活跃 lineage 的 descendant,旧历史仍可通过 archive ref 获取; - 确认 `main` ruleset、`staging-provenance` reviewers/variable、Actions permissions 和 workflow source 均符合 ADR-033/034; - 首次远端运行只允许产生 candidate/registry/provenance/release evidence,不直接触发 production。 -## Approval Gate +## Approval -本 ADR 保持 Proposed,直到 repository owner 明确批准以下外部动作:创建 ruleset/environment/archive refs、push review branch、合并到 active lineage、重命名两条远端分支和切换 default branch。批准前继续保持本地提交,不 push。 +Repository owner approval was applied stepwise to ruleset/environment/archive +creation, review-branch publication, PR merge, both branch renames, default +branch selection, and final ruleset tightening. All verification items above +were re-read from GitHub after mutation. This ADR is Accepted for mainline +recovery only; it does not approve candidate publication or any deployment.