Skip to content

ci(dispatch): allow checkout@v7 fork PR checkout in all reusable workflows#3696

Merged
ggallen merged 1 commit into
fullsend-ai:mainfrom
ggallen:worktree-fix-3689-dispatch-fork-checkout
Jul 8, 2026
Merged

ci(dispatch): allow checkout@v7 fork PR checkout in all reusable workflows#3696
ggallen merged 1 commit into
fullsend-ai:mainfrom
ggallen:worktree-fix-3689-dispatch-fork-checkout

Conversation

@ggallen

@ggallen ggallen commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

  • actions/checkout@v7 blocks fork PR head checkouts on pull_request_target unless allow-unsafe-pr-checkout is set
  • The github context is inherited through workflow_call chains, so github.event_name is pull_request_target in every reusable workflow — not just the dispatch Route job where the failure was first observed
  • The initial triage identified only reusable-dispatch.yml, but fixing just the dispatch would unblock routing only to have the stage workflow (review, triage, code, fix, retro, prioritize) fail at its own "Checkout config repository" step with the same error
  • All 7 checkout steps are safe: they read only .fullsend/ config files, no fork code is executed, and credentials are not persisted
  • Follows the same pattern established in fix(ci): restore e2e fork PR checkout after actions/checkout@v7 bump #2503 (e2e) and ci(functional-tests): use pull_request_target for fork PR support #2534 (functional-tests)

Fixes #3689

Test plan

  • Re-run fork PR dispatch on an enrolled repo (e.g. openshift-pipelines/opc); Route job and stage job both check out config successfully
  • Verify non-fork PRs and push events are unaffected (allow-unsafe-pr-checkout expression evaluates to false outside pull_request_target)

🤖 Generated with Claude Code

@ggallen ggallen requested a review from a team as a code owner July 8, 2026 17:36
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Allow safe fork PR checkout in Route job with actions/checkout@v7

🐞 Bug fix ⚙️ Configuration changes 🕐 Less than 10 minutes

Grey Divider

AI Description

• Unblocks fork PR head checkouts for the Route job under pull_request_target.
• Adds a guarded checkout@v7 override only when the event is pull_request_target.
• Documents why this checkout remains safe (sparse config-only, no credentials persisted).
Diagram

graph TD
  A["Route job"] --> B{{"Event is pull_request_target?"}} --> C["actions/checkout@v7"] --> D["Read .fullsend/config.yaml"] --> E["Determine stage"]
  F["Fork PR head"] --> C
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fetch config via GitHub API instead of checkout
  • ➕ Avoids any PR checkout policy bypass entirely
  • ➕ Can be limited to a single file read with explicit permissions
  • ➖ More implementation complexity (API calls, auth, error handling)
  • ➖ Harder to mirror local file semantics (YAML parsing, refs) in a simple workflow step
2. Pin to an earlier actions/checkout version
  • ➕ Avoids needing allow-unsafe-pr-checkout
  • ➖ Defers rather than addresses the new security model in v7
  • ➖ Risks missing security fixes and diverging from ecosystem defaults
3. Change routing workflow trigger away from pull_request_target
  • ➕ May eliminate the fork checkout restriction depending on trigger
  • ➕ Simplifies threat model for fork PR code
  • ➖ Often incompatible with required permissions/secrets for dispatch routing
  • ➖ May break existing routing behavior or reduce capabilities

Recommendation: Proceed with the current approach: the Route job’s sparse checkout is intentionally limited to .fullsend/config.yaml and uses persist-credentials: false, so enabling allow-unsafe-pr-checkout only for pull_request_target is a minimal, well-scoped fix aligned with prior patterns. Consider the GitHub API approach only if policy requires eliminating all fork ref checkouts, even config-only ones.

Files changed (1) +4 / -0

Other (1) +4 / -0
reusable-dispatch.ymlGate unsafe fork PR checkout for config-only sparse checkout +4/-0

Gate unsafe fork PR checkout for config-only sparse checkout

• Adds allow-unsafe-pr-checkout to the checkout step, enabled only for pull_request_target events. Includes inline rationale explaining the checkout is limited to .fullsend/config.yaml and does not persist credentials.

.github/workflows/reusable-dispatch.yml

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 5:37 PM UTC · Ended 5:40 PM UTC
Commit: e8381e3 · View workflow run →

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Site preview

Preview: https://3b1d1d82-site.fullsend-ai.workers.dev

Commit: 08b3f523f14a902e19dd217dd4219e2de9039846

@ggallen ggallen force-pushed the worktree-fix-3689-dispatch-fork-checkout branch from 508a1ab to 806b507 Compare July 8, 2026 17:40
@ggallen ggallen changed the title fix(dispatch): allow checkout@v7 fork PR checkout in Route job fix(ci): allow checkout@v7 fork PR checkout in all reusable workflows Jul 8, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 5:41 PM UTC · Ended 5:42 PM UTC
Commit: e8381e3 · View workflow run →

@ggallen ggallen force-pushed the worktree-fix-3689-dispatch-fork-checkout branch from 806b507 to c6d6936 Compare July 8, 2026 17:42
@qodo-code-review

qodo-code-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 54 rules

Grey Divider


Informational

1. Fork config bypasses gates ✓ Resolved 🐞 Bug ⛨ Security
Description
Enabling allow-unsafe-pr-checkout on the Route job allows .fullsend/config.yaml to be read from
a fork PR under pull_request_target, and that file is then used for kill-switch and role gating
that controls whether stage workflows run. Since pull_request_target routes closed events to
retro without auth, a fork PR can potentially enable/disable role gates via config changes and
trigger privileged workflows with secrets/ID-token permissions in the trusted context.
Code

.github/workflows/reusable-dispatch.yml[R87-90]

+          # checkout@v7 blocks fork PR checkouts on pull_request_target by default.
+          # Safe here: sparse checkout reads only .fullsend/config.yaml, no fork
+          # code is executed, and credentials are not persisted.
+          allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }}
Relevance

⭐ Low

Team previously accepted allow-unsafe-pr-checkout and rejected hardening/gating of
pull_request_target retro/secret exposure risks.

PR-#2534
PR-#1688
PR-#2503

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new unsafe checkout opt-in occurs before routing/gating, and the Route job’s stage output
depends on role-check, which reads .fullsend/config.yaml. pull_request_target closed events
always route to retro, and retro is a privileged reusable workflow receiving secrets and
id-token permissions; therefore letting fork PR content control the config gate is a security
boundary regression.

.github/workflows/reusable-dispatch.yml[76-79]
.github/workflows/reusable-dispatch.yml[81-90]
.github/workflows/reusable-dispatch.yml[260-270]
.github/workflows/reusable-dispatch.yml[347-384]
.github/workflows/reusable-dispatch.yml[519-538]
.github/workflows/reusable-retro.yml[54-63]
.github/workflows/reusable-retro.yml[146-152]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The Route job opts into `allow-unsafe-pr-checkout` on `pull_request_target`, which makes the workflow read `.fullsend/config.yaml` from PR content (potentially a fork). That same file is later used to enforce kill-switch and role gating, so a fork PR can influence whether privileged stage workflows (e.g. `retro`) run.

### Issue Context
- The Route job uses `.fullsend/config.yaml` for `kill_switch` and `roles[]` checks, and the Route job’s `outputs.stage` is suppressed only when `role-check` marks `skipped=true`.
- `pull_request_target` with `action=closed` is routed to `retro` unconditionally.
- `retro` runs a reusable workflow that mints tokens and sets up GCP (secrets/id-token), so the gate must be based on trusted (base-branch) config.

### Fix Focus Areas
- .github/workflows/reusable-dispatch.yml[81-91]

### Proposed fix
1) Change the "Checkout caller repository" step to explicitly check out a trusted base ref for `pull_request_target` (e.g. `github.event.pull_request.base.sha` or `github.sha`) so `.fullsend/config.yaml` is sourced from the base branch.
2) Once `ref:` is forced to the base ref, remove `allow-unsafe-pr-checkout` (or keep it explicitly `false`).

Example:
```yaml
- name: Checkout caller repository
 uses: actions/checkout@...
 with:
   ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }}
   persist-credentials: false
   sparse-checkout: .fullsend/config.yaml
   sparse-checkout-cone-mode: false
```

This preserves the goal (Route can read config and route) while keeping kill-switch/role gating anchored to trusted config.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 5:43 PM UTC · Ended 5:48 PM UTC
Commit: e8381e3 · View workflow run →

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ggallen ggallen force-pushed the worktree-fix-3689-dispatch-fork-checkout branch from c6d6936 to d113ad0 Compare July 8, 2026 17:48
@ggallen ggallen changed the title fix(ci): allow checkout@v7 fork PR checkout in all reusable workflows ci(dispatch): allow checkout@v7 fork PR checkout in all reusable workflows Jul 8, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 5:49 PM UTC · Ended 5:52 PM UTC
Commit: e8381e3 · View workflow run →

@ggallen ggallen force-pushed the worktree-fix-3689-dispatch-fork-checkout branch from d113ad0 to 3f0d6f5 Compare July 8, 2026 17:51
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 5:53 PM UTC · Completed 6:04 PM UTC
Commit: 3f0d6f5 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review

Findings

Medium


Prior review findings resolved: The [parameter-order] low-severity finding from the prior review has been addressed — the reusable-dispatch.yml with: block now follows a consistent parameter ordering (ref, persist-credentials, allow-unsafe-pr-checkout) matching the other 6 workflows. The [permission-expansion] and [missing-ref-pinning] findings from the initial review remain resolved — all 7 workflows include persist-credentials: false and explicit ref: pinning to the base SHA.

Previous run

Review

Findings

Medium

Low

  • [parameter-order] .github/workflows/reusable-dispatch.yml — The dispatch workflow already had a with: block with persist-credentials, sparse-checkout, and sparse-checkout-cone-mode. The new ref and allow-unsafe-pr-checkout parameters are interleaved around existing parameters, creating a different ordering pattern than the other 6 workflows where the entire with: block is new. Minor consistency nit.

Prior review findings resolved: The [permission-expansion] and [missing-ref-pinning] low-severity findings from the prior review have both been addressed — all 6 stage workflows now include persist-credentials: false and explicit ref: pinning to the base SHA, matching the dispatch workflow's defensive posture.

Previous run (2)

Review

Findings

Medium

Low

  • [permission-expansion] .github/workflows/reusable-code.yml — Six stage workflows (code, fix, prioritize, retro, review, triage) add allow-unsafe-pr-checkout without persist-credentials: false. The inline comment claims "credentials are not persisted" but persist-credentials defaults to true in actions/checkout@v7. Since only the default ambient GITHUB_TOKEN is used (no custom token: parameter), this does not expand the attack surface — the token is already available to all subsequent steps via ${{ github.token }}. Adding persist-credentials: false would make the comment factually accurate and align with the dispatch workflow's defensive posture.

  • [missing-ref-pinning] .github/workflows/reusable-code.yml — Six stage workflows rely on the implicit pull_request_target base-branch checkout default rather than explicitly pinning ref: to the base SHA as reusable-dispatch.yml does. The default behavior is safe (pull_request_target checks out the base branch, not the fork head), so this is a marginal defense-in-depth improvement for consistency across all reusable workflows.


Labels: PR modifies CI workflow files under .github/workflows/ for dispatch routing

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment component/ci CI pipelines and checks component/dispatch Workflow dispatch and triggers labels Jul 8, 2026
@ggallen ggallen force-pushed the worktree-fix-3689-dispatch-fork-checkout branch from 3f0d6f5 to 33d6051 Compare July 8, 2026 18:06
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 6:07 PM UTC · Completed 6:19 PM UTC
Commit: 33d6051 · View workflow run →

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed via a 5-agent review squad. The original revision had two real issues (both independently confirmed by 3 agents): the "credentials are not persisted" comment was inaccurate for 6/7 files since persist-credentials wasn't set, and the checkout resolved the PR merge ref rather than the base branch, so fork-controlled customized/ overlay content could land in the workspace.

Both were fixed in the latest push, which pins ref to github.event.pull_request.base.sha under pull_request_target and sets persist-credentials: false across all 7 reusable workflows — this removes fork content from the checkout entirely rather than just working around the action's ref-format guard. CI is green (commit-lint, e2e, functional-tests, DCO). Fix is complete and consistent across all 7 reusable workflows that run under pull_request_target.

Non-blocking follow-up: the checkout block (comment + ref pin + persist-credentials + allow-unsafe-pr-checkout) is now duplicated identically across 7 files — worth extracting into a composite action at some point, but not required for this fix.

@fullsend-ai-review fullsend-ai-review Bot removed the requires-manual-review Review requires human judgment label Jul 8, 2026
@fullsend-ai-review fullsend-ai-review Bot added the requires-manual-review Review requires human judgment label Jul 8, 2026

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM.

…flows

checkout@v7 added a security guard that blocks fork PR head checkouts
in pull_request_target workflows by default. The github context is
inherited through workflow_call chains, so github.event_name is
pull_request_target in every reusable workflow — not just the dispatch
Route job where the failure was first observed.

All 7 "Checkout config repository" steps are safe: they read only
.fullsend/ config files, no fork code is executed, and credentials
are not persisted. The dispatch Route job additionally pins its ref
to the base branch so kill-switch and role gating always reads
trusted config.

Stage workflows now also include explicit ref pinning and
persist-credentials: false for consistency with the dispatch workflow.

Fixes fullsend-ai#3689

Signed-off-by: Greg Allen <gallen@redhat.com>
Signed-off-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Greg Allen <gallen@redhat.com>
@ggallen ggallen force-pushed the worktree-fix-3689-dispatch-fork-checkout branch from 33d6051 to 08b3f52 Compare July 8, 2026 18:24
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 6:25 PM UTC · Completed 6:36 PM UTC
Commit: 08b3f52 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment and removed requires-manual-review Review requires human judgment labels Jul 8, 2026

@ifireball ifireball left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@ggallen ggallen added this pull request to the merge queue Jul 8, 2026
Merged via the queue into fullsend-ai:main with commit 807037f Jul 8, 2026
21 checks passed
@ggallen ggallen deleted the worktree-fix-3689-dispatch-fork-checkout branch July 8, 2026 19:46
@fullsend-ai-retro

fullsend-ai-retro Bot commented Jul 8, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 7:48 PM UTC · Completed 7:58 PM UTC
Commit: 08b3f52 · View workflow run →

@fullsend-ai-retro

Copy link
Copy Markdown

PR #3696 fixed actions/checkout@v7 fork PR checkout failures across 7 reusable workflows. The triage agent correctly identified the root cause but under-scoped the fix to 1 of 7 affected files, missing github context inheritance through workflow_call chains. The code agent implemented the narrow scope correctly (exit code 0) but failed at the post-script because pinact and lychee system tools were missing from the sandbox. The human author manually fixed all 7 files. The review agent performed well — it caught two real security issues (missing persist-credentials: false and missing ref: pinning to base SHA) that were independently confirmed by a 5-agent human review squad. Four review runs were cancelled by rapid force pushes (existing issue #1014). Total: ~2.5 hours from issue to merge. Two proposals filed below; a third observation (evidence for #1014 re: review dispatch debouncing) is noted in this summary only.

Proposals filed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/ci CI pipelines and checks component/dispatch Workflow dispatch and triggers requires-manual-review Review requires human judgment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reusable-dispatch: actions/checkout@v7 rejects fork PR checkouts in Route job

4 participants