From e45a5c98b09c1dd9a244eba5961eea3fd0cf132a Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Tue, 28 Apr 2026 17:54:15 -0700 Subject: [PATCH] fix(ci): auto-promote-staging opens a PR + uses merge queue, not direct push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the fix #2234 applied to auto-sync-main-to-staging.yml in the reverse direction. Both workflows now use the same merge-queue path that humans use; no special-case bypass. Why Every tick of auto-promote-staging.yml since main's branch protection went stricter has been failing with: remote: error: GH006: Protected branch update failed for refs/heads/main. remote: - Required status checks "Analyze (go)", "Analyze (javascript-typescript)", "Analyze (python)", "Canvas (Next.js)", "Detect changes", "E2E API Smoke Test", "Platform (Go)", "Python Lint & Test", and "Shellcheck (E2E scripts)" were not set by the expected GitHub apps. remote: - Changes must be made through a pull request. The previous version did `git merge --ff-only origin/staging && git push origin main` directly. That works against a permissive branch — it doesn't work against a ruleset that requires checks satisfied by the expected GitHub apps. Only PR merges through the queue produce check runs from the right apps. Result was that today's 12+ merges to staging never propagated to main; the auto-promote ran every tick and failed every tick, while operators had to keep opening manual `staging → main` bridges. Fix - Replace the direct git push step with a step that opens (or reuses) a PR base=main head=staging and enables auto-merge. The merge queue lands it once gates are green on the merge_group ref. - The PR's head IS the staging branch (no per-SHA promote branch needed) — the whole purpose is "advance main to staging's tip". - Add `pull-requests: write` permission so the workflow can call gh pr create + gh pr merge --auto. - Drop the `git merge-base --is-ancestor` divergence check — the merge queue itself enforces branch protection now, and rejects the PR if main has diverged from staging history. Loop safety preserved: when this PR's merge lands on main, it triggers auto-sync-main-to-staging.yml which opens a sync PR back to staging. That sync PR's eventual merge is by GITHUB_TOKEN (the merge queue) which doesn't trigger downstream workflow_run events — so auto-promote-staging.yml does NOT re-fire from its own merge landing. Refs: #2234 (the parallel fix for auto-sync-main-to-staging.yml), task #142, multiple failing runs visible in https://github.com/Molecule-AI/molecule-core/actions/workflows/auto-promote-staging.yml --- .github/workflows/auto-promote-staging.yml | 123 +++++++++++++-------- 1 file changed, 74 insertions(+), 49 deletions(-) diff --git a/.github/workflows/auto-promote-staging.yml b/.github/workflows/auto-promote-staging.yml index 53946c95..f8191ce7 100644 --- a/.github/workflows/auto-promote-staging.yml +++ b/.github/workflows/auto-promote-staging.yml @@ -1,25 +1,44 @@ name: Auto-promote staging → main # Fires after any of the staging-branch quality gates complete. When ALL -# required gates are green on the same staging SHA, fast-forwards `main` -# to that SHA automatically — closing the gap that historically let -# features sit on staging for weeks waiting for a bulk promotion PR -# (see molecule-core#1496 for the 1172-commit example). +# required gates are green on the same staging SHA, opens (or re-uses) +# a PR `staging → main` and enables auto-merge so the merge queue lands +# it. Closes the gap that historically let features sit on staging for +# weeks waiting for a bulk promotion PR (see molecule-core#1496 for the +# 1172-commit example). +# +# 2026-04-28 rewrite (PR #142): the previous version did a direct +# `git merge --ff-only origin staging && git push origin main`. That +# breaks against main's branch-protection ruleset, which requires +# status checks "set by the expected GitHub apps" — direct pushes +# can't satisfy that condition (only PR merges through the queue can). +# The workflow was failing every tick with: +# remote: error: GH006: Protected branch update failed for refs/heads/main. +# remote: - Required status checks ... were not set by the expected GitHub apps. +# Fix: mirror the PR-based pattern from auto-sync-main-to-staging.yml +# (the reverse-direction sync, fixed in #2234 for the same reason). +# Both directions now use the same merge-queue path that humans use, +# no special-case bypass. # # Safety model: # - Runs ONLY on workflow_run events for the staging branch. # - Requires EVERY named gate workflow to have the same head_sha and # all be `conclusion == success`. If any of them is red, skipped, # cancelled, or pending, we abort (stay on the current main). -# - Uses --ff-only: refuses to advance main if main has diverged from -# the staging history (e.g. a hotfix landed directly on main). In -# that case a human resolves the fork. -# - Writes a commit summary so the promote shows up in git log as a -# deliberate act, not a stealth move. +# - The PR base=main head=staging path lets GitHub itself enforce +# branch protection. If main has diverged from staging or required +# checks aren't satisfied, the merge queue declines the PR — no +# need for a manual ff-only ancestry check here. +# - Loop safety: the auto-sync-main-to-staging workflow fires when +# main lands the auto-promote PR, but its merge into staging is by +# GITHUB_TOKEN which doesn't trigger downstream workflow_run events +# (GitHub Actions safety). So this workflow doesn't re-fire from +# its own promote landing. # -# **Initial rollout:** ship this file but leave the `enabled` input set -# such that nothing auto-promotes until staging CI has been reliably -# green for a few days. Toggle via repo variable `AUTO_PROMOTE_ENABLED`. +# Toggle via repo variable AUTO_PROMOTE_ENABLED (true/unset). When +# unset, the workflow logs what it would have done but doesn't open +# the PR — useful for dry-running the gate logic without surfacing +# a noisy PR while staging CI is still flaky. on: workflow_run: @@ -38,6 +57,7 @@ on: permissions: contents: write + pull-requests: write jobs: check-all-gates-green: @@ -134,14 +154,14 @@ jobs: set -eu # Repo variable AUTO_PROMOTE_ENABLED=true flips this on. While # it's unset, the workflow dry-runs (logs what it would have - # done) but doesn't actually push to main. Set the variable in + # done) but doesn't open the promote PR. Set the variable in # Settings → Secrets and variables → Actions → Variables. if [ "${AUTO_PROMOTE_ENABLED:-}" != "true" ] && [ "${FORCE_INPUT:-false}" != "true" ]; then { echo "## ⏸ Auto-promote disabled" echo echo "Repo variable \`AUTO_PROMOTE_ENABLED\` is not set to \`true\`." - echo "All gates are green on staging; would have promoted to \`main\`." + echo "All gates are green on staging; would have opened a promote PR to \`main\`." echo echo "To enable: Settings → Secrets and variables → Actions → Variables → \`AUTO_PROMOTE_ENABLED=true\`." echo "To test once manually: workflow_dispatch with \`force=true\`." @@ -150,50 +170,55 @@ jobs: exit 0 fi - - name: Checkout main - if: ${{ vars.AUTO_PROMOTE_ENABLED == 'true' || github.event.inputs.force == 'true' }} - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - ref: main - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Fast-forward main → staging HEAD + - name: Open (or reuse) staging → main promote PR + enable auto-merge if: ${{ vars.AUTO_PROMOTE_ENABLED == 'true' || github.event.inputs.force == 'true' }} env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} TARGET_SHA: ${{ needs.check-all-gates-green.outputs.head_sha }} run: | - set -eu - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + set -euo pipefail - git fetch origin staging - git fetch origin main + # Look for an existing open promote PR (idempotent on re-run + # of the workflow). The PR's head IS the staging branch — the + # whole point is "advance main to staging's tip", so we don't + # need a per-SHA branch like auto-sync-main-to-staging uses. + PR_NUM=$(gh pr list --repo "$REPO" \ + --base main --head staging --state open \ + --json number --jq '.[0].number // ""') - # Refuse to advance main if it's diverged from staging history. - # Someone landed a commit directly on main that's not on - # staging → human needs to decide how to reconcile. - if ! git merge-base --is-ancestor "$(git rev-parse origin/main)" "$TARGET_SHA"; then - { - echo "## ❌ Auto-promote refused — main has diverged" - echo - echo "\`main\` (\`$(git rev-parse --short origin/main)\`) is not an ancestor of staging (\`${TARGET_SHA:0:7}\`)." - echo "Someone committed directly to main or the histories forked." - echo - echo "Resolve manually: merge main into staging, get CI green on the merged commit," - echo "then the auto-promote will succeed on the next run." - } >> "$GITHUB_STEP_SUMMARY" - exit 1 + if [ -z "$PR_NUM" ]; then + TITLE="staging → main: auto-promote ${TARGET_SHA:0:7}" + BODY_FILE=$(mktemp) + cat > "$BODY_FILE" <&1; then + echo "::warning::Failed to enable auto-merge on PR #${PR_NUM} — operator may need to merge manually." + fi { - echo "## ✅ Auto-promoted main → ${TARGET_SHA:0:7}" + echo "## ✅ Auto-promote PR opened" echo - echo "All gate workflows green on staging at this SHA." - echo "\`main\` fast-forwarded to match." + echo "- Source: staging at \`${TARGET_SHA:0:8}\`" + echo "- PR: #${PR_NUM}" + echo + echo "Merge queue lands the PR once required gates are green; no human action needed unless gates fail." } >> "$GITHUB_STEP_SUMMARY"