ci: run the gate before the deploy, not after it (#70)

* ci: run the gate before the deploy, not after it

master is the live branch — every app fetches its config from
raw.githubusercontent.com/.../master/... — and the flow was arranged so that content
reached it first and was checked afterwards.

auto-pr.yml fired on a push to *master* and opened a master → main PR. Code Quality ran
on that PR, i.e. on the way into the mirror, long after the config was already being
served. A check that reports "the live config is broken" is not a gate.

The same inversion made master unwritable by the front door: five required status
checks, and no workflow triggering on a PR into master, so the only way to change the
live branch was to bypass its own protection. That is not a hypothetical — it was
bypassed twice on 2026-08-10, and the second time was to undo the first.

Now: work lands on main, Code Quality decides, and promote-to-live moves master to the
exact commit that passed. It refuses anything that is not an ancestor of main, and uses
--force-with-lease so a master that moved underneath it is a failure rather than a
silent overwrite. main already ran Code Quality on push, so no trigger change is needed.

Also: each daily sync opened a PR and nothing closed the previous one. Thirty-one
branches spanning 2026-02-10 to 2026-08-08 were cleared by hand on 2026-08-11, every one
superseded by the next day's run. A sync is a snapshot of upstream, so an older open
sync PR is never the right thing to merge — it is noise that hides whether anything is
genuinely waiting. The sync now closes what it supersedes.

Branch protection still needs moving in the same direction and cannot be done from a
commit: main requires no PR, no approvals and no checks, while master carries the five
checks that can never run there. The settings change is proposed separately.

* ci: require an admin's approval before anything reaches the live branch

main is where work lands and where Code Quality decides; promote-to-live then moves
master to whatever passed, and every app reads master directly. An approval on main is
therefore the last human judgement before a change is served to wallets in the field —
and until now main required no review at all.

One approval, from either admin. Requiring two would mean two of two, which stalls
whenever one of them is the author.
This commit is contained in:
2026-08-11 05:39:28 -07:00
committed by GitHub
parent 80fb0544ed
commit 6d96f82ceb
5 changed files with 100 additions and 90 deletions
+14
View File
@@ -0,0 +1,14 @@
# Who has to approve before anything reaches the live branch.
#
# main is where work lands and where Code Quality decides; promote-to-live then moves
# master to whatever passed, and every app reads master directly. So an approval here is
# the last human judgement before a change is being served to wallets in the field.
#
# One approval is enough, but it has to come from one of these two. Requiring two
# approvals would have meant requiring two of two, which stalls whenever one of them is
# the author.
#
# Read-access collaborators cannot approve, so adding a reviewer here also means giving
# them write access.
* @SatoshiQaziMuhammed @QaziMuhammedKochgiri
-48
View File
@@ -1,48 +0,0 @@
name: Auto Merge
on:
workflow_run:
workflows: ["Code Quality"]
types: [completed]
jobs:
auto-merge:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
permissions:
contents: write
pull-requests: write
steps:
- name: Find and merge master → main PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
HEAD_BRANCH="${{ github.event.workflow_run.head_branch }}"
echo "Workflow ran on branch: $HEAD_BRANCH"
# Only merge PRs from master to main
if [ "$HEAD_BRANCH" != "master" ]; then
echo "Not a master branch PR, skipping"
exit 0
fi
PR_NUMBER=$(gh pr list \
--repo "$GITHUB_REPOSITORY" \
--base main \
--head master \
--state open \
--json number \
--jq '.[0].number')
if [ -z "$PR_NUMBER" ]; then
echo "No open PR from master to main found, skipping"
exit 0
fi
echo "Merging PR #$PR_NUMBER"
gh pr merge "$PR_NUMBER" \
--repo "$GITHUB_REPOSITORY" \
--merge \
--delete-branch=false
-40
View File
@@ -1,40 +0,0 @@
name: Auto PR (master → main)
on:
push:
branches: [master]
jobs:
create-pr:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create or update PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Check if there's already an open PR from master to main
EXISTING_PR=$(gh pr list --base main --head master --state open --json number --jq '.[0].number')
if [ -n "$EXISTING_PR" ]; then
echo "PR #$EXISTING_PR already exists — new commits will appear automatically"
exit 0
fi
echo "Creating new PR: master → main"
if gh pr create \
--base main \
--head master \
--title "Sync: master → main" \
--body "Automated PR to sync master branch changes to main.
This PR was created automatically and will be merged once CI checks pass."; then
echo "PR created successfully"
else
echo "PR creation skipped (branches may already be in sync)"
fi
+65
View File
@@ -0,0 +1,65 @@
name: Promote main → master
# master is the live branch: every app reads its config straight from
# raw.githubusercontent.com/.../master/... . Nothing should reach it that has not
# passed Code Quality first.
#
# The flow used to run the other way. auto-pr.yml fired on a push to *master* and
# opened a master → main PR, so content was already live before a single check ran —
# the gate sat downstream of the thing it was meant to gate. A check that reports
# "the live config is broken" is not a gate.
#
# It also made master unwritable by the front door: five checks were required there and
# no workflow triggered on a PR into master, so the only way to change the live branch
# was to bypass its own protection. A rule that can only be bypassed teaches people to
# bypass it.
#
# Now: work lands on main, Code Quality decides, and only then is master moved to match.
on:
workflow_run:
workflows: ["Code Quality"]
types: [completed]
permissions:
contents: write
jobs:
promote:
# Only a green run, only on main, and only for a real push — a pull_request run
# tests a merge commit that does not exist on main yet.
if: >
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.event == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout the exact commit that passed
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
- name: Refuse to promote a commit that is not on main
run: |
git fetch origin main
if ! git merge-base --is-ancestor "${{ github.event.workflow_run.head_sha }}" origin/main; then
echo "::error::${{ github.event.workflow_run.head_sha }} is not an ancestor of main — refusing"
exit 1
fi
echo "Commit is on main."
- name: Promote
run: |
SHA="${{ github.event.workflow_run.head_sha }}"
# --force-with-lease, not --force: if master has moved since this job read it,
# the push is refused rather than silently discarding whatever moved it.
git push --force-with-lease origin "$SHA":refs/heads/master
echo "master is now $SHA"
echo "### Promoted to live" >> "$GITHUB_STEP_SUMMARY"
echo "\`master\` → \`$SHA\`" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Apps read this branch directly; the change is live as soon as their config refreshes." >> "$GITHUB_STEP_SUMMARY"
+21 -2
View File
@@ -100,7 +100,26 @@ jobs:
sync
dependencies
- name: Auto-merge if tests pass
# Each daily run opens a new sync PR. Nothing closed the previous one, so they
# accumulated: 31 branches spanning 2026-02-10 to 2026-08-08 were cleared out by
# hand on 2026-08-11, every one of them superseded by the next day's run.
#
# A sync is a snapshot of upstream. The newest one contains everything the older
# ones did, so an older open sync PR is never the right thing to merge — it is
# noise that hides whether anything is genuinely waiting for review.
- name: Close superseded sync PRs
if: steps.check_output.outputs.output_changed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KEEP: sync/nova-base-${{ env.NOVA_COMMIT }}
run: |
echo "PR created. Review and merge to apply Nova updates."
gh pr list --repo "$GITHUB_REPOSITORY" --state open --label sync \
--json number,headRefName --jq '.[] | @base64' | while read -r row; do
decoded=$(echo "$row" | base64 -d)
NUM=$(echo "$decoded" | python3 -c 'import json,sys; print(json.load(sys.stdin)["number"])')
HEAD=$(echo "$decoded" | python3 -c 'import json,sys; print(json.load(sys.stdin)["headRefName"])')
[ "$HEAD" = "$KEEP" ] && continue
echo "Closing #$NUM ($HEAD) — superseded by $KEEP"
gh pr close "$NUM" --repo "$GITHUB_REPOSITORY" --delete-branch \
--comment "Superseded by \`$KEEP\`. A sync is a snapshot of upstream, so the newer one contains everything this did."
done