diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index 38159091..2a0a38ca 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -251,3 +251,23 @@ jobs: # No-ops instantly on the GitHub-hosted fallback (script check). if: always() && needs.ci-target.outputs.homeserver == 'true' run: scripts/prune-buildx-cache.sh "/var/buildx-cache/${{ matrix.image }}" + + # #3311: the one required status check for this workflow. Matrix row names + # carry a "(GitHub-hosted)" suffix on fallback days, so no row is a stable + # context for main's ruleset; this job is. always() so a failed router or + # row still reports a red gate instead of leaving the check pending. + containers-gate: + name: Containers gate + if: always() + needs: [ci-target, build] + runs-on: ubuntu-latest + steps: + - name: Fail unless the router and every image row succeeded + env: + ROUTER: ${{ needs.ci-target.result }} + BUILD: ${{ needs.build.result }} + run: | + if [ "$ROUTER" != "success" ] || [ "$BUILD" != "success" ]; then + echo "::error::ci-target=$ROUTER build=$BUILD" + exit 1 + fi diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index f1544d1e..fae4d780 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -22,6 +22,26 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} + # #3311: under semver, 0.y.z has no stability promise -- a 0.9 -> 0.10 + # bump is a major change, and Cargo's caret rules treat it as one. + # fetch-metadata still labels it semver-minor, which is how rand + # 0.9 -> 0.10 and sha2 0.10 -> 0.11 qualified for auto-merge and landed + # on main with a broken backend build (#3287, #3289). Hold any update + # that moves a 0.x dependency's minor version (grouped PRs: any member). + - name: Hold 0.x minor bumps for manual review + id: zero-major + env: + DEPS: ${{ steps.metadata.outputs.updated-dependencies-json }} + run: | + held="$(jq -r '[.[] | select(.updateType == "version-update:semver-minor" + and ((.prevVersion // "") | startswith("0."))) + | "\(.dependencyName) \(.prevVersion) -> \(.newVersion)"] | join(", ")' <<<"${DEPS:-[]}")" + if [ -n "$held" ]; then + echo "hold=true" >>"$GITHUB_OUTPUT" + echo "held=$held" >>"$GITHUB_OUTPUT" + echo "::notice::0.x minor bump(s), not auto-merging: $held" + fi + - name: Check out the PR head if: >- steps.metadata.outputs.update-type == 'version-update:semver-patch' || @@ -88,7 +108,8 @@ jobs: (steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor') && steps.image-pull.outcome != 'failure' && - steps.lockfile-check.outcome != 'failure' + steps.lockfile-check.outcome != 'failure' && + steps.zero-major.outputs.hold != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_URL: ${{ github.event.pull_request.html_url }} @@ -99,7 +120,8 @@ jobs: (steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor') && steps.image-pull.outcome != 'failure' && - steps.lockfile-check.outcome != 'failure' + steps.lockfile-check.outcome != 'failure' && + steps.zero-major.outputs.hold != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_URL: ${{ github.event.pull_request.html_url }} @@ -140,3 +162,13 @@ jobs: run: | gh pr comment "$PR_URL" --body \ "Not auto-merging: pulling node:22-alpine failed (registry rate limit, transient error, or a runner network hiccup) before the lockfile could even be tested. This is not a #2741 lockfile problem -- re-run this workflow (or push a new commit) to retry." + + - name: Leave the 0.x minor bump for a human + if: steps.zero-major.outputs.hold == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.html_url }} + HELD: ${{ steps.zero-major.outputs.held }} + run: | + gh pr comment "$PR_URL" --body \ + "Not auto-merging: this moves the minor version of a 0.x dependency ($HELD). Under semver that is a breaking change even though Dependabot labels it semver-minor (#3311). Check the changelog and merge by hand once the required checks pass." diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index c554819e..0a345d53 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -1681,3 +1681,57 @@ jobs: - name: Fail if any check failed if: needs.scripts-and-compose.result != 'success' run: exit 1 + + # #3311: the one required status check for this workflow. main's ruleset + # requires a context that reports on every PR; the jobs above come in + # homeserver/GitHub-hosted pairs whose names change with the executor, so + # none of them individually is a stable context. Mirrors + # go-modules-complete's rule for every pair: the homeserver twin succeeded, + # or it was skipped and its GitHub-hosted twin succeeded. A new job pair + # must be added to both `needs:` and PAIRS below, or it is not gated. + quality-gate: + name: Quality gate + if: always() + needs: + - ci-target + - public-safety + - public-safety-cloud + - design-lab-readonly + - design-lab-readonly-cloud + - go-modules-complete + - frontend-next + - frontend-next-cloud + - frontend-next-browser + - frontend-next-browser-cloud + - backend-service + - backend-service-cloud + - vendored-theme + - vendored-theme-cloud + - scripts-and-compose-complete + runs-on: ubuntu-latest + steps: + - name: Fail unless every gated job, or its fallback twin, succeeded + env: + NEEDS: ${{ toJSON(needs) }} + shell: bash + run: | + result() { jq -r --arg j "$1" '.[$j].result' <<<"$NEEDS"; } + fail=0 + single() { + local r; r="$(result "$1")" + [[ "$r" == "success" ]] && return 0 + echo "::error::$1: expected success, got $r"; fail=1 + } + pair() { + local hs cloud; hs="$(result "$1")"; cloud="$(result "$1-cloud")" + [[ "$hs" == "success" ]] && return 0 + [[ "$hs" == "skipped" && "$cloud" == "success" ]] && return 0 + echo "::error::$1: expected success or skip+fallback-success; got $hs / $cloud"; fail=1 + } + single ci-target + single go-modules-complete + single scripts-and-compose-complete + for p in public-safety design-lab-readonly frontend-next frontend-next-browser backend-service vendored-theme; do + pair "$p" + done + exit "$fail" diff --git a/docs/CI-CD.md b/docs/CI-CD.md index d16d34d6..0d47b9f7 100644 --- a/docs/CI-CD.md +++ b/docs/CI-CD.md @@ -98,15 +98,54 @@ without a directory move. Go stays co-located; only Python and shell use ## Dependabot -Dependabot checks GitHub Actions, Go modules, npm dependencies, and Docker base +Dependabot checks GitHub Actions, Go modules, Cargo, npm, pip, and Docker base images every week. Patch and minor Dependabot pull requests are approved and -placed into GitHub's auto-merge queue. They still wait for branch protection -and all required checks; major upgrades always require manual review. +placed into GitHub's auto-merge queue, where they wait for the `main` ruleset's +required checks (below). Major upgrades always require manual review, and so +does any minor bump of a `0.x` dependency: semver gives `0.y` no stability +promise, but `dependabot/fetch-metadata` still labels it `semver-minor` +(#3287 `rand` 0.9 → 0.10 and #3289 `sha2` 0.10 → 0.11 broke the backend +build that way, #3311). + +Auto-merge only waits if something is required. With no protection on the +base branch, GitHub considers a PR mergeable at once and `gh pr merge --auto` +merges it on the spot, before CI has run. That is what happened before the +ruleset existed (#3311). The repository setting **Allow auto-merge** and the Actions permission **Allow GitHub Actions to create and approve pull requests** must remain enabled for this workflow. +## `main` ruleset (#3311) + +`main` is protected by the repository ruleset **main protection**: + +- changes land through a pull request (no direct pushes), with no required + approving review (single maintainer); +- no force-push and no branch deletion; +- these status checks must pass, and are the only required ones: + +| context | workflow | what it aggregates | +|---|---|---| +| `Quality gate` | `quality.yml` | every Quality job pair, homeserver or GitHub-hosted twin | +| `Containers gate` | `containers.yml` | the router and every image build row | +| `Go formatting and tests` | `quality.yml` | Go fmt + tests, either executor | +| `Scripts and Compose` | `quality.yml` | the `scripts-and-compose` job matrix | + +Individual jobs are never required directly: their names change with the +executor (`… (GitHub-hosted)` on fallback days), and a required context that +never reports leaves every PR pending forever. When adding a job pair to +`quality.yml`, add both twins to `quality-gate`'s `needs:` and the pair name +to its `pair` loop, or the new job is not gated. A skipped job counts as +passing for GitHub, which is why each gate checks results itself instead of +relying on skip semantics. + +Required checks are strict=false (a PR does not have to be rebased onto the +latest `main` before merging). In an emergency, a repository admin can set the +ruleset's enforcement to *Disabled* (Settings → Rules) and must re-enable it +afterwards; there is deliberately no standing bypass actor, since automation +merges with the owner's token. + ## Pull request workflow ### Stacked PRs and `Closes #N` (#2922)