diff --git a/.github/workflows/post-merge-verify.yml b/.github/workflows/post-merge-verify.yml new file mode 100644 index 0000000..41d3263 --- /dev/null +++ b/.github/workflows/post-merge-verify.yml @@ -0,0 +1,114 @@ +name: post-merge-verify +# P2-6(ADR-0041):自动合并的核心安全绳——合并到 main 后自动冒烟,失败自动 +# revert(auto-merge 回滚)。把"事前人审"实质替换为"事后快速回滚":坏变更在 +# main 的存活时间从"天"压缩到约 10 分钟级(验证+gate)。 +on: + push: + branches: [main] + workflow_call: + inputs: + smoke: + description: "业务仓自定义冒烟命令(缺省跑治理仓自检:YAML/JSON/脚本语法)" + required: false + default: "" + +permissions: + contents: read + +jobs: + smoke: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: 冒烟验证(治理仓自检 / 业务仓自定义) + run: | + set -o pipefail + if [ -n "${{ inputs.smoke }}" ]; then + bash -c '${{ inputs.smoke }}' + exit $? + fi + python3 - <<'EOF' + import glob, json, sys, yaml + files = glob.glob("governance/**/*.yaml", recursive=True) + glob.glob("standards/**/*.yaml", recursive=True) + for f in files: + yaml.safe_load(open(f, encoding="utf-8")) + print(f"OK yaml x{len(files)}") + jfiles = sorted(glob.glob("governance/rulesets/*.json")) + ["governance/expected-state.json"] + for f in jfiles: + d = json.load(open(f, encoding="utf-8")) + assert isinstance(d, dict), f"{f} 顶层须为对象" + print(f"OK json x{len(jfiles)}") + EOF + for s in governance/apply.sh governance/drift-check.sh scripts/new-repo-init.sh scripts/gh-app-token.sh; do + bash -n "$s" + done + echo "OK post-merge smoke" + + revert: + # 冒烟失败 → 自动 revert(防回环双闸:revert 不嵌套 revert;每仓每小时最多 1 次) + needs: smoke + if: failure() + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write # revert 分支创建(REST revert 端点服务端建 ref) + pull-requests: write # revert PR + auto-merge + issues: write # 降级告警通道 + steps: + - name: 判定防回环闸 + id: guard + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + HEAD_MSG: ${{ github.event.head_commit.message }} + run: | + # 闸 1:本次 commit 本身是 revert 类(标题含 [auto-revert])→ 不再嵌套 + if grep -q "\[auto-revert\]" <<<"$HEAD_MSG"; then + echo "nested=true" >> "$GITHUB_OUTPUT" + fi + # 闸 2:本仓近 1 小时内已有 revert PR → 不重复(限频 1/h) + RECENT=$(gh api "repos/$REPO/pulls?state=all&sort=created&direction=desc&per_page=20" \ + --jq '[.[] | select(.title | test("\\[auto-revert\\]")) | select(.created_at > (now - 3600 | todateiso8601))] | length') + echo "recent=$RECENT" >> "$GITHUB_OUTPUT" + echo "nested=$(grep -q '\[auto-revert\]' <<<"$HEAD_MSG" && echo true || echo false)" >> "$GITHUB_OUTPUT" + - name: App 令牌(AG-1 身份——revert PR 的 gate 须能被触发;GITHUB_TOKEN 造的 PR 不触发工作流) + id: app + continue-on-error: true # App 未安装本仓时降级为仅告警(不静默失败——下方有判定) + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: 4632704 + private-key: ${{ secrets.AGENT_APP_SECRET }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: write + permission-pull-requests: write + - name: 自动 revert(REST revert 端点 + auto-merge) + if: steps.guard.outputs.nested != 'true' && steps.guard.outputs.recent == '0' && steps.app.outcome == 'success' + env: + GH_TOKEN: ${{ steps.app.outputs.token }} + REPO: ${{ github.repository }} + SHA: ${{ github.sha }} + run: | + set -o pipefail + PRN=$(gh api "repos/$REPO/commits/$SHA/pulls?per_page=5" --jq 'if length > 0 then .[0].number else "" end') + if [ -z "$PRN" ]; then + echo "HEAD commit 无关联 PR(直推?drift-check §8 已另行执法)——仅告警" + exit 3 + fi + TITLE="[auto-revert] #$PRN:post-merge 冒烟失败(run ${{ github.run_id }})" + RESP=$(gh api -X POST "repos/$REPO/pulls/$PRN/revert" -f title="$TITLE" \ + -f body="post-merge-verify 冒烟失败,自动回滚(ADR-0041)。原 PR #$PRN,commit ${SHA:0:8},失败 run:${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}" \ + --jq '.number') + echo "revert PR #$RESP 已建,enable auto-merge" + gh pr merge "$RESP" --repo "$REPO" --auto --squash + - name: 降级/兜底告警(revert 不可用或被闸拦) + if: failure() || steps.app.outcome != 'success' || steps.guard.outputs.nested == 'true' || steps.guard.outputs.recent != '0' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + gh issue create --repo "$REPO" --title "P0: post-merge 冒烟失败且自动 revert 未执行(run ${{ github.run_id }})" --body "合并 ${SHA:-HEAD} 后冒烟失败。自动 revert 未执行的原因:App 令牌不可用(未安装本仓?)/ 防回环闸(嵌套 revert 或 1h 限频)/ 无关联 PR。人工复核并回滚:${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}(ADR-0041)" --label auto-revert-alert || \ + gh issue create --repo "$REPO" --title "P0: post-merge 冒烟失败且自动 revert 未执行(run ${{ github.run_id }})" --body "同上(label 创建失败兜底)" \ No newline at end of file