Skip to content

feat: add check-branch-protection option to push-to-pull-request-branch#28365

Merged
pelikhan merged 6 commits intomainfrom
copilot/add-administration-permission-scope-again
Apr 25, 2026
Merged

feat: add check-branch-protection option to push-to-pull-request-branch#28365
pelikhan merged 6 commits intomainfrom
copilot/add-administration-permission-scope-again

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 24, 2026

Without administration: read, the branch protection pre-flight check in push-to-pull-request-branch silently falls through on every run (403 → warn → continue), making the check useless noise. This PR adds a check-branch-protection: false opt-out to skip the API call entirely—GitHub still enforces protection at push time. When the check is enabled (the default), administration: read is now automatically added to GitHub App tokens so the API call succeeds.

Changes

  • Go struct + parser (push_to_pull_request_branch.go): new CheckBranchProtection *bool field; parsed from check-branch-protection frontmatter key
  • Handler registry (compiler_safe_outputs_handlers.go): wires check_branch_protection into GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG via AddBoolPtr
  • JS handler (push_to_pull_request_branch.cjs): reads config.check_branch_protection !== false (defaults true); delegates to the new checkBranchPushable helper; default-branch guard still runs unconditionally
  • Branch protection helper (pr_helpers.cjs): new checkBranchPushable(githubClient, owner, repo, branchName, checkBranchProtection) function encapsulates the full branch security block; returns null when safe to push or an error string to block the push
  • Permissions (safe_outputs_permissions.go): ComputePermissionsForSafeOutputs now automatically adds administration: read when check-branch-protection is enabled; omitted when check-branch-protection: false is set (GitHub App-only scope — no effect on standard GITHUB_TOKEN)
  • Schema (main_workflow_schema.json): adds check-branch-protection boolean property to the push-to-pull-request-branch object schema
  • Docs (safe-outputs-pull-requests.md): documents the option and explains the trade-off

Usage

safe-outputs:
  push-to-pull-request-branch:
    check-branch-protection: false  # skip GET /branches/{branch}/protection; skip administration: read

Omitting the field keeps current behaviour (check enabled, administration: read automatically added to GitHub App tokens).

Copilot AI and others added 2 commits April 24, 2026 22:39
Add `check-branch-protection: false` option to the `push-to-pull-request-branch`
safe output configuration. When set to false, the branch protection API pre-flight
check is skipped, avoiding the need for `administration: read` permission.

The GitHub platform still enforces branch protection at push time regardless of
this setting, so disabling the pre-flight check does not reduce actual security.

Changes:
- Add `CheckBranchProtection *bool` field to PushToPullRequestBranchConfig
- Parse `check-branch-protection` in parsePushToPullRequestBranchConfig
- Wire `check_branch_protection` into the handler registry builder
- Update JS handler to skip branch protection API call when false
- Update JSON schema with the new property
- Update documentation with new option and explanation
- Add tests for explicit false and default (omitted) cases

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3b381d54-f8fa-4ebf-bb33-3089685d2ba8

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support for administration permission scope in workflow frontmatter feat: add check-branch-protection option to push-to-pull-request-branch Apr 24, 2026
Copilot AI requested a review from pelikhan April 24, 2026 22:45
// Unexpected errors (5xx, network failures, etc.) – fail closed to
// avoid bypassing branch protection due to transient API issues.
const msg = `Cannot verify branch protection rules for "${branchName}": ${getErrorMessage(protectionError)}. Push blocked to prevent accidental writes to protected branches.`;
if (checkBranchProtection) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@copilot refactor branch protection code to helper file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 96ab603. Extracted the entire branch security block into checkBranchPushable(githubClient, owner, repo, branchName, checkBranchProtection) in pr_helpers.cjs. It returns null when the push is safe, or an error string to block it. Added 6 unit tests covering all branches (404, 403, 5xx, protected, default branch, and disabled check). The call site in push_to_pull_request_branch.cjs is now a single await checkBranchPushable(...) call.

…in pr_helpers.cjs

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/4c3a23d2-247b-4629-8a38-133eacf7905f

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI requested a review from pelikhan April 24, 2026 23:04
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot recompile

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 24, 2026

@copilot recompile

Ran make build && make recompile — the lock files were already up to date, no changes produced. All tests and lint pass.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw (http block)
  • https://api.github.com/repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b
    • Triggering command: /usr/bin/gh gh api /repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b --jq [.object.sha, .object.type] | @tsv (http block)
  • https://api.github.com/repos/github/gh-aw
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw --jq .default_branch (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot when check branch protection is enabled, the job that uses this code must automatically add the "administration: read" permission for github-app based tokens

…ection is enabled

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/fe6a4ce5-4817-4bdc-8675-9e181eb1d449

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 24, 2026

@copilot when check branch protection is enabled, the job that uses this code must automatically add the "administration: read" permission for github-app based tokens

Done in 320deca. ComputePermissionsForSafeOutputs now adds administration: read automatically when check-branch-protection is enabled (the default). When check-branch-protection: false is set, the permission is omitted. Since administration is a GitHub App-only scope, it is forwarded via convertPermissionsToAppTokenFields and only affects minted app tokens — standard GITHUB_TOKEN workflows are unaffected.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • https://api.github.com/graphql
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw (http block)
    • Triggering command: /usr/bin/gh /usr/bin/gh api graphql -f query=query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { hasDiscussionsEnabled } } -f owner=github -f name=gh-aw t.go go 64/pkg/tool/linuconfig test�� to pkg/actionpinremote.origin.url _test.go k/_temp/uv-python-dir/bash user.name (http block)
  • https://api.github.com/repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b
    • Triggering command: /usr/bin/gh gh api /repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b --jq [.object.sha, .object.type] | @tsv n^{commit} (http block)
    • Triggering command: /usr/bin/gh gh api /repos/astral-sh/setup-uv/git/ref/tags/eac588ad8def6316056a12d4907a9d4d84ff7a3b --jq [.object.sha, .object.type] | @tsv h ../../../.prettierignore (http block)
  • https://api.github.com/repos/github/gh-aw
    • Triggering command: /usr/bin/gh gh api /repos/github/gh-aw --jq .default_branch get --local ache/uv/0.11.7/x86_64/bash copilot.originalgit (http block)

If you need me to access, download, or install something from one of these locations, you can either:

@pelikhan pelikhan requested a review from Copilot April 24, 2026 23:58
@pelikhan pelikhan marked this pull request as ready for review April 24, 2026 23:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a check-branch-protection opt-out for the push-to-pull-request-branch safe-output, and (when enabled/default) ensures GitHub App tokens include administration: read so the branch protection pre-flight API call can succeed.

Changes:

  • Introduces check-branch-protection (default true) in the Go config/parser + handler config wiring.
  • Refactors the JS branch safety checks into a reusable checkBranchPushable(...) helper and makes the protection check skippable.
  • Updates permissions computation, schema, docs, and adds/updates tests to cover the new behavior.
Show a summary per file
File Description
pkg/workflow/safe_outputs_permissions.go Adds defaulting helper and auto-adds administration: read when branch protection checks are enabled.
pkg/workflow/safe_outputs_permissions_test.go Updates/extends permission expectation coverage for the new option and default behavior.
pkg/workflow/push_to_pull_request_branch.go Adds CheckBranchProtection *bool to config and parses check-branch-protection from frontmatter.
pkg/workflow/compiler_safe_outputs_handlers.go Emits check_branch_protection into handler config when explicitly set.
pkg/workflow/push_to_pull_request_branch_test.go Verifies lock/handler config includes the flag when set to false and omits it by default.
actions/setup/js/push_to_pull_request_branch.cjs Reads check_branch_protection (default true) and delegates pushability checks to helper.
actions/setup/js/pr_helpers.cjs Adds checkBranchPushable(...) helper encapsulating default-branch + protection checks.
actions/setup/js/pr_helpers.test.cjs Adds unit tests for checkBranchPushable(...).
pkg/parser/schemas/main_workflow_schema.json Adds check-branch-protection to the workflow schema for push-to-pull-request-branch.
docs/src/content/docs/reference/safe-outputs-pull-requests.md Documents the new option and its trade-offs.
docs/src/content/docs/reference/frontmatter-full.md Adds the new option to the full frontmatter reference.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 11/11 changed files
  • Comments generated: 1

Comment on lines +355 to +362
describe("checkBranchPushable", () => {
const { checkBranchPushable } = require("./pr_helpers.cjs");

const mockCore = { info: vi.fn(), warning: vi.fn(), error: vi.fn() };
beforeEach(() => {
global.core = mockCore;
vi.clearAllMocks();
});
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The new checkBranchPushable tests mutate global.core in beforeEach but never restore the previous value. This can leak global state into other test suites and cause order-dependent failures. Capture the original global.core before overriding it and restore it in an afterEach (or delete it if it was originally undefined).

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 75/100

⚠️ Acceptable — with one note on test-to-code ratio

Metric Value
New/modified tests analyzed 11
✅ Design tests (behavioral contracts) 11 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 9 (82%)
Duplicate test clusters 0
Test inflation detected Yes (1 file — see below)
🚨 Coding-guideline violations 0

Test Classification Details

View all 11 test classifications
Test File Classification Notes
checkBranchPushable — returns null (404, non-default branch) pr_helpers.test.cjs ✅ Design Error path: 404 is treated as "no protection"
checkBranchPushable — blocks push on default branch pr_helpers.test.cjs ✅ Design Core behavioral contract
checkBranchPushable — blocks push with protection rules pr_helpers.test.cjs ✅ Design Core behavioral contract
checkBranchPushable — skips check when checkBranchProtection=false pr_helpers.test.cjs ✅ Design Boundary case; also asserts mock NOT called
checkBranchPushable — error on 5xx pr_helpers.test.cjs ✅ Design Error path
checkBranchPushable — null+warn on 403 pr_helpers.test.cjs ✅ Design Error path; asserts observable side-effect (warning)
TestPushToPullRequestBranchCheckBranchProtectionFalse push_to_pull_request_branch_test.go ✅ Design Verifies compiler propagates config to lock file
TestPushToPullRequestBranchCheckBranchProtectionDefaultOmitted push_to_pull_request_branch_test.go ✅ Design Verifies key is absent when not set (default behaviour)
Table row: default fallback — includes administration: read safe_outputs_permissions_test.go ✅ Design Tests permission contract
Table row: check-branch-protection: false — no administration perm safe_outputs_permissions_test.go ✅ Design Edge case: disabling feature removes the permission
Table row: check-branch-protection: true explicit — includes administration: read safe_outputs_permissions_test.go ✅ Design Explicit-true path matches default

⚠️ Test Inflation Note

push_to_pull_request_branch_test.go adds 87 lines against 8 production lines in push_to_pull_request_branch.go (ratio ≈ 10.9:1, threshold 2:1).

This triggers the automatic 10-point inflation deduction. However, the excess is entirely justified: the two new tests are compiler round-trip tests that require embedding a multi-line markdown fixture, writing it to a temp file, running the compiler, and reading back the lock YAML. This boilerplate is structural overhead, not low-value padding. The tests themselves are high-quality behavioral tests. No action required — just a heads-up.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 5 tests (2 new functions + 3 new table rows in existing table-driven test)
  • 🟨 JavaScript (*.test.cjs): 6 tests (vitest)

Build tags on modified Go test files: ✅ both files begin with //go:build !integration


Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All 11 new/modified tests verify observable behavioral contracts. No coding-guideline violations detected.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct

References: §24917291160

🧪 Test quality analysis by Test Quality Sentinel · ● 369.7K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 75/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). All 11 new/modified tests verify observable behavioral contracts. No coding-guideline violations detected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Commit pushed: 79a6719

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (149 new lines in pkg/ and actions/) but does not have a linked Architecture Decision Record (ADR).

AI has analyzed the PR diff and generated a draft ADR to help you get started:

📄 Draft ADR: docs/adr/28365-optional-branch-protection-check-for-push-to-pull-request-branch.md

What to do next

  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI couldn't infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-28365: Optional Branch Protection Pre-flight Check for push-to-pull-request-branch

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.


📋 What the draft ADR covers

The generated draft captures the following decision from the PR diff:

Decision: Make the branch protection pre-flight check in push-to-pull-request-branch opt-out via check-branch-protection: false, and automatically add administration: read to GitHub App token permissions when the check is enabled (the default).

Alternatives analyzed:

  • Always require administration: read with no opt-out (rejected: forces broad permission on all users)
  • Remove the branch protection check entirely (rejected: removes useful defense-in-depth)
  • Silently ignore 403 and keep existing behavior (rejected: misleading logs, no security value)

Key consequences noted:

  • Positive: eliminates the 403 silent-failure; administration: read is now only requested when actually used
  • Negative: upgrades automatically increase permission footprint for existing GitHub App deployments
📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 28365-title.md for PR #28365).

🔒 This PR cannot merge until an ADR is linked in the PR body.

References: §24917291168

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · ● 160.5K ·

@pelikhan pelikhan merged commit bfb15c5 into main Apr 25, 2026
@pelikhan pelikhan deleted the copilot/add-administration-permission-scope-again branch April 25, 2026 00:41
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: support 'administration' permission scope in workflow frontmatter

3 participants