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..329860a 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 @@ -50,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). @@ -62,11 +59,27 @@ 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) || (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: @@ -88,10 +101,18 @@ 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') 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: 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