Skip to content

ci: run PR workflows without pull_request_target - #2409

Merged
tamirdresher merged 1 commit into
devfrom
squad/2407-pr-workflows-without-pull-request-target
Nov 8, 2026
Merged

tamirdresher merged 1 commit into
devfrom
squad/2407-pr-workflows-without-pull-request-target

Conversation

@tamirdresher

Copy link
Copy Markdown
Collaborator

What

Moves the four PR workflows off pull_request_target: squad-heartbeat, squad-impact, squad-pr-readiness, and squad-repo-health. Each one now does its untrusted work in a pull_request run (read-only token, no secrets). A workflow_run job then runs the default branch's code and writes back to the PR with minimal permissions. New files:

  • squad-heartbeat-pr-closed.yml, squad-impact-request.yml, squad-pr-readiness-request.yml, squad-repo-health-report.yml
  • scripts/workflow-run-pr-binding.mjs, scripts/repo-health-report.mjs

The shadow workflow, the runbook, and their tests are updated too.

Working as release-engineer (Release Engineer).

Why

The github org's Actions policy blocks pull_request_target. Since the transfer, every run of these workflows has ended in startup_failure with "Event 'pull_request_target' is not allowed to trigger Actions workflows" (41 runs, for example 42305876712). GitHub will also block the event by default in public repositories starting 2026-12-02 (changelog).

Closes #2407

How

  • Trust boundary: no job checks out or runs PR-head code while holding a write token or secrets. Before posting, writers check that:
    • the claim's head SHA and head repository match the triggering run;
    • the base repository is this one;
    • the PR is open and targets dev;
    • the PR is one of the run's listed PRs.
  • Artifacts are untrusted data. They're downloaded by exact name, read with a size limit, and parsed against an exact schema.
  • Impact: the writer still computes the report from the PR files API with default-branch scripts, so a fork can't forge its own risk tier.
  • PR Readiness: draft state and labels now come from the bound API PR instead of the event payload. The Squad CI path used to pass null labels, so skip-changelog was ignored there. It's honored now.
  • Repo Health:
    • The scanners run from the PR base commit in the pull_request run. The PR head is fetched only as Git data.
    • The reporter posts only if squad-repo-health.yml is unchanged, judged by comparing blobs at the run's head SHA and its merge base with dev. This closes a time-of-check/time-of-use gap found in review: pushing after approval can't change the result.
    • Scanner output must match an exact shape. Malformed output now changes no comment. Before, it counted as clean and deleted the stale comment.
  • Disclosed trade-off: under pull_request, a PR can change the BLOCKING Bootstrap Protection job definition. CODEOWNERS is the compensating control. dev doesn't currently require code-owner review, so maintainers should enable required code-owner review on dev.
  • Behavior changes:
    • Comments on fork PRs appear after a maintainer approves the runs, the same approval Squad CI already needs.
    • PRs with merge conflicts don't refresh until the conflict is resolved.
    • A PR that edits squad-repo-health.yml gets no posted findings.
  • Review: security-reviewer rejected the first round (a time-of-check/time-of-use gap and an undisclosed control change). Both are fixed, and the follow-up review approved.
  • After merge: workflow_run only fires for workflows on the default branch, so run the runbook canary and watch the first fork PR.

Validation (branch on its own, Windows, Node 27):

Command Result
npx vitest run test/workflow-run-pr-binding.test.ts test/pull-request-workflow-split.test.ts test/scripts/impact-report-generator.test.ts test/scripts/repo-health-comment-transport.test.ts test/assignment-safety-attribution.test.ts ✅ 5 files, 272 passed, 2 skipped (one needs a file name Windows can't create, one needs jq and bash; both run on Linux CI)
node --test test/ci-concurrency.test.cjs test/workflows.test.cjs ✅ 48/48
actionlint 1.7.12 with shellcheck 0.10.0 on all changed workflows ✅ clean
Red proofs ✅ 25 cases in the first round, 20 mutations after the security revision (including a head-moves test that expects 0 writes), and 11 for the shadow-workflow change. Each one turns a test red

The other repo-move PRs are #2406, #2403, #2405, and #2404 (issues).


⚠️ Quick Check

  • If SDK/CLI source files changed: completed the applicable Changeset step below (npx changeset add / .changeset/*.md, direct CHANGELOG.md entry for maintainers, or skip-changelog label for no user-facing changes): N/A, no SDK/CLI source changes

PR Readiness Checklist

The PR readiness bot will validate these automatically after push.
Check each item before requesting review. See CONTRIBUTING.md for full details.

Branch & Commit

  • Branch created from dev (not main)
  • Branch is up to date with dev (git fetch upstream && git rebase upstream/dev)
  • Verified diff contains only intended changes (git diff --cached --stat)
  • PR is not in draft mode (mark ready when checks pass)
  • Commit history is clean (squash fixups before review)

Build & Test

  • npm run build passes: no package source changes (build verified on the combined repo-move tree)
  • npm test passes (all tests green): targeted suites above; the full suite runs in CI
  • npm run lint passes (type check clean)
  • npm run lint:eslint passes
  • For migration PRs (>20 files): include test output summary in PR description: N/A (19 files)

Changeset

  • Changeset added via npx changeset add (if packages/squad-sdk/src/ or packages/squad-cli/src/ changed): N/A
  • Or direct CHANGELOG.md entry (maintainers only — write-protected for external contributors)
  • Or skip-changelog label applied (if no user-facing changes)

Docs

  • README section updated (if new feature/module): .github/REPO_HEALTH_RUNBOOK.md updated
  • Docs feature page (if new user-facing capability): N/A

Exports

  • package.json subpath exports updated (if new module): N/A

Breaking Changes

None for users. The contributor-facing behavior changes are listed under How.

Waivers

None.

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit 4c744f2

PR Scope: 🔧 Infrastructure

⚠️ 2 item(s) to address before review

Status Check Details
✅ Single commit 1 commit — clean history
❌ Not in draft PR is still in draft — mark as ready for review when done
✅ Branch up to date Up to date with dev
❌ Copilot review No Copilot review yet — it may still be processing
✅ Changeset present No source files changed — changeset not required
✅ Scope clean No .squad/ or docs/proposals/ files
✅ No merge conflicts No merge conflicts
✅ Copilot threads resolved No Copilot review threads
✅ CI passing All checks passing

Files Changed (19 files, +2607 −343)

File +/−
.github/REPO_HEALTH_RUNBOOK.md +43 −16
.github/workflows/squad-heartbeat-pr-closed.yml +26 −0
.github/workflows/squad-heartbeat.yml +15 −8
.github/workflows/squad-impact-request.yml +44 −0
.github/workflows/squad-impact.yml +56 −12
.github/workflows/squad-pr-readiness-request.yml +47 −0
.github/workflows/squad-pr-readiness.yml +60 −21
.github/workflows/squad-repo-health-report.yml +100 −0
.github/workflows/squad-repo-health-shadow.yml +5 −2
.github/workflows/squad-repo-health.yml +80 −65
scripts/impact-utils/report-generator.mjs +1 −1
scripts/repo-health-comment.mjs +164 −29
scripts/repo-health-report.mjs +220 −0
scripts/workflow-run-pr-binding.mjs +272 −0
test/assignment-safety-attribution.test.ts +10 −1
test/pull-request-workflow-split.test.ts +380 −0
test/scripts/impact-report-generator.test.ts +23 −0
test/scripts/repo-health-comment-transport.test.ts +687 −188
test/workflow-run-pr-binding.test.ts +374 −0

Total: +2607 −343


This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.

The github org blocks pull_request_target, so heartbeat, impact,
pr-readiness and repo-health have failed at startup since the transfer.
GitHub also blocks it by default in public repositories from 2026-12-02.

Each workflow now does untrusted work under pull_request (read-only
token, no secrets) and writes back from a workflow_run job running
default-branch code. Writers bind the claim to the run (head SHA, head
repository, base repository, open PR targeting dev) before posting.
Repo Health decides whether a PR changed its own workflow from blobs
pinned to the run's head SHA and the merge base.

Closes #2407

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@tamirdresher
tamirdresher force-pushed the squad/2407-pr-workflows-without-pull-request-target branch from 3473468 to 4c744f2 Compare November 8, 2026 11:07
@tamirdresher
tamirdresher marked this pull request as ready for review November 8, 2026 13:45
Copilot AI auto review requested due to automatic review settings November 8, 2026 13:45
@tamirdresher
tamirdresher merged commit 78c5f44 into dev Nov 8, 2026
56 of 60 checks passed

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.

🟡 Changes recommended

Workflow-level concurrency allows skipped fork-PR runs and separate PRs sharing a branch key to cancel readiness refreshes, leaving checklists missing or stale.

1 open issue
What changed in this PR

Restores PR automation after the move to github/squad by replacing blocked pull_request_target triggers with unprivileged PR workflows and default-branch writers, addressing #2407.

Changes:

  • Split heartbeat, impact, readiness, and repo-health automation across pull_request and workflow_run.
  • Add shared PR binding, strict artifact validation, and commit-specific repo-health reporting.
  • Update regression tests, shadow verification, the runbook, and the impact-report link.
File Description
test/​workflow-run-pr-binding.test.ts Tests claim validation and PR/run binding.
test/​scripts/​repo-health-comment-transport.test.ts Tests artifact transport, reporting authority, and comment lifecycle.
test/​scripts/​impact-report-generator.test.ts Checks the updated tracking-issue link.
test/​pull-request-workflow-split.test.ts Checks workflow triggers, permissions, and split contracts.
test/​assignment-safety-attribution.test.ts Updates repository-specific heartbeat trigger expectations.
scripts/​workflow-run-pr-binding.mjs Binds claims to workflow runs and live PRs.
scripts/​repo-health-report.mjs Validates scanner artifacts and reporting authority.
scripts/​repo-health-comment.mjs Validates findings and adds commit-specific refusal notices.
scripts/​impact-utils/​report-generator.mjs Points the report footer at github/squad.
.github/​workflows/​squad-repo-health.yml Runs base-commit scanners and uploads results without write permissions.
.github/​workflows/​squad-repo-health-shadow.yml Extends shadow coverage and updates explanatory comments.
.github/​workflows/​squad-repo-health-report.yml Adds the default-branch repo-health writer.
.github/​workflows/​squad-pr-readiness.yml Binds completed runs before refreshing readiness.
.github/​workflows/​squad-pr-readiness-request.yml Uploads readiness claims from unprivileged PR runs.
.github/​workflows/​squad-impact.yml Computes and posts impact reports after binding.
.github/​workflows/​squad-impact-request.yml Uploads impact claims from unprivileged PR runs.
.github/​workflows/​squad-heartbeat.yml Receives PR-close signals through workflow_run.
.github/​workflows/​squad-heartbeat-pr-closed.yml Adds the unprivileged PR-close signal workflow.
.github/​REPO_HEALTH_RUNBOOK.md Documents trust limitations and split-workflow canary verification.

Review effort: Max (auto)


concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.workflow_run.pull_requests[0].number }}
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed, it's a regression from this PR. Before it, the group was keyed by PR number (pull_request.number || workflow_run.pull_requests[0].number), so skipped runs couldn't cancel real ones. Tracked in #2420: bind first, then put job-level concurrency on the posting job, keyed by the bound PR number. squad-impact.yml and squad-repo-health-report.yml share the key pattern but aren't affected: each has one trigger and only dev as base.

@tamirdresher

Copy link
Copy Markdown
Collaborator Author

Post-merge authoritative canary for this change: passed (.github/REPO_HEALTH_RUNBOOK.md → "Authoritative post-merge canary").

  • Canary PR: test: repo-health post-merge canary #2422, a draft so no review was requested; closed unmerged and its branch deleted.
  • Hostile path: .squad/canary-`paired`-${-apostrophe-'-"double quote".md. The commit was built through the Git Data API.
Push Head Repo Health (pull_request) Repo Health Report (workflow_run) Verification
1. add file 1dfbdaf4 42513974060 ✅ 42513993979 ✅ One <!-- squad-repo-health-leakage --> comment (link). It lists only the exact path, carries Authoritative evidence, and names head 1dfbdaf4 ✅
2. delete file 76ed1450 42514130963 ✅ 42514152385 ✅ No marker comment remains ✅

Runbook gap: the procedure also triggers Squad Upgrade Matrix. Its windows-latest leg always fails on push 1 with error: invalid path (run), because Windows checkouts can't contain a " in a filename. The failure comes from the canary itself, not a regression. The runbook should warn about it so the next operator doesn't chase it. The no-op Squad CI and Upgrade Matrix runs on push 2, whose tree is identical to dev, were cancelled.

tamirdresher added a commit that referenced this pull request Nov 9, 2026
…#2436)

#2409 moved the impact, Post Repo Health findings and heartbeat writers
onto workflow_run jobs. Their check runs attach to the default-branch
tip, not the PR head, and squad-impact.yml / squad-repo-health-report.yml
cancel superseded runs, so a quick push on any PR leaves a `cancelled`
check on dev. checkCIStatus() treats cancelled/failure as failing CI, so
every dev-to-main PR reported "check(s) failing: impact" based on
unrelated PRs' runs. readiness-bind was already excluded for this reason
(#2423); this adds the remaining workflow_run writer names.

Co-authored-by: Copilot <Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

Repo move: PR workflows fail under the github org's pull_request_target block

3 participants