Skip to content

Branch Protection

What blocks a merge, and what only reports

Running a check and being able to block on it are different things. This page covers the second: the branch rulesets, which checks each one requires, and the settings that decide how a merge is allowed to land. The checks themselves are documented on their own pages — Supply-Chain Pinning, Dependency Scanning and the Coverage Floor — and the controls index is the one-page summary of where each holds.

All three repositories' rulesets were read from the API again on 27 September 2026 and had not moved since 19 September, when all three acc rulesets gained the build and deploy checks: each now requires the build and deploy checks alongside audit and scan, so a red test suite blocks a merge to acc rather than only stopping the deploy. The main rulesets did not move. What made that possible was not a ruleset edit alone — see How a path-filtered workflow became requireable.

What blocks a merge

Running a check and being able to block on it are different things, and until August 2026 these repositories only did the first. All three now carry a branch ruleset named acc supply-chain gate, active on refs/heads/acc with no bypass actors. All three share the two rules that matter:

  • required_status_checks → the audit context must pass, and since 19 September 2026 scan and each repository's build/deploy checks alongside it
  • pull_request → a pull request is required (0 approvals; these repositories have a single maintainer, and GitHub does not permit self-approval)

Both rules are needed together — requiring the status check alone would still let a direct push to acc sail past it. The practical effect is that git push origin acc is rejected outright in all three repositories, including for releases and including for the repository owner. Linked Data Explorer adopted the same ruleset in v2026.08.7; all three are named acc supply-chain gate and carry zero bypass actors.

They are not identical in shape. Read per ruleset from the API on 27 September 2026:

acc main
CPSV Editor pull request, audit, scan, Build and deploy ACC a pull request, no status checks — classic branch protection, not a ruleset
Linked Data Explorer pull request, audit, scan, deploy, Build and Deploy Frontend, Build and Deploy ROPA Site, deletion, non-fast-forward pull request, audit, scan, deletion, non-fast-forward
RONL Business API pull request, audit, scan, build, Build and Deploy ACC Frontend, Build and Deploy ACC PA Demo, Build and Deploy ACC Public Site, deletion, non-fast-forward pull request, audit, deletion, non-fast-forward

Two of the three now gate main. The Linked Data Explorer's main promotion gate came first, on 9 September 2026; the RONL Business API created its own on 12 September, before the promotion pull request was opened, layered over a classic protection under which an administrator could push to main directly — so the branch that deploys production had been the less protected of its two. That classic protection is still configured, force pushes allowed, and it is the ruleset's zero bypass actors and non-fast-forward rule that now close it. The Linked Data Explorer's main likewise still carries a classic protection, without admin enforcement, beneath its ruleset.

Each main ruleset used to mirror its acc twin, and no longer does. Until 19 September the two differed in exactly one parameter: require_extra_approval_for_unattributed_changes is true on acc and false on main, because a promotion carries commits under several author identities against a ruleset requiring zero approvals, and the flag would demand an approval nobody can give. That difference stands. What joined it is the build checks, which were added to acc only — so acc now requires strictly more than main does in both repositories, and the RONL Business API's main still requires audit alone, without scan. Read the table rather than assuming symmetry.

The CPSV Editor's main requires a pull request but no status checks — decided and kept, not overlooked (ttl-editor#131): main is promoted from acc, whose commits already passed audit and scan. See Supply-Chain Pinning for the argument on both sides.

scan is now required on acc in all three. The RONL Business API was the last holdout: its scan ran on every pull request while the baseline from its first authenticated scan was triaged, on the reasoning that a gate required before its baseline is triaged is a gate that gets bypassed in its first week. The triage finished, and the promotion was — as predicted — a ruleset edit that touched no file. Its main still requires audit alone.

Merge strategy is enforced by repository settings rather than by convention: all three disable squash and rebase merges, leaving merge commits only, with delete_branch_on_merge enabled. Both alternatives rewrite commit hashes — rebase deceptively so, since it preserves the commit count — and a changelog entry that cites commits by SHA is orphaned either way.

What the rulesets still do not require

A red suite now blocks a merge to acc in all three, and still does not block one to main. This page said the opposite until 19 September 2026, and was right at the time: the builds and suites ran on every pull request, a red one stopped the deploy, and the only checks the rulesets named were audit and — in two of the three — scan. A dependency pull request whose build failed could be merged.

That was not hypothetical. The RONL Business API's axios security update (#109) broke its backend build: axios 1.18 widened a header type, and one suite stopped compiling while 1,859 tests passed. It showed as a red check on the pull request — which is what running the suite before the merge buys — and with audit the only required check, nothing more: the pull request stayed mergeable until the fix landed in v2026.09.7.

What closed it is the build and deploy checks in the table above. Each of those jobs runs npm ci, the linter, the type check where one exists and the unit suites before it builds, so requiring the job requires everything in front of it. On acc, a pull request that breaks a test now has a red required check and no merge button. In the RONL Business API that holds for source changes; a lockfile-only pull request still skips the three site suites, whose changes patterns do not name the root lockfile, and runs only the backend's.

No ruleset requires a branch to be up to date before merging (strict_required_status_checks_policy: false in all five). Every required check is therefore green against the pull request's own base, not against the acc it merges into. On 25 September 2026 three RONL Business API dependency pull requests (#221–#223), each green, merged back to back into a lockfile that matched no package.json. The audit job's lockfile-sync step now names that failure on the next pull request; it cannot prevent it.

On main it is still advice. The Linked Data Explorer's main promotion gate requires audit and scan; the RONL Business API's requires audit; the CPSV Editor's main requires no status check at all. The deploy checks were deliberately not added there, and the reason now differs by repository. In the Linked Data Explorer and the RONL Business API, production deploys run from a push to main through promote-to-production.yml, not on the promotion pull request: the RONL Business API's four production workflows have no pull_request trigger at all, and the Linked Data Explorer's backend has none, so there is no check to require. The Linked Data Explorer's two production site workflows do build on a promotion pull request — a preview of the production site — but that trigger is still path-filtered, and a required check that never reports wedges the pull request permanently. The CPSV Editor's Deploy PROD (white-sky) keeps paths-ignore on its pull_request trigger for the same reason.

ICTU's guideline asks for more than the check list — that the entire pipeline succeeds before an update merges, and that the transitive changes in a lockfile diff are reviewed on a risk basis: new runtime packages, new origins, downgrades and licence changes. The first half is now met on acc; none of the three has tooling for that lockfile review yet. The scores are on ICTU Dependency Guideline, recommendation R9, and the work is tracked in linked-data-explorer#119.

How a path-filtered workflow became requireable

Requiring a build check looks like one ruleset edit and is not, because of a rule that only bites once the check is required:

A required status check must report on every pull request. A check that never reports is not "skipped" — it is pending forever, and the pull request can never merge.

The deploy workflows were all path-filtered, so a pull request touching nothing they watch started no run and reported no check. Filtering at the trigger and requiring the check are therefore mutually exclusive — which is why this was open work for as long as it was.

The fix is to move the filter one level down, and it is worth copying verbatim:

  • The trigger loses its paths: / paths-ignore: on pull_request. The workflow now starts for every pull request against its branch. push keeps its filter; nothing is required on a push.
  • A first job, changes, decides relevance. It asks the API for the pull request's changed files and matches them against the same pattern the push filter uses, kept in step by hand — GitHub Actions has no YAML anchors, so the two copies are maintained together. Renames count under both names, as GitHub's own filter does.
  • The build job runs needs: changes with an if: instead. A job skipped by its own if: reports success, where a workflow filtered out at its trigger reports nothing at all. That difference is the whole mechanism.
  • The condition is fail-safe. It reads !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.relevant == 'true') — so the build runs whenever the lookup did not succeed, not only when it says "relevant". An API failure or a rate limit means a full build, never a free pass.

This landed in all three repositories on 19 September 2026, in the CPSV Editor's Deploy ACC (orange-beach), the Linked Data Explorer's three *-acc workflows and the RONL Business API's four. Production workflows were left out: the CPSV Editor's is still filtered at its trigger, and in the other two a promotion workflow now calls them on the push rather than on the pull request. Their checks are un-required, which is why main is still ungated on builds.

What makes it enforcement

A workflow that runs but cannot block is advice. The ruleset converts it into a gate. In all three adopting repositories the ruleset is named acc supply-chain gate, targets refs/heads/acc, and is active with zero bypass actors:

  • required_status_checks → context audit; scan as well, in the CPSV Editor and the Linked Data Explorer since v2026.09.3 and in the RONL Business API since 19 September 2026; and, since the same date, each repository's build and deploy checks — the full list is in the table above
  • pull_request → required_approving_review_count: 0

The Linked Data Explorer and the RONL Business API each have a twin, main promotion gate, on main — see Adoption status.

Both rules are needed together. Requiring the check alone would still let a direct push to acc bypass the gate entirely.

A required check is matched by its job name, and by nothing else. Any job anywhere in the repository that reports under the same name satisfies — or blocks — it. The daily dependency audit was first named audit, the name of the supply-chain job every ruleset requires; the collision blocked ronl-business-api#206, and the job is now dependency-audit. The SBOM job is release-sbom for the same reason. The Linked Data Explorer's production site jobs were renamed from the shared Build and Deploy Job to Build and Deploy Production Frontend and … ROPA Site so that either could ever be required. Renaming a required job means editing the ruleset in the same change.

Approvals are 0 because these repositories have a single maintainer and GitHub does not permit self-approval — requiring 1 would make acc unmergeable. Raise it when a second reviewer exists.

The merge method is a setting, not a rule

A changelog entry names each commit by its SHA, so any merge strategy that rewrites hashes orphans every citation in it. The first version of this rule said never squash — and missed that rebase-and-merge rewrites hashes just as thoroughly, deceptively so, because it preserves the commit count while replacing every hash. That gap surfaced only when someone looked at the actual merge dropdown.

All three repositories now disable squash and rebase at repository level (Settings → General → Pull Requests), leaving merge commits only, with delete_branch_on_merge enabled:

allow_merge_commit: true    allow_squash_merge: false
allow_rebase_merge: false   delete_branch_on_merge: true

GitHub's default button is Squash and merge, so without the setting a single absent-minded click would orphan a release's entire entry. The failure is now impossible by construction rather than forbidden by prose — which is the general shape worth copying: a rule that depends on remembering is a rule that eventually fails.

A side effect is that Renovate's dependency pull requests land as merge commits too. That costs nothing: --no-merges already excludes the merge commit from a changelog range, and the underlying update commit is what an entry should name.

A repository adopting this template must apply the setting too. The rule without it is one click from failing.