Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 34 additions & 2 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' ||
Expand Down Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand Down Expand Up @@ -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."
54 changes: 54 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
45 changes: 42 additions & 3 deletions docs/CI-CD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading