Skip to content

Disable checkout by default for pull_request_target workflows#46771

Merged
pelikhan merged 6 commits into
mainfrom
copilot/fix-checkout-pr-branch-failure
Jul 20, 2026
Merged

Disable checkout by default for pull_request_target workflows#46771
pelikhan merged 6 commits into
mainfrom
copilot/fix-checkout-pr-branch-failure

Conversation

Copilot AI commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

The "Checkout PR branch" step hard-fails on pull_request_target: closed runs because the head branch is deleted on merge, and on any fork PR since fork branches are inaccessible in the base repo. Additionally, checkout: false in frontmatter was not suppressing this step — only the default actions/checkout was skipped.

Changes

  • pkg/workflow/pr.goShouldGeneratePRCheckoutStep now returns false when CheckoutDisabled is true. This fixes the pre-existing bug where checkout: false still generated the "Checkout PR branch" step (confirmed by ai-moderator.lock.yml which had checkout: false but the step was present).

  • pkg/workflow/workflow_builder.go — Auto-sets CheckoutDisabled = true for pull_request_target triggers when no checkout key is present in frontmatter and no checkout configs are set. Users who need checkout can opt in explicitly:

    checkout:
      repository: ${{ github.repository }}
      ref: ${{ github.event.pull_request.base.sha }}
  • pkg/workflow/pull_request_target_validation_test.go — Updated test expectations: with checkout auto-disabled, there's no insecure-checkout warning/error for unconfigured pull_request_target workflows.

  • pkg/workflow/pr_test.go / pr_ready_for_review_checkout_test.go — Added unit and integration tests covering: CheckoutDisabled suppresses the step, pull_request_target with no checkout key produces no "Checkout PR branch" step, explicit checkout mapping still produces the step.

  • .github/workflows/ai-moderator.lock.yml — Recompiled; the existing checkout: false now correctly removes the step it was failing to suppress.


run: https://github.com/github/gh-aw/actions/runs/29735453773

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • pi.dev

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "pi.dev"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · 7.59 AIC · ⌖ 5.86 AIC · ⊞ 5.4K ·
Comment /souschef to run again

- ShouldGeneratePRCheckoutStep now respects CheckoutDisabled flag
- Auto-disable checkout for pull_request_target when not explicitly configured
- Update tests to reflect new default behavior
- Recompile ai-moderator workflow (checkout: false now correctly removes PR checkout step)"

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix checkout PR branch step failure on closed pulls Disable checkout by default for pull_request_target workflows Jul 20, 2026
Copilot AI requested a review from pelikhan July 20, 2026 09:15
@pelikhan
pelikhan marked this pull request as ready for review July 20, 2026 09:16
Copilot AI review requested due to automatic review settings July 20, 2026 09:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates checkout generation for pull_request_target workflows to avoid unavailable PR-head refs.

Changes:

  • Auto-disables implicit checkout for pull_request_target.
  • Makes checkout: false suppress PR-specific checkout.
  • Adds tests and recompiles the affected workflow.
Show a summary per file
File Description
pkg/workflow/workflow_builder.go Adds automatic checkout disabling.
pkg/workflow/pr.go Honors disabled checkout.
pkg/workflow/pr_test.go Tests checkout suppression.
pkg/workflow/pr_ready_for_review_checkout_test.go Tests compiled checkout behavior.
pkg/workflow/pull_request_target_validation_test.go Updates validation expectations.
.github/workflows/ai-moderator.lock.yml Removes generated PR checkout steps.

Review details

Tip

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

Comments suppressed due to low confidence (2)

pkg/workflow/workflow_builder.go:112

  • This check runs before imported checkout settings are merged, so a pull_request_target workflow that inherits checkout: from a shared workflow is marked disabled and line 129 then drops the inherited configuration entirely. That breaks the established checkout-import contract (checkout_import_test.go:14-68). Treat a non-empty MergedCheckout as an explicit configuration, or move auto-disablement after import merging.
	if len(workflowData.CheckoutConfigs) == 0 {

pkg/workflow/workflow_builder.go:116

  • The repository accepts on as a scalar or list (event_validation.go:116-130), but this map-only assertion leaves on: pull_request_target and on: [pull_request_target] with checkout enabled. Those valid forms therefore retain the failing PR-head checkout and bypass the new safe default. Reuse the existing event-presence helper, which handles all supported forms.
			if onVal, ok := result.Frontmatter["on"]; ok {
				if onMap, ok := onVal.(map[string]any); ok {
					if _, hasPRT := onMap["pull_request_target"]; hasPRT {
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Medium

Comment on lines +296 to +297
expectPRCheckout: true,
description: "pull_request_target with explicit checkout mapping should generate 'Checkout PR branch' step",

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.

Addressed in f534297 and 37bc639. The original code only checked CheckoutDisabled, so an explicit checkout: mapping on a pull_request_target workflow still emitted checkout_pr_branch.cjs. The fix adds an IsPullRequestTarget field (set in workflow_builder.go for all pull_request_target-only workflows) and checks it first in ShouldGeneratePRCheckoutStep — so the PR-head checkout step is always suppressed for pull_request_target, regardless of whether an explicit actions/checkout config was provided. The test expectation was corrected to expectPRCheckout: false.

Comment thread pkg/workflow/workflow_builder.go Outdated
Comment on lines +108 to +111
// Auto-disable checkout for pull_request_target workflows when not explicitly configured.
// For pull_request_target events, the head branch is often deleted (closed/merged PRs)
// or inaccessible (fork PRs), causing the "Checkout PR branch" step to fail.
// Users who need checkout can explicitly set a checkout configuration in frontmatter.

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.

Updated in commit 36427c9 and expanded further in f534297. docs/src/content/docs/reference/checkout.md now has an explicit ## pull_request_target Checkout section describing: (1) the PR head checkout is auto-disabled by default for pull_request_target workflows, (2) opt-in with an explicit checkout: mapping for a trusted ref (e.g. base SHA), and a warning against using head.sha or refs/pull/.../head in a privileged job. The checkout: false description was also updated to say it suppresses both actions/checkout and the PR-specific step.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Test message

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: Disable checkout by default for pull_request_target workflows

The fix correctly addresses the hard-fail on deleted/fork head branches and the pre-existing bug where checkout: false was ignored. One blocking issue found:

Mixed-trigger regression (see inline comment on workflow_builder.go:116): a workflow with both pull_request and pull_request_target triggers has checkout incorrectly auto-disabled for all events. The guard must verify pull_request (and other safe triggers) are absent before setting CheckoutDisabled = true.

Non-blocking observations
  • The test suite covers the three key scenarios well (no-checkout-key, explicit false, explicit mapping).
  • ai-moderator.lock.yml recompile looks correct — the removed steps align with the disabled checkout.
  • The ShouldGeneratePRCheckoutStep early-exit placement is clean and correct.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 28.3 AIC · ⌖ 4.44 AIC · ⊞ 5K

Comment thread pkg/workflow/workflow_builder.go Outdated
if _, checkoutExplicitlySet := result.Frontmatter["checkout"]; !checkoutExplicitlySet {
if onVal, ok := result.Frontmatter["on"]; ok {
if onMap, ok := onVal.(map[string]any); ok {
if _, hasPRT := onMap["pull_request_target"]; hasPRT {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mixed-trigger regression: if a workflow uses both pull_request and pull_request_target triggers, checkout is auto-disabled for all events — including pull_request events where head branches are accessible. The guard should skip auto-disabling when pull_request (or other normal checkout-compatible triggers) co-exist.

_, hasPR := onMap["pull_request"]
if hasPRT && !hasPR {
    workflowData.CheckoutDisabled = true
}

@copilot please address this.

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.

Fixed in f534297. The auto-disable block now guards with !hasPR (using the new frontmatterHasTrigger helper) so workflows combining pull_request and pull_request_target triggers are not affected.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

REQUEST_CHANGES — 3 correctness bugs in the auto-disable logic

The core idea is sound but the implementation has three bugs that will silently misfire in real-world usage.

Blocking findings

1. (Critical) on: scalar/list form bypasses auto-disableon: pull_request_target or on: [push, pull_request_target] silently skips the type assertion and checkout is never disabled. This is the most common YAML form in many workflows. See comment on line 115.

2. (High) Guard evaluates before imported checkout configs are merged — A workflow importing a shared workflow that provides checkout configs will have CheckoutConfigs empty at the guard, trigger auto-disable, and permanently lose the shared checkout. See comment on line 112.

3. (Medium) Multi-trigger workflows have checkout globally killed — Any workflow with pull_request_target alongside another trigger (e.g. workflow_dispatch, push) will have CheckoutDisabled = true for all triggers, not just the pull_request_target path. See comment on line 116.

All three bugs share the same root: the guard is doing trigger detection + checkout-config awareness in a fragile, ad-hoc way. A small refactor to (a) use a proper YAML-form-aware hasTrigger helper, (b) move the block after import merging, and (c) require single-trigger exclusivity would close all three.

🔎 Code quality review by PR Code Quality Reviewer · 36.5 AIC · ⌖ 4.79 AIC · ⊞ 5.6K
Comment /review to run again

Comment thread pkg/workflow/workflow_builder.go Outdated
if len(workflowData.CheckoutConfigs) == 0 {
if _, checkoutExplicitlySet := result.Frontmatter["checkout"]; !checkoutExplicitlySet {
if onVal, ok := result.Frontmatter["on"]; ok {
if onMap, ok := onVal.(map[string]any); ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

on: as scalar or list silently bypasses the auto-disable logic — if a user writes on: pull_request_target (scalar) or on: [push, pull_request_target] (sequence), the inner onMap type assertion fails and CheckoutDisabled is never set, defeating the safety mechanism.

💡 Suggested fix

Extract a helper that handles all three YAML on: forms:

func hasTrigger(onVal any, trigger string) bool {
    switch v := onVal.(type) {
    case string:
        return v == trigger
    case []any:
        for _, item := range v {
            if s, ok := item.(string); ok && s == trigger {
                return true
            }
        }
    case map[string]any:
        _, ok := v[trigger]
        return ok
    }
    return false
}

Then replace the nested map assertion with hasTrigger(onVal, "pull_request_target"). The same blind spot exists at lines 159 and 615 in this 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.

Fixed in f534297. Added frontmatterHasTrigger(onVal, trigger string) bool in workflow_builder.go that handles all three YAML on: forms: string scalar (on: pull_request_target), sequence (on: [push, pull_request_target]), and mapping (on:\n pull_request_target:\n types: [closed]). Both hasPRT and hasPR are detected using this helper, so on: pull_request_target (scalar) and on: [pull_request_target] (sequence) correctly trigger auto-disable.

Comment thread pkg/workflow/workflow_builder.go Outdated
// For pull_request_target events, the head branch is often deleted (closed/merged PRs)
// or inaccessible (fork PRs), causing the "Checkout PR branch" step to fail.
// Users who need checkout can explicitly set a checkout configuration in frontmatter.
if len(workflowData.CheckoutConfigs) == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Auto-disable guard fires before imported checkout configs are merged, so a workflow that imports a shared workflow providing checkout configs will have CheckoutConfigs empty at this check, trigger auto-disable, and lose the shared checkout — which is the opposite of the intended behavior.

💡 Suggested fix

Move the auto-disable block to after the import-merge block (lines 125–144), or alternatively re-evaluate after merging:

// ... merge imported checkout configs first ...

// Auto-disable for pull_request_target only when nothing was configured at all
if len(workflowData.CheckoutConfigs) == 0 && !workflowData.CheckoutDisabled {
    if hasPullRequestTarget(result.Frontmatter) {
        workflowData.CheckoutDisabled = true
    }
}

Placing it after the merge ensures that an explicitly imported checkout config prevents auto-disable.

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.

Fixed in f534297. The auto-disable block was moved to after the import-merge block. Imports are merged at lines 112–129; auto-disable runs at lines 131–152 (after). The guard checks len(workflowData.CheckoutConfigs) == 0 at that point, so an imported checkout config (merged in at 127) prevents auto-disable. The comment in the code also explicitly notes this ordering: "This block runs after import merging so that imported checkout configs prevent auto-disable."

Comment thread pkg/workflow/workflow_builder.go Outdated
if _, checkoutExplicitlySet := result.Frontmatter["checkout"]; !checkoutExplicitlySet {
if onVal, ok := result.Frontmatter["on"]; ok {
if onMap, ok := onVal.(map[string]any); ok {
if _, hasPRT := onMap["pull_request_target"]; hasPRT {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Multi-trigger workflows have checkout globally disabled — if a workflow uses both push and pull_request_target in its on: map, this block sets CheckoutDisabled = true for all triggers, breaking the push path which has no such restriction.

💡 Suggested fix

This is inherently tricky because CheckoutDisabled is a single boolean. The auto-disable should only apply when pull_request_target is the only (or at least the primary) trigger. Either restrict auto-disable to single-trigger pull_request_target workflows, or introduce per-trigger checkout configuration rather than a global flag.

As a minimal safe approach, skip auto-disable when other triggers coexist:

if onMap, ok := onVal.(map[string]any); ok {
    _, hasPRT := onMap["pull_request_target"]
    onlyPRT := hasPRT && len(onMap) == 1
    if onlyPRT {
        workflowData.CheckoutDisabled = true
    }
}

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.

Fixed in f534297. Auto-disable only fires when hasPRT && !hasPR — i.e., when pull_request_target is present but pull_request is not. Workflows combining pull_request_target with other triggers like push or workflow_dispatch do still get IsPullRequestTarget = true (to suppress checkout_pr_branch.cjs), but CheckoutDisabled is only auto-set when no pull_request trigger co-exists and no explicit checkout config is present.

…_request_target

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

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

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

📄 Draft ADR committed: docs/adr/46771-disable-checkout-by-default-for-pull-request-target.md — review and complete it before merging.

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

📋 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 could not 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-46771: Disable PR Branch Checkout by Default for pull_request_target Workflows

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.

📋 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., 46771-disable-checkout-by-default-for-pull-request-target.md for PR #46771).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 57.6 AIC · ⌖ 10.2 AIC · ⊞ 4.4K ·
Comment /review to run again

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 5 test(s): 5 design, 0 implementation, 0 violation(s).

Coverage: All 5 tests directly verify the feature invariant: pull_request_target workflows auto-disable checkout when not explicitly configured.

Tests Added:

  • TestShouldGeneratePRCheckoutStep_CheckoutDisabled (2 unit test branches) — Verifies the control gate at function level with clear boundary assertions
  • TestPullRequestTargetCheckoutDisabledByDefault (3 integration test scenarios) — End-to-end workflow compilation tests covering: no checkout key, explicit false, and trusted checkout mapping
  • pull_request_target_validation_test.go (4 case updates) — Updated validation rule expectations to reflect auto-disable feature

Quality Metrics:

  • Design tests: 5/5 (100%)
  • Edge/error coverage: 5/5 (100%)
  • Violations: 0 (all build tags correct, no mock libraries)
  • Implementation tests: 0/5 (0% - threshold: 30%)
  • Test inflation: 6.7:1 (justified by integration test scaffolding and workflow YAML samples)

Verdict: ✅ Passed. Tests provide high-confidence behavioral coverage with no coding guideline violations. Recommend approval.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 19.8 AIC · ⌖ 11.2 AIC · ⊞ 7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%). All tests are behavioral contracts with comprehensive edge-case coverage. No mock library violations or coding guideline issues. Ready to merge.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd — requesting changes on two correctness issues and one test-coverage gap.

📋 Key Themes & Highlights

Key Themes

  • Incomplete trigger-format coverage (/diagnosing-bugs): The auto-disable logic only handles on: as a YAML map. The string shorthand (on: pull_request_target) and list form (on: [pull_request_target, push]) are silently skipped, leaving those workflows still generating the failing checkout step.
  • Redundant state write (/diagnosing-bugs): The outer CheckoutConfigs == 0 guard does not also check !CheckoutDisabled, so a checkout: false workflow re-sets the flag unnecessarily — fragile if the flag gains new semantics.
  • Shallow integration test (/tdd): The trusted base-SHA opt-in test only asserts presence of "Checkout PR branch", not the absence of insecure head-ref patterns, so it wouldn't catch a future regression that checks out the wrong ref.

Positive Highlights

  • ✅ Clean, minimal fix in pr.goCheckoutDisabled guard placed at the top of ShouldGeneratePRCheckoutStep before any other logic.
  • ✅ Comprehensive test matrix covering no-checkout-key, explicit false, and explicit mapping cases.
  • ✅ Correctly threads the two bugs together: the pre-existing checkout: false bug and the new auto-disable behaviour are fixed in a single coherent change.
  • ✅ Lock file recompiled to remove the previously-broken steps.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 47.7 AIC · ⌖ 4.83 AIC · ⊞ 6.7K
Comment /matt to run again

Comment thread pkg/workflow/workflow_builder.go Outdated
if len(workflowData.CheckoutConfigs) == 0 {
if _, checkoutExplicitlySet := result.Frontmatter["checkout"]; !checkoutExplicitlySet {
if onVal, ok := result.Frontmatter["on"]; ok {
if onMap, ok := onVal.(map[string]any); ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] The map[string]any assertion silently skips auto-disable when on: is a string or list — e.g. on: pull_request_target or on: [pull_request_target, push] — leaving those workflows exposed to the original failure.

💡 Suggested fix

After the existing map-path check, also handle the string form:

if onStr, ok := onVal.(string); ok && onStr == "pull_request_target" {
    workflowBuilderLog.Print("Auto-disabling checkout for pull_request_target workflow")
    workflowData.CheckoutDisabled = true
}

And for the list form ([]any), iterate and check each element. Without this, a workflow authored with on: pull_request_target (the shorthand YAML syntax) will not receive the auto-disable.

@copilot please address this.

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.

Fixed in f534297. Added frontmatterHasTrigger(onVal, trigger) that handles all three YAML on: forms: string scalar (on: pull_request_target), sequence (on: [pull_request_target, push]), and mapping. Both the auto-disable check and the IsPullRequestTarget flag now use this helper.

Comment thread pkg/workflow/workflow_builder.go Outdated
// For pull_request_target events, the head branch is often deleted (closed/merged PRs)
// or inaccessible (fork PRs), causing the "Checkout PR branch" step to fail.
// Users who need checkout can explicitly set a checkout configuration in frontmatter.
if len(workflowData.CheckoutConfigs) == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] The outer guard len(workflowData.CheckoutConfigs) == 0 is checked before checkoutExplicitlySet, so a workflow that sets checkout: false first populates CheckoutDisabled = true (line 102) but also happens to have empty CheckoutConfigs, making it pass this outer condition — and then re-sets CheckoutDisabled = true unnecessarily for any pull_request_target workflow. This is harmless today, but it is logically redundant and fragile: if CheckoutDisabled is used as a sentinel for something in the future the double-write may mask a real state.

💡 Suggestion

Add an early exit for the already-explicitly-disabled case:

if !workflowData.CheckoutDisabled && len(workflowData.CheckoutConfigs) == 0 {
    if _, checkoutExplicitlySet := result.Frontmatter["checkout"]; !checkoutExplicitlySet {
        ...
    }
}

This makes the intent explicit: only auto-disable when nothing has been configured.

@copilot please address this.

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.

Fixed in f534297. The outer guard now reads if !workflowData.CheckoutDisabled && len(workflowData.CheckoutConfigs) == 0 { so a workflow with checkout: false (which already sets CheckoutDisabled = true) bypasses the block entirely and avoids the redundant write.

Workflow triggered when a PR is opened with a trusted base checkout.
`,
expectPRCheckout: true,
description: "pull_request_target with explicit checkout mapping should generate 'Checkout PR branch' step",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The test asserts expectPRCheckout: true for the trusted base-SHA opt-in path but only checks for the presence of "Checkout PR branch" — it doesn't verify that an insecure head ref (e.g. refs/pull/.../head) is absent. A future regression in checkout_pr_branch.cjs that resolves to the head ref would still pass this test while silently breaking the security guarantee.

💡 Suggested assertion

After the existing hasPRCheckout check, add:

if hasPRCheckout {
    for _, insecure := range []string{"refs/pull", "head.ref", "head_ref"} {
        if strings.Contains(lockStr, insecure) {
            t.Errorf("%s: expected trusted base checkout but lock file contains insecure pattern %q", tt.description, insecure)
        }
    }
}

This makes the integration test a genuine security regression guard, not just a presence check.

@copilot please address this.

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.

Addressed in f534297 and 37bc639. The root cause was that ShouldGeneratePRCheckoutStep did not suppress checkout_pr_branch.cjs for pull_request_target workflows — so adding a security assertion on an expectPRCheckout: true case would always fail. The fix: ShouldGeneratePRCheckoutStep now returns false for pull_request_target-only workflows (via the new IsPullRequestTarget field), and the test case is corrected to expectPRCheckout: false. A refs/pull absence assertion is still present as a regression guard for any future test case where a PR checkout step IS expected.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

- Add frontmatterHasTrigger() helper supporting string/[]any/map forms
  of the YAML `on:` field (fixes silent bypass when on: is scalar/list)
- Add IsPullRequestTarget field to WorkflowData; set it when
  pull_request_target trigger is present without pull_request co-trigger
- ShouldGeneratePRCheckoutStep now returns false for IsPullRequestTarget
  to prevent checkout_pr_branch.cjs from fetching refs/pull/<n>/head in
  a privileged context (the trusted base-SHA checkout still happens)
- Move auto-disable block after import-merge so imported checkout configs
  prevent auto-disable
- Guard auto-disable with !CheckoutDisabled to avoid redundant writes
- Fix mixed-trigger regression: only auto-disable when pull_request
  trigger is absent
- Update integration test: trusted base-SHA checkout case no longer
  expects the PR checkout step (expectPRCheckout: false)
- Add security assertion: PR checkout step must not contain refs/pull
- Add unit tests for frontmatterHasTrigger and IsPullRequestTarget
- Update checkout.md docs to describe pull_request_target defaults and
  that checkout: false suppresses both steps

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…ratePRCheckoutStep

- Remove head.ref/head_ref from security assertion (only check refs/pull
  which is the specific pattern emitted by checkout_pr_branch.cjs)
- Clarify pr.go comment: suppression is for pull_request_target-only
  workflows (not all pull_request_target workflows)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

[pr-sous-chef] @copilot please run the pr-finisher skill, address the latest maintainer request, and rerun checks once the branch is ready.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • pi.dev

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "pi.dev"

See Network Configuration for more information.

Generated by 👨‍🍳 PR Sous Chef · 7.59 AIC · ⌖ 5.86 AIC · ⊞ 5.4K ·
Comment /souschef to run again

Copilot AI requested a review from gh-aw-bot July 20, 2026 11:04
@pelikhan
pelikhan merged commit 4972443 into main Jul 20, 2026
24 checks passed
@pelikhan
pelikhan deleted the copilot/fix-checkout-pr-branch-failure branch July 20, 2026 11:55
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.14

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.

"Checkout PR branch" step hard-fails on merged pull_request_target: closed runs (deleted head branch)

4 participants