Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/cd-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
name: Publish - Staging Candidate Image

on:
push:
branches: [main]
workflow_dispatch:

permissions:
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/verify-staging-provenance.yml
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions data_agent/test_staging_candidate_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion data_agent/test_staging_provenance_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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。
Expand All @@ -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 验证仍是未完成的外部验收项。

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-035:GitHub Mainline 历史恢复与受保护主线切换

**Status**: Proposed
**Status**: Accepted

**Date**: 2026-07-26

Expand Down Expand Up @@ -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 先于首次发布建立,不再依赖当前无保护状态。

Expand All @@ -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.
Loading