docs/CI_REQUIRED_CHECK_TOPOLOGY.md
Status: design proposal. Do not change branch protection until the workflow changes below have landed and the new check has appeared on at least one recent commit.
GitHub branch protection should require one stable PR gate while still letting CI skip expensive risk checks on low-risk pull requests. The required gate must not be a path-filtered workflow or a conditionally-triggered workflow, because GitHub leaves required checks pending when the whole workflow is skipped by path filters, branch filters, or commit-message skip directives.
GitHub's safe distinction is:
merge_group.References:
Current PR-related workflow names:
.github/workflows/ci.yml: CI
Runs on pull_request, push to main, and merge_group. Contains fast
jobs plus conditional embedded Dolt jobs..github/workflows/regression.yml: Regression Tests
Runs on pull_request, push to main, and manual dispatch. Does not
currently run on merge_group. Uses job-level conditional regression
execution..github/workflows/cross-version-smoke.yml: Cross-Version Smoke Tests
Runs on every PR to main, tag pushes, and manual dispatch. Does not
currently run on merge_group..github/workflows/nix-build.yml: nix build
Uses workflow-level paths filters on pull_request and push. This
workflow must not be directly required..github/workflows/update-vendor-hash.yml:
Update vendorHash for dependabot Go bumps
Runs on pull_request_target for Dependabot Go bumps. It mutates Dependabot
branches and must not be a required PR check.As of 2026-05-26, the live gastownhall/beads ruleset named
Protect main - light (beads and gastown) enforces deletion and non-fast-forward
protection on the default branch. It does not currently require status checks.
After rollout, branch protection or the default-branch ruleset should require exactly this GitHub Actions check:
CI Gate / RequiredmainDo not require these existing check names directly:
Detect CI tierCheck build-tag policyCheck cmd/bd pure-Go tests compile (CGO_ENABLED=0)Check version consistencyCheck doc flags freshnessCheck for .beads changesTest (ubuntu-latest)Test (macos-latest)Test (storage domain + uow)Build (Embedded Dolt)Test (Embedded Dolt Storage)Test (Embedded Dolt Cmd 1/20) through Test (Embedded Dolt Cmd 20/20)Test (Windows - smoke)Check formattingLintTest Nix FlakeDifferential Regression (v0.49.6 baseline)Upgrade smoke (<version> -> candidate)Resolve versions to testnix build .#defaultThose checks should remain visible for diagnosis, but only the aggregate gate is branch-protection required.
.github/workflows/ci.yml is the required workflow owner. Its trigger must stay
unfiltered:
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
merge_group:
Do not add paths, paths-ignore, or narrower branch filters to this workflow.
Path and risk decisions belong in detector jobs and job-level if conditions.
Add one final job to .github/workflows/ci.yml:
ci-gate:
name: CI Gate / Required
runs-on: ubuntu-latest
needs:
- detect-ci-tier
- check-build-tags
- check-cmd-bd-puregeo-tests
- check-version-consistency
- check-doc-flags
- check-no-beads-changes
- test
- test-domain-uow
- build-embedded
- test-embedded-storage
- test-embedded-cmd
- test-windows
- fmt-check
- lint
- test-nix
if: ${{ always() }}
steps:
- name: Evaluate CI gate
env:
FULL_EMBEDDED: ${{ needs.detect-ci-tier.outputs.full_embedded }}
DETECT_CI_TIER: ${{ needs.detect-ci-tier.result }}
CHECK_BUILD_TAGS: ${{ needs.check-build-tags.result }}
CHECK_CMD_BD_PUREGEO_TESTS: ${{ needs.check-cmd-bd-puregeo-tests.result }}
CHECK_VERSION_CONSISTENCY: ${{ needs.check-version-consistency.result }}
CHECK_DOC_FLAGS: ${{ needs.check-doc-flags.result }}
CHECK_NO_BEADS_CHANGES: ${{ needs.check-no-beads-changes.result }}
TEST: ${{ needs.test.result }}
TEST_DOMAIN_UOW: ${{ needs.test-domain-uow.result }}
BUILD_EMBEDDED: ${{ needs.build-embedded.result }}
TEST_EMBEDDED_STORAGE: ${{ needs.test-embedded-storage.result }}
TEST_EMBEDDED_CMD: ${{ needs.test-embedded-cmd.result }}
TEST_WINDOWS: ${{ needs.test-windows.result }}
FMT_CHECK: ${{ needs.fmt-check.result }}
LINT: ${{ needs.lint.result }}
TEST_NIX: ${{ needs.test-nix.result }}
run: bash .github/scripts/ci-gate.sh
Add .github/scripts/ci-gate.sh as a small shell evaluator. It should fail on
any failure or cancelled result. It should accept skipped only for jobs
that are intentionally absent for that event or risk tier:
CHECK_NO_BEADS_CHANGES=skipped is acceptable on push and merge_group
because the job is PR-only.BUILD_EMBEDDED, TEST_EMBEDDED_STORAGE, and TEST_EMBEDDED_CMD may be
skipped only when FULL_EMBEDDED != true.success.This keeps branch protection pointed at one job while preserving the underlying job names and logs.
Conditional risk checks should use this pattern:
detect-risk:
name: Detect risk
outputs:
run_risk: ${{ steps.detect.outputs.run_risk }}
risk-check:
name: Risk check
needs: detect-risk
if: needs.detect-risk.outputs.run_risk == 'true'
ci-gate:
name: CI Gate / Required
needs: [detect-risk, risk-check]
if: ${{ always() }}
The required aggregate should treat risk-check=skipped as success only when
detect-risk.outputs.run_risk != true. If the detector wanted the risk check
and the risk check is skipped, failed, or cancelled, the aggregate must fail.
Do not use this pattern for required checks:
on:
pull_request:
paths:
- 'go.mod'
- 'go.sum'
If that workflow or one of its jobs is made required, PRs that do not touch the listed paths can be blocked waiting for a check that GitHub never creates.
The current embedded Dolt topology already fits the required-check model:
detect-ci-tier always runs.build-embedded, test-embedded-storage, and test-embedded-cmd use
job-level if..github/scripts/ci-embedded-tier.sh runs full embedded coverage for
push, merge_group, unavailable PR diff bounds, and risky paths.Regression Tests can stay visible as a non-required workflow. If regression
becomes branch-protection relevant, do not require
Differential Regression (v0.49.6 baseline) directly.
Use one of these narrow changes instead:
ci.yml, wire them into
CI Gate / Required, and add merge_group behavior that defaults to running
regression.regression.yml separate, remove any workflow-level skip filters, add
merge_group, add a final Regression Gate / Informational aggregate, and
leave it non-required unless branch protection is intentionally expanded.The preferred required-check topology keeps only CI Gate / Required required.
.github/workflows/nix-build.yml currently uses workflow-level paths filters.
Keep nix build .#default non-required.
If the full Nix build must affect mergeability, move it into ci.yml behind a
detector and job-level if, then teach CI Gate / Required when a skipped Nix
build is acceptable. Do not make the path-filtered nix build workflow or
nix build .#default job directly required.
Cross-Version Smoke Tests should remain non-required for ordinary PRs unless
maintainers explicitly choose to pay that cost in the aggregate gate. If it
becomes required, add merge_group and put it behind a detector plus aggregate
inside the required topology. Do not require matrix-expanded
Upgrade smoke (<version> -> candidate) jobs directly.
The required CI Gate / Required check must be reported for merge_group.
Otherwise, GitHub can enqueue a PR and then fail to merge because the required
check was never reported for the synthetic merge group commit.
Policy for merge_group:
CI must include merge_group.detect-ci-tier should keep treating merge_group as full embedded coverage.merge_group, because the merge group commit may combine individually safe
PRs into a risky integration state.Check for .beads changes may
be skipped by design..github/scripts/ci-gate.sh and the ci-gate job to ci.yml.CI Gate / Required from GitHub Actions.CI Gate / Required fail.CI Gate / Required on the merge group.CI Gate / Required from GitHub Actions.CI Gate / Required from the default-branch ruleset or branch
protection.CI Gate / Required.If rollback is needed because the aggregate logic is wrong, prefer first relaxing branch protection to remove the aggregate requirement. That unblocks merges without hiding the failed workflow logs needed for diagnosis.
The topology above prevents pending required checks caused by path-filtered and
branch-filtered workflows. GitHub can still skip push and pull_request
workflows when the HEAD commit message contains skip directives such as
[skip ci]. If maintainers want a hard guarantee that commit-message skips
fail closed instead of pending, the required check must be emitted by a tiny
trusted reporter that is not itself skipped by those directives, for example a
pull_request_target workflow that does not check out or run PR code and
creates a check run named CI Gate / Required on the PR head SHA after
inspecting the untrusted pull_request workflow results.
That reporter is intentionally outside the narrow first rollout. Until then,
do not use commit-message skip directives on PRs targeting main.