From 10814e68ae273f83538fe604252593fce46dab83 Mon Sep 17 00:00:00 2001 From: Jeppe B <2jepp9350@gmail.com> Date: Thu, 16 Jul 2026 12:33:44 +0200 Subject: [PATCH] Prepare API default branch protection --- .github/BRANCH_PROTECTION.md | 49 +++++++++++++++++ .github/rulesets/protect-default-branch.json | 57 ++++++++++++++++++++ .github/workflows/code_quality.yml | 8 +-- .github/workflows/tests.yml | 47 +++++++++++++++- README.md | 5 ++ 5 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 .github/BRANCH_PROTECTION.md create mode 100644 .github/rulesets/protect-default-branch.json diff --git a/.github/BRANCH_PROTECTION.md b/.github/BRANCH_PROTECTION.md new file mode 100644 index 00000000..1b86ce9d --- /dev/null +++ b/.github/BRANCH_PROTECTION.md @@ -0,0 +1,49 @@ +# Default branch protection + +`master` is changed through pull requests. Do not push or publish directly to +the default branch, including through automation or the Git Data API. + +## Normal publishing flow + +1. Create a scoped `agent/*` or feature branch from the current `origin/master`. +2. Commit and push only the intended changes. +3. Open a pull request targeting `master`. +4. Wait for the `Required CI` check. If `master` moves, update the branch and + wait for the strict check to rerun. +5. Resolve every review conversation and squash-merge the pull request. +6. Confirm the post-merge `Release Manager gate` completes on `master`. + +The aggregate check covers the PHP unit, integration, API, and legacy matrix, +plus Edge Agent, Edge Broker, and Edge Gateway Backend. Qodana is advisory and +the Release Manager gate is intentionally post-merge. + +## Desired ruleset + +[`rulesets/protect-default-branch.json`](rulesets/protect-default-branch.json) +is the importable final desired-state repository-ruleset request body. For the +initial POST, copy the file and override `enforcement` to `disabled`. Inspect +the normalized ruleset and verify a green preparation PR and post-merge run, +then PUT the exact committed file to activate it. + +The desired rule targets `~DEFAULT_BRANCH`, requires pull requests with zero +approvals, conversation resolution, strict `Required CI` from GitHub Actions +integration `15368`, squash-only linear history, and blocks deletion and force +pushes. Repository administrators receive pull-request-only bypass; they do not +receive a standing direct-push bypass. + +When the ruleset is activated, align repository settings at the same time: +retain squash merging, disable merge commits and rebase merging, enable +auto-merge and branch-update suggestions, delete merged branches automatically, +keep the Actions token read-only, and prevent Actions from approving reviews. + +## Break glass + +When an incident cannot wait for the normal gate: + +1. Open a pull request and describe the incident, risk, and reason for bypass. +2. Have a repository administrator use the pull-request-only bypass. +3. Monitor `Required CI` and the post-merge Release Manager workflow. +4. Open a follow-up pull request for any deferred validation or remediation. + +Never bypass by updating `refs/heads/master` directly. Ruleset changes and +emergency bypasses must remain visible in GitHub's audit trail. diff --git a/.github/rulesets/protect-default-branch.json b/.github/rulesets/protect-default-branch.json new file mode 100644 index 00000000..03164588 --- /dev/null +++ b/.github/rulesets/protect-default-branch.json @@ -0,0 +1,57 @@ +{ + "name": "Protect default branch", + "target": "branch", + "enforcement": "active", + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "pull_request" + } + ], + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "~DEFAULT_BRANCH" + ] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "required_linear_history" + }, + { + "type": "pull_request", + "parameters": { + "allowed_merge_methods": [ + "squash" + ], + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_approving_review_count": 0, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "do_not_enforce_on_create": false, + "required_status_checks": [ + { + "context": "Required CI", + "integration_id": 15368 + } + ], + "strict_required_status_checks_policy": true + } + } + ] +} diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 45f54109..73654317 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -3,9 +3,11 @@ on: workflow_dispatch: pull_request: push: - branches: # Specify your branches here - - main # The 'main' branch - - 'releases/*' # The release branches + branches: + - master + - beta + - canary + - internal jobs: qodana: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 05e3ff71..789c9b03 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,6 +3,18 @@ name: Tests on: pull_request: push: + branches: + - master + - beta + - canary + - internal + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: php: @@ -336,11 +348,42 @@ jobs: if: always() run: docker compose -f docker-compose.yml -f .github/docker-compose.ci.yml down -v + required-ci: + name: Required CI + runs-on: ubuntu-latest + needs: [php, edge-agent, edge-broker, edge-gateway-backend] + if: ${{ always() }} + + steps: + - name: Verify required jobs succeeded + env: + PHP_RESULT: ${{ needs.php.result }} + EDGE_AGENT_RESULT: ${{ needs.edge-agent.result }} + EDGE_BROKER_RESULT: ${{ needs.edge-broker.result }} + EDGE_GATEWAY_BACKEND_RESULT: ${{ needs.edge-gateway-backend.result }} + run: | + set -euo pipefail + failed=0 + for dependency in \ + "php=${PHP_RESULT}" \ + "edge-agent=${EDGE_AGENT_RESULT}" \ + "edge-broker=${EDGE_BROKER_RESULT}" \ + "edge-gateway-backend=${EDGE_GATEWAY_BACKEND_RESULT}" + do + name="${dependency%%=*}" + result="${dependency#*=}" + if [ "$result" != "success" ]; then + echo "Required dependency ${name} completed with result: ${result:-missing}" >&2 + failed=1 + fi + done + test "$failed" -eq 0 + release-manager-gate: name: Release Manager gate runs-on: [self-hosted, Linux, X64, pleno, backend] - needs: [php, edge-agent, edge-broker, edge-gateway-backend] - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + needs: [required-ci] + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.required-ci.result == 'success' }} steps: - name: Record Release Manager API gate diff --git a/README.md b/README.md index 9007c42e..0df9c8d3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ Backend API for Copenhagen Truck Wash services. +Changes are published from a scoped feature branch through a pull request to +`master`; direct default-branch pushes are not part of the release workflow. +See [default branch protection](.github/BRANCH_PROTECTION.md) for the CI gate +and emergency procedure. + ## Architecture & Stack - **Edge Proxy:** [Traefik 2.11](https://doc.traefik.io/traefik/) (Handles TLS termination, routing, and rate limiting). - **Web Server:** [Caddy 2.7](https://caddyserver.com/) (Serves the PHP application via FastCGI).