From 1a27abca0d9ffdcd8846a24de4aed4ee8ca4ecb8 Mon Sep 17 00:00:00 2001 From: Takwa Date: Tue, 13 Oct 2026 13:35:24 +0200 Subject: [PATCH 1/2] Apply platform.yml only after the repo checks pass On a merge, ci.yml and platform.yml started side by side, so a commit could be approved and applied to the boxes while its lint, syntax or manifest checks were failing. ci.yml is now also a reusable workflow; platform.yml calls it for the commit and its apply job needs it, so the approval request only appears once every check has passed. A manual apply waits for them too. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 2 ++ .github/workflows/platform.yml | 19 +++++++++++++++++-- README.md | 7 ++++--- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 911fcc0..938a8dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ on: branches: [main] pull_request: workflow_dispatch: + # platform.yml runs these first and only applies if they all pass. + workflow_call: permissions: contents: read diff --git a/.github/workflows/platform.yml b/.github/workflows/platform.yml index 96ff9e7..bf524be 100644 --- a/.github/workflows/platform.yml +++ b/.github/workflows/platform.yml @@ -4,9 +4,11 @@ # monitoring"). The service playbooks still run from an operator's machine. # # pull request -> dry run (--check --diff) against the boxes -# merge to main -> the real run, once approved in the `production` +# merge to main -> the repo checks (ci.yml), then, only if they all pass, +# the real run, once approved in the `production` # environment (required reviewers) -# manual -> either, optionally limited to some boxes +# manual -> either, optionally limited to some boxes; apply also +# waits for the checks # # Pull requests from forks get no secrets and are skipped. name: platform @@ -62,6 +64,16 @@ env: ANSIBLE_FORCE_COLOR: "true" jobs: + # The same checks as ci.yml (lint, syntax, manifests, generated files, + # shellcheck), for this commit. The apply needs them, so a commit that fails + # them is never offered for approval. They run a second time on a merge, + # alongside ci.yml's own run: jobs cannot wait on another workflow. + checks: + if: >- + (github.event_name == 'push') || + (github.event_name == 'workflow_dispatch' && inputs.mode == 'apply') + uses: ./.github/workflows/ci.yml + check: if: >- (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || @@ -88,6 +100,9 @@ jobs: run: scripts/ci-platform.sh check "$LIMIT" apply: + needs: checks + # No status function here, so GitHub adds success(): every job of the + # checks must have passed. if: >- (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && inputs.mode == 'apply') diff --git a/README.md b/README.md index 4cb292c..6399d70 100644 --- a/README.md +++ b/README.md @@ -1451,8 +1451,8 @@ does; the others still run from an operator's machine: | When | What | |---|---| | Pull request touching it | `--check --diff` against the boxes (forks get no secrets and are skipped) | -| Merge to `main` | the real run, once a required reviewer approves it in the `production` environment | -| Actions → platform → Run workflow | `check` or `apply`, optionally `--limit`ed | +| Merge to `main` | the repo checks (`ci.yml`) for that commit, then, only if they all pass, the real run, once a required reviewer approves it in the `production` environment | +| Actions → platform → Run workflow | `check` or `apply` (apply also waits for the checks), optionally `--limit`ed | It restarts nothing that serves traffic, so it runs every box in one batch: the hub registers the others using what they produced earlier in the same run. @@ -1528,7 +1528,8 @@ same path a deploy takes), so what is checked is what would be applied: | `shellcheck scripts/*.sh` | the usual shell traps | `platform.yml` also has its own workflow, which does talk to the boxes (see -"Argo CD and monitoring"). +"Argo CD and monitoring"). It calls this one (`ci.yml` is also a reusable +workflow) and only applies once every job here has passed. The Cloudflare range check is a separate weekly workflow rather than part of this one, because it reaches `api.cloudflare.com`: an outage there should not From f6bcbeaa108e37fa4251d8d9f0618d80f8730855 Mon Sep 17 00:00:00 2001 From: Takwa Date: Tue, 13 Oct 2026 13:36:15 +0200 Subject: [PATCH 2/2] Keep pull request dry runs from cancelling a pending apply Dry runs and applies shared one concurrency group, and GitHub keeps only one pending run per group: pushing a pull request branch cancelled the apply of the #2 merge while it waited for approval. Each job now has its own group: dry runs per branch (a newer push replaces the older), applies in platform-apply (never cancelled by a pull request). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/platform.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/platform.yml b/.github/workflows/platform.yml index bf524be..329860a 100644 --- a/.github/workflows/platform.yml +++ b/.github/workflows/platform.yml @@ -52,11 +52,6 @@ on: permissions: contents: read -# One run at a time against the boxes; never cancel one half-way. -concurrency: - group: platform - cancel-in-progress: false - env: # ansible.cfg sets the yaml callback, which no installed collection provides # here (see ci.yml). @@ -79,6 +74,12 @@ jobs: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || (github.event_name == 'workflow_dispatch' && inputs.mode == 'check') runs-on: ubuntu-latest + # A newer push to the same branch replaces its dry run. Kept apart from the + # apply's group: GitHub keeps one pending run per group, so a shared group + # let a pull request's dry run cancel an apply waiting for approval. + concurrency: + group: platform-check-${{ github.ref }} + cancel-in-progress: true steps: - uses: actions/checkout@v4 with: @@ -107,6 +108,11 @@ jobs: (github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && inputs.mode == 'apply') runs-on: ubuntu-latest + # One apply at a time, never cancelled half-way. A newer merge waiting + # behind it replaces an older one still waiting: the latest main wins. + concurrency: + group: platform-apply + cancel-in-progress: false # Required reviewers: nothing reaches the boxes until one approves. environment: production steps: