Skip to content

fix(branches): refuse a non-boolean control at parse, and let a null "off" converge - #599

Merged
Vivswan merged 6 commits into
mainfrom
fix/branches-boolean-controls
Nov 12, 2026
Merged

Vivswan merged 6 commits into
mainfrom
fix/branches-boolean-controls

Conversation

@Vivswan

@Vivswan Vivswan commented Nov 12, 2026 •

Copy link
Copy Markdown
Owner

Before

parse: accepted -> PUT protection, the string in the body -> 422 at apply
null "off"      -> compared as empty                      -> drift on every check
                -> a wildcard rule's mutation is sent null -> the next rules read fails
branches:
  - name: main
    protection:
      required_linear_history: "yes"
      allow_deletions: null
      required_pull_request_reviews: { dismiss_stale_reviews: "no" }

--- non-boolean control on a literal branch
::error::branches: replacing protection for branch "main" failed - PUT /repos/e2e-owner/e2e-repo/branches/main/protection: 422 Validation Failed (["Invalid request.\n\nFor 'properties/required_linear_history', \"yes\" is not a boolean."]). The API rejected the request; fix the "branches" values in the settings file to satisfy the message above. Usually a sub-object is missing a required half: "required_status_checks" needs both "strict" and "contexts", "required_pull_request_reviews" values must fit their documented shapes, and "restrictions" needs "users" and "teams" lists (or declare the whole key as null). The fields and values this endpoint accepts are documented at https://docs.github.com/rest/branches/branch-protection#update-branch-protection
result: failed
branches: [{ name: main, protection: { enforce_admins: null } }]          # mode: check; live GET: enforce_admins {enabled: false}

--- null off on a literal branch, check mode
drift: branches[main].protection.enforce_admins: expected empty, live has false
result: drift
branches: [{ name: "release/*", protection: { enforce_admins: null } }]   # mode: apply; live rule: isAdminEnforced true

--- null off on a wildcard rule
branches: updated protection rule "release/*"
result: applied
--- null off on a wildcard rule, apply-idempotence second apply
::error::branches: GRAPHQL BranchProtectionRules returned a body outside the documented shape - [0].isAdminEnforced: Invalid input: expected boolean, received null. Check the "api-version" input against the GitHub GraphQL reference for this operation
result: failed
--- null off on a wildcard rule, apply-idempotence check
::error::branches: GRAPHQL BranchProtectionRules returned a body outside the documented shape - [0].isAdminEnforced: Invalid input: expected boolean, received null. Check the "api-version" input against the GitHub GraphQL reference for this operation
result: failed

After

parse: a boolean of the PUT is not a boolean?            -> refused, no request sent
       null on a literal branch, PUT not nullable there? -> refused, no request sent
null "off" -> read as false -> clean against {enabled: false}
                            -> a wildcard rule's mutation is sent false
--- non-boolean control on a literal branch
::error::/repo/settings.yml has malformed section entries: branches[0].protection.required_pull_request_reviews.dismiss_stale_reviews: required_pull_request_reviews.dismiss_stale_reviews must be an unquoted true or false (YAML parses "no"/"off"/"yes" as strings, not booleans), so the toggle direction is unambiguous; branches[0].protection.required_linear_history: required_linear_history must be an unquoted true or false (YAML parses "no"/"off"/"yes" as strings, not booleans), so the toggle direction is unambiguous; branches[0].protection.allow_deletions has no empty state; write true or false. Fix these values in the settings file (only the named keys are validated; extra fields pass through, except in closed sections and strict nested objects like actions.cache, which reject unrecognized keys)
result: failed
--- null off on a literal branch, check mode
result: clean
--- null off on a wildcard rule
branches: updated protection rule "release/*"
result: applied
--- null off on a wildcard rule, apply-idempotence second apply
result: applied
--- null off on a wildcard rule, apply-idempotence check
result: clean
entry      value                                     this PR               basis
main       a control: "yes"                          refused               the PUT answers 422 (first block)
main       a review setting: "no"                    refused               GitHub's descriptor types it boolean, at
                                                                           PUT /repos/{owner}/{repo}/branches/{branch}/protection
                                                                           requestBody required_pull_request_reviews.dismiss_stale_reviews
main       null under either kind, outside           refused               the PUT answers 422 whenever it carries the null,
           enforce_admins and allow_force_pushes                           and the descriptor marks neither kind nullable
main       null under enforce_admins or              read as off           the descriptor marks the two nullable
           allow_force_pushes                                              where the GET omits the control, one PUT now sends false
main       a copied {url, enabled} wrapper           refused, same advice  now at protection.enforce_admins, not .enabled under it
release/*  any boolean: "yes"                        refused               a GraphQL Boolean input cannot take a string
release/*  null, outside those two                   KEPT as on main       a GraphQL Boolean input is nullable, so a run can complete
branches: [{ name: "release/*", protection: { allow_deletions: null } }]   # mode: apply; no live rule; the same on main and here

--- null on a wildcard rule's non-nullable control
branches: created protection rule "release/*"
result: applied
--- null on a wildcard rule's non-nullable control, apply-idempotence second apply
::error::branches: GRAPHQL BranchProtectionRules returned a body outside the documented shape - [0].allowsDeletions: Invalid input: expected boolean, received null. Check the "api-version" input against the GitHub GraphQL reference for this operation
result: failed
--- null on a wildcard rule's non-nullable control, apply-idempotence check
::error::branches: GRAPHQL BranchProtectionRules returned a body outside the documented shape - [0].allowsDeletions: Invalid input: expected boolean, received null. Check the "api-version" input against the GitHub GraphQL reference for this operation
result: failed

Every refused input failed whenever a write carried it. Owner's exception: a literal null under a non-nullable boolean ran clean while the live GET omitted that control (GitHub's descriptor allows it), and is refused now.

Owner's call: the last row stays accepted (capture above). GitHub's nullable Boolean may take it, and check would read expected empty, live has false every run. Precedent: required_signatures: null, refused there.

The published JSON schema refuses the same inputs, so an editor flags them.

How

  • Typed once: control() and toggle() type every PUT boolean.
  • Null as off: nullReadAsOff reads the two nullable controls as false.
  • Library: zod now types eleven BranchProtectionConfig keys as boolean | null, so a TypeScript caller passing "yes" stops compiling. SettingsFile.safeParse() called directly now refuses a literal null under a non-nullable control.

Proof

  • Tests: 187 pass, 35 red on main.
  • Gates: e2e slice 38 of 38, six static gates pass.
Technical details
  • One wording: control() and toggle() carry the required_signatures wording to each typed boolean.
  • Typed booleans: the eight PUT controls from enforce_admins to allow_fork_syncing, and dismiss_stale_reviews, require_code_owner_reviews, require_last_push_approval.
  • Exported types: the narrowing reaches BranchConfig, SettingsFile, and SectionInput<"branches">, where the eleven keys were an unknown passthrough.
  • Literal-only refusal: a rule on BranchConfig in schema.ts raises zod's invalid_type issue for a null under a PUT boolean GitHub does not mark nullable.
  • Its wording: validation rewords that issue as <path> has no empty state; write true or false, the line required_signatures: null already gets.
  • Published twin: one conditional() call in BranchConfig's meta publishes that rule, typing the nine keys boolean when the name holds none of *, ?, [.
  • One predicate: the twin's pattern is the source of WILDCARD_NAME, the regex isWildcardPattern tests.
  • Twin held to the rule: the rows in schema.test.ts judge each PUT boolean with both validators, for a quoted value and for null on each kind of entry.
  • Two contracts, one shape: the shape takes null under every PUT boolean because a wildcard entry may carry it.
  • Where it is stated: the comment on refuseNullsThePutRefuses in schema.ts names both contracts.
  • No transform: SettingsFile's inferred type is what a library consumer writes, so the null-as-off reading is a function in the section.
  • PUT body: a declared null under enforce_admins or allow_force_pushes is sent as false.
  • One PUT where the GET omits the control: read as false, that null diffs against a missing field as a declared false always did, so one apply sends the PUT and the next run is clean.
  • Wrapper: the url and enabled lines under a wrapped control fold into the control's one line.
  • Unchanged: required_signatures and required_status_checks.strict refuse null on every entry, as before.
  • Exception, GET's contract: paths["/repos/{owner}/{repo}/branches/{branch}/protection"].get.responses["200"] (Branch Protection) has no required list, so its answer may omit any control.
  • Exception, why it ran: a declared null against a missing field is no difference, so main planned no PUT and nothing carried the null.
  • Exception, the other answers: with the control reported, or the branch unprotected, main planned the PUT carrying the null, which the PUT answers 422.
  • 422 hint: it names users or teams under restrictions or dismissal_restrictions on a personal repository, the 100-actor total, and required_linear_history without squash or rebase merging.
  • Hint wording: these are causes the parser does not refuse, and GitHub's descriptor states each.
  • Old hint: the parser refuses every cause it named.
  • Message: the missing-branch refusal leads with branches[<name>]:, a line the engine prints as is, so a run no longer prints the section key twice.
  • Message, pinned: branches-missing-branch-advisory-apply-fails.yml now holds the printed ::error::branches[release]: ... line, which fails on the engine before that rule.
  • Message, captured: the sixth and seventh blocks below, main and then this branch.
  • safeParse, why: the null rule sits in BranchConfig's shape, which SettingsFile composes, where an untyped key passed before.
  • Kept row, GitHub's type: UpdateBranchProtectionRuleInput.allowsDeletions is Boolean, and BranchProtectionRule.allowsDeletions is Boolean!.
  • Corpus census: 377 scenario files on main hold 49 PUT booleans (42 controls, 7 review settings) and 7 required_signatures values, every one a boolean.
  • Census remainder: the one other value is a copied wrapper, already refused on main.
  • Census reading: no scenario relied on an input this PR refuses.
  • Census control: the 49 show the walk reaches the corpus.
  • Re-expected: branches-get-response-copied-rejected.yml and the copied-GET case in schema.test.ts expect the wrapper at branches[0].protection.enforce_admins.
  • Re-expected: the shared-alias case in schema.test.ts shares a review holder, since a wrapper under a typed control stops at the control.
  • Re-expected: the missing-branch case in branches.test.ts expects branches[main]: ... without the doubled key.
  • Deleted allowlist entry: test/schema-corpus.test.ts listed the copied-GET scenario as schema-looser, and its own gate named the entry stale once both validators refused it.
  • Census allowlist: test/sections/refusal-messages.test.ts registers control and toggle as data arguments, like actorList.
  • Upgrade page: docs/upgrading/v2-to-v3/refusals-lists.md quotes the wrapper refusal at its new path.
  • New scenarios: branches-non-boolean-control-rejected, branches-null-off-control-converges, and branches-literal-null-control-rejected, each red on main.
  • Null-only scenario: the third holds nothing but a literal null, so the corpus gate judges the document where the two validators could disagree.
  • Capture command: bun capture.ts /repo scenarios.yml, the script below, once on a copy of origin/main and once on this branch.
  • Capture scenarios: the settings shown, with the live state the comments name and Administration write.
  • Second runs: the scenarios with a second apply arm fixpoint: apply_idempotent, which runs the second apply and the check.
  • Census command: bun census.ts /repo, the second script below.
  • Test command: bun test test/sections/branches/schema.test.ts test/sections/branches/branches.test.ts.
  • Red count: the same two files on a copy of origin/main give 152 pass and 35 fail.
  • e2e command: bun test/e2e/run.ts --sections branches.
  • Static gates: typecheck, lint, lint:arch, knip, check:compat, build:check.
  • Redaction: the temp settings path is shown as /repo/settings.yml, and debug request lines are left out.
  • Exception, captured: the first two blocks below, main and then this branch, two runs each.
  • One PUT, captured: the third and fourth blocks, main and then this branch.
  • Census output: the fifth block, from main.
  • Files: src/sections/branches/{schema,index,keys,endpoints,graphql-vocabulary}.ts, test/sections/branches/{schema,branches}.test.ts, five files under test/sections/branches/scenarios/, docs/sections/branches.docs.yml, docs/reference/coverage.md (regenerated).
branches: [{ name: main, protection: { enforce_admins: true, allow_deletions: null } }]   # mode: check
# first run: the live GET answers enforce_admins {enabled: true} and omits allow_deletions
# second run: the live GET also answers allow_deletions {enabled: false}

--- literal null where the GET omits the control, check mode
result: clean
--- literal null where the GET reports the control off, check mode
drift: branches[main].protection.allow_deletions: expected empty, live has false
result: drift
--- literal null where the GET omits the control, check mode
::error::/repo/settings.yml has malformed section entries: branches[0].protection.allow_deletions has no empty state; write true or false. Fix these values in the settings file (only the named keys are validated; extra fields pass through, except in closed sections and strict nested objects like actions.cache, which reject unrecognized keys)
result: failed
--- literal null where the GET reports the control off, check mode
::error::/repo/settings.yml has malformed section entries: branches[0].protection.allow_deletions has no empty state; write true or false. Fix these values in the settings file (only the named keys are validated; extra fields pass through, except in closed sections and strict nested objects like actions.cache, which reject unrecognized keys)
result: failed
branches: [{ name: main, protection: { required_linear_history: true, enforce_admins: null } }]   # mode: apply; live GET: required_linear_history {enabled: true}

--- null off where the GET omits the control
result: applied
--- null off where the GET omits the control, apply-idempotence second apply
result: applied
--- null off where the GET omits the control, apply-idempotence check
result: clean
--- null off where the GET omits the control
branches: applied protection to "main"
result: applied
--- null off where the GET omits the control, apply-idempotence second apply
result: applied
--- null off where the GET omits the control, apply-idempotence check
result: clean
$ bun census.ts /repo
scenario files walked: 377
  37  literal PUT control boolean
   1  literal PUT control object
   4  literal PUT review setting boolean
   6  literal required_signatures boolean
   5  wildcard PUT control boolean
   3  wildcard PUT review setting boolean
   1  wildcard required_signatures boolean
branches: [{ name: release, protection: { enforce_admins: true } }]   # mode: apply; the repository has no branch release

--- a declared branch that does not exist
::error::branches: branches[release]: the declared branch does not exist on the repo, so its protection cannot be applied; create the branch, or remove it from the settings file
result: failed
--- a declared branch that does not exist
::error::branches[release]: the declared branch does not exist on the repo, so its protection cannot be applied; create the branch, or remove it from the settings file
result: failed
// capture.ts: each scenario of a YAML list through the action bundle against the mock API, every run's output in order
import { readFileSync } from "node:fs";
import { parse } from "yaml";

const [repo, listPath] = process.argv.slice(2);
const { parseScenario } = await import(`${repo}/test/e2e/scenario.ts`);
const { runScenario, stripMaskLines } = await import(`${repo}/test/e2e/runner/runner.ts`);
const shown = (stdout: string) =>
  stripMaskLines(stdout)
    .split("\n")
    .filter((line: string) => line !== "" && !line.startsWith("::debug::"))
    .join("\n");
for (const raw of parse(readFileSync(listPath, "utf8"))) {
  const report = await runScenario(parseScenario(raw, listPath));
  console.log(`--- ${raw.name}\n${shown(report.stdout)}`);
  for (const rerun of report.reruns) {
    console.log(`--- ${raw.name}, ${rerun.label}\n${shown(rerun.stdout)}`);
  }
}
// census.ts: the booleans of every branches protection in the scenario corpus, counted by where they sit and the kind of value they hold
import { readdirSync, readFileSync } from "node:fs";
import { parse } from "yaml";

const repo = process.argv[2];
const PUT = "enforce_admins required_linear_history allow_force_pushes allow_deletions block_creations required_conversation_resolution lock_branch allow_fork_syncing".split(" ");
const REVIEW = "dismiss_stale_reviews require_code_owner_reviews require_last_push_approval".split(" ");
const counts: Record<string, number> = {};
const count = (where: string, value: unknown) => {
  const key = `${where} ${value === null ? "null" : typeof value}`;
  counts[key] = (counts[key] ?? 0) + 1;
};
const walk = (node: any): void => {
  if (typeof node !== "object" || node === null) return;
  const branches = Array.isArray(node.branches) ? node.branches : (node.branches?.entries ?? []);
  for (const { name, protection } of Array.isArray(branches) ? branches : []) {
    const kind = /[*?[]/.test(String(name)) ? "wildcard" : "literal";
    for (const key of PUT) if (protection && key in protection) count(`${kind} PUT control`, protection[key]);
    const reviews = protection?.required_pull_request_reviews;
    for (const key of REVIEW) if (reviews && key in reviews) count(`${kind} PUT review setting`, reviews[key]);
    if (protection && "required_signatures" in protection) count(`${kind} required_signatures`, protection.required_signatures);
  }
  for (const value of Object.values(node)) walk(value);
};
const files = (readdirSync(`${repo}/test`, { recursive: true }) as string[]).filter((f) => /scenarios\/.*\.yml$/.test(f));
for (const file of files) walk(parse(readFileSync(`${repo}/test/${file}`, "utf8")));
console.log(`scenario files walked: ${files.length}`);
for (const key of Object.keys(counts).sort()) console.log(`${String(counts[key]).padStart(4)}  ${key}`);

…"off" converge

Every boolean of the protection PUT body is now typed in the schema: the eight controls and the three review settings. A quoted value such as "yes" is refused when the file is parsed, on a literal branch and on a wildcard rule alike, in the wording required_signatures already had. Before, the PUT answered 422 at apply, and a wildcard rule's mutation was sent the string.

A null is refused on a literal branch wherever the PUT does not mark the boolean nullable, as "has no empty state; write true or false". A wildcard rule still takes that null: the rule mutation types every twin as a nullable Boolean, so GitHub can accept the file.

Under enforce_admins and allow_force_pushes null is the PUT's own spelling of off, so the section now reads it as false in one place. A check against GitHub's {enabled: false} is clean, where it reported "expected empty, live has false" on every run. A wildcard rule's mutation is sent false, where a null it stored failed the next rules read.

A copied {url, enabled} wrapper is refused at the control's own path with the same advice; the key sweep used to report it one step deeper, at .enabled. The published JSON schema types the new booleans, so the known-divergence entry for the copied-GET scenario is deleted: both validators refuse it now.

The PUT's 422 hint names only what GitHub's descriptor says the PUT judges against the repository, since the parser refuses a malformed shape first. The missing-branch refusal no longer leads with the section key twice.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 02:57
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

File size check

0 over a hard cap (fails), 6 warning(s).

File Size Tier Cap
src/engine/diff.ts:56 11 comment lines warn 10
src/github/api-error.ts:9 11 comment lines warn 10
src/problem.ts:73 18 comment lines warn 10
src/sections/contract/errors.ts:18 11 comment lines warn 10
src/sections/contract/module.ts:55 12 comment lines warn 10
src/sections/shared/list/decl.ts:265 14 comment lines warn 10

Split the file, wrap the line, shorten or exempt the comment, or list the path in .file-size-allow.local with a # reason.

5 managed file(s) skipped; repo-platform owns them.

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Member shape check

20 finding(s).

File Line Rule
src/action/io.ts 24 one-line-comment
src/cli/actions.ts 12 one-line-comment
src/github/api.ts 55 one-line-comment
src/sections/branches/schema.ts 246 all-or-none
src/sections/branches/schema.ts 267 all-or-none
src/sections/branches/schema.ts 405 constant-spacing
src/sections/branches/schema.ts 444 one-line-comment
src/sections/pages/index.ts 71 one-line-comment
test/cli/actions.test.ts 45 one-line-comment
test/scripts/release-pipeline-fixture.test.ts 25 one-line-comment
test/scripts/release-pipeline-fixture.ts 53 one-line-comment
test/sections/contract/validate-hook.test.ts 80 all-or-none
test/sections/teams/teams.test.ts 20 all-or-none
test/sections/teams/teams.test.ts 23 all-or-none
test/sections/teams/teams.test.ts 24 all-or-none
test/sections/teams/teams.test.ts 25 all-or-none
test/sections/teams/teams.test.ts 26 all-or-none
test/sections/teams/teams.test.ts 28 all-or-none
test/sections/teams/teams.test.ts 31 all-or-none
test/sections/teams/teams.test.ts 34 constant-spacing

Add the blank lines, make the comment one line directly above the item, comment every field or none, or list the path in .member-shape-allow.local with a # reason.

4 generated file(s) skipped.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues were identified for the branch-protection validation and null-handling changes.

No open issues 🎉

What changed in this PR

This PR strengthens branch-protection validation in GitHub Settings as Code and makes nullable off controls converge across literal branches and wildcard rules.

Changes:

  • Reject non-boolean controls and unsupported literal-branch nulls during parsing, with actionable errors.
  • Normalize null to false for enforce_admins and allow_force_pushes before comparison and writes.
  • Add regression coverage and update diagnostics, schema documentation, and migration guidance.
File Description
test/​sections/​refusal-messages.test.ts Register boolean helpers' data arguments.
test/​sections/​branches/​schema.test.ts Cover boolean validation, null contracts, and wrapper errors.
test/​sections/​branches/​scenarios/​branches-null-off-control-converges.yml Verify null-as-off convergence and repeat-run stability.
test/​sections/​branches/​scenarios/​branches-non-boolean-control-rejected.yml Verify invalid controls fail before requests.
test/​sections/​branches/​scenarios/​branches-get-response-copied-rejected.yml Update the expected wrapper-error path.
test/​sections/​branches/​branches.test.ts Test normalized writes and missing-branch wording.
test/​schema-corpus.test.ts Remove the obsolete copied-response schema exception.
src/​sections/​branches/​schema.ts Type protection booleans and provide targeted errors.
src/​sections/​branches/​keys.ts Clarify the control-list contract in a comment.
src/​sections/​branches/​index.ts Reject unsupported nulls and normalize nullable off controls.
src/​sections/​branches/​graphql-vocabulary.ts Include typed controls in REST-carried key coverage.
src/​sections/​branches/​endpoints.ts Replace outdated 422 advice with repository-state causes.
docs/​upgrading/​v2-to-v3/​refusals-lists.md Update the documented wrapper-error path.
docs/​sections/​branches.docs.yml Document boolean validation and null behavior.
docs/​reference/​coverage.md Refresh generated branch-protection coverage notes.

Review effort: Balanced

…hema

The published schema accepted `protection: {allow_deletions: null}` on a literal branch while the runtime refused it. The refusal now lives on BranchConfig in schema.ts beside its published twin: one conditional that types the same nine keys boolean when the name is not a wildcard pattern, over the regex the runtime tests. index.ts keeps the wildcard key sweep alone.

The rows in schema.test.ts now judge every PUT boolean with both validators, and a scenario whose settings hold only that null puts the document in front of the corpus gate.

The PUT's 422 hint says what it is: causes the parser does not refuse. A list of 101 actors can be counted from the file, so "only GitHub can judge" was wrong. The test that restated two refusals main already had is deleted, and three comments on constants are one line each.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 03:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are recorded for the branch-protection validation and null-handling changes.

No open issues 🎉

Review effort: Balanced

With the engine now leaving alone a line that leads with its section key, the missing-branch refusal prints as `branches[<name>]: ...`, where a run printed `branches: branches[<name>]: ...`. The scenario lines held only a substring both forms contain, so one stdout line now pins the printed error.

The comment about the published twin moves above `.meta(`, so the members of that object are uncommented alike.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 05:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues block approval of the branch-protection validation and null-handling changes.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 12, 2026 05:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues remain for the branch-protection validation and null-normalization changes.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 12, 2026 06:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues remain for the branch-protection validation and null-handling changes.

No open issues 🎉

Review effort: Balanced

@Vivswan
Vivswan marked this pull request as ready for review November 12, 2026 06:07
@Vivswan Vivswan added the merge-when-green Owner approved: merge once every gate is green label Nov 12, 2026
@Vivswan
Vivswan merged commit 346b640 into main Nov 12, 2026
32 checks passed
@Vivswan
Vivswan deleted the fix/branches-boolean-controls branch November 12, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-when-green Owner approved: merge once every gate is green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants