Skip to content

feat: emit branch-protection.json from the manifest#238

Merged
joshua-temple merged 1 commit into
mainfrom
feat/emit-branch-protection
Jun 21, 2026
Merged

feat: emit branch-protection.json from the manifest#238
joshua-temple merged 1 commit into
mainfrom
feat/emit-branch-protection

Conversation

@joshua-temple

Copy link
Copy Markdown
Collaborator

Problem

Operators protecting a cascade-managed trunk had to hand-assemble the GitHub branch-protection PUT body and guess which status-check contexts were safe to require. Requiring the wrong context (one that never reports) silently blocks every pull request.

Fix

Adds a new top-level command, cascade branch-protection, in a new internal/branchprotection package. It emits a JSON wrapper with two keys: protection (the exact body to PUT to repos/{owner}/{repo}/branches/{branch}/protection) and operator_todo (sibling guidance that is never sent to GitHub). cascade emits the file; the operator applies it. cascade never calls the GitHub API.

Flags: --config/-c, --manifest-key, --branch (default main), --output/-o (default stdout, - also stdout). The command is emit-on-demand and is not added to the verify-tracked Plan set.

Contexts derivation and the safety invariant

GitHub records a check-run context under a job's name:. The required contexts contain only the cascade-controlled Setup and Finalize steps jobs, whose names cascade emits and now reads from shared generate.SetupJobName / generate.FinalizeJobName constants (so a rename updates both the generated workflow and the emitted contexts in lockstep). Both jobs are unconditional (setup has no if:, finalize uses always()), so they always report, which is what makes them safe to require. Because of this, the .protection object applied verbatim never creates a required check that can never report, so it never blocks a pull request on its own.

The reusable-workflow caller jobs (validate, build, deploy) are deliberately omitted from contexts: cascade knows each caller's <DisplayName> but not the inner job name GitHub appends to form the real <DisplayName> / <inner-job> context, and that inner job lives in the operator's reusable workflow. Requiring a bare prefix would never match and would block every pull request, so those prefixes are surfaced under operator_todo.complete_these_contexts as <DisplayName> / <inner-job> placeholders for the operator to complete. With no callbacks, that array is emitted as an empty list for output stability.

Verification

  • go build ./cmd/... ./internal/... clean.
  • go test ./... -count=1: all packages pass (generator output stays byte-identical after the constant refactor).
  • golangci-lint run ./...: no issues.
  • Unit tests assert the contexts equal the generator's real safe names (from the shared constants), the safety invariant (no bare DisplayName in contexts for a validate+build+deploy manifest, each surfaced as a placeholder in operator_todo), determinism, JSON round-trip, defaults, and restrictions: null present.
  • Integration test drives the real NewCommand() end to end against a temp manifest. No Docker e2e/scenarios/ scenario fits: this change emits no workflow and no schema field, so there is no committed workflow fixture for testcontainers+gitea+act to exercise. The integration test follows the verify_clean_test.go precedent and notes this in a comment.

Defaults match the existing hotfix branch-protection advisory (required_approving_review_count: 1, enforce_admins: true). No new manifest field; CurrentSchemaVersion unchanged.

Closes #5

Signed-off-by: Joshua Temple <joshua.temple@stablekernel.com>
@joshua-temple joshua-temple enabled auto-merge (squash) June 21, 2026 08:59
@joshua-temple joshua-temple merged commit 225a5b1 into main Jun 21, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: emit branch-protection.json from manifest

1 participant