Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions .github/scripts/issue-auto-assign.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,30 @@ test('summary failure fails automation; untrusted text is never rendered', async
await f.run(); assert.equal(f.failures.length, 1);
});

test('old-base checkout loads the trusted workflow helper without selecting PR head', () => {
const yaml = readFileSync(new URL('../workflows/issue-auto-assign.yml', import.meta.url), 'utf8').replace(/\r\n/g, '\n');
const workflowSha = 'a'.repeat(40);
const oldBaseSha = 'b'.repeat(40);
const untrustedHeadSha = 'c'.repeat(40);
const contextRefs = {
'github.workflow_sha': workflowSha,
'github.event.pull_request.base.sha': oldBaseSha,
'github.event.pull_request.head.sha': untrustedHeadSha,
};
const helper = '.github/scripts/issue-auto-assign.mjs';
const filesByRevision = new Map([
[workflowSha, new Set([helper])],
[oldBaseSha, new Set()],
[untrustedHeadSha, new Set([helper])],
]);
const expression = yaml.match(/^ ref: \$\{\{ ([\w.]+) \}\}$/m)?.[1];
assert.ok(Object.hasOwn(contextRefs, expression), 'checkout expression must resolve in the event fixture');
const checkoutSha = contextRefs[expression];
assert.ok(filesByRevision.get(checkoutSha)?.has(helper), 'selected revision must supply the helper even when the historical base lacks it');
assert.notEqual(checkoutSha, untrustedHeadSha, 'PR head helper must never be selected');
assert.equal(checkoutSha, workflowSha, 'helper must come from the trusted executing pull_request_target workflow');
});

test('workflow trust, permissions, serialization and executable github-script boundary', async () => {
const yaml = readFileSync(new URL('../workflows/issue-auto-assign.yml', import.meta.url), 'utf8').replace(/\r\n/g, '\n');
assert.match(yaml, /pull_request_target:\s*\n\s*types: \[opened, edited, reopened, ready_for_review\]/);
Expand All @@ -121,10 +145,10 @@ test('workflow trust, permissions, serialization and executable github-script bo
[['contents', 'read'], ['issues', 'write'], ['pull-requests', 'read']]);
assert.match(yaml, /actions\/checkout@d23441a48e516b6c34aea4fa41551a30e30af803/);
assert.match(yaml, /actions\/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd/);
assert.match(yaml, /ref: \$\{\{ github.event.pull_request.base.sha \}\}/);
assert.match(yaml, /ref: \$\{\{ github.workflow_sha \}\}/);
assert.match(yaml, /persist-credentials: false/);
assert.match(yaml, /sparse-checkout: \.github\/scripts\/issue-auto-assign.mjs/);
assert.doesNotMatch(yaml, /head\.sha|workflow_sha|secrets\.|run:|checks:|actions: write/);
assert.doesNotMatch(yaml, /github\.event\.pull_request\.base\.sha|pull_request\.head|head\.sha|secrets\.|run:|checks:|actions: write/);
const script = yaml.split(' script: |\n')[1].split('\n').map(line => line.replace(/^ /, '')).join('\n');
const execute = new (Object.getPrototypeOf(async function() {}).constructor)('github', 'context', 'core', 'load',
script.replace(/await import\([^\n]+\)/, 'await load()'));
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/issue-auto-assign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ jobs:
issues: write
pull-requests: read
steps:
- name: Load trusted base helper
- name: Load trusted workflow helper
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ github.event.pull_request.base.sha }}
ref: ${{ github.workflow_sha }}
persist-credentials: false
sparse-checkout: .github/scripts/issue-auto-assign.mjs
sparse-checkout-cone-mode: false
Expand Down
11 changes: 7 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,13 @@ unassignments, historical scans or required-check/settings changes.

This is a convenience, **not a replacement for approved, assigned ownership before
implementation**. Required validation and refresh publishers remain independent;
rerun native PR validation after assignment changes. The workflow uses only a
trusted event base-SHA helper, never PR-head code or candidate bootstrap. It becomes
available only after integration on the trusted base. A real fork run remains
pending until then; mocked tests do not establish live token/event behavior.
rerun native PR validation after assignment changes. In this trusted
`pull_request_target` workflow, `github.workflow_sha` supplies the helper from the
executing workflow revision even when the historical PR base lacks it, never
PR-head code or candidate bootstrap. `workflow_sha` is not a universal trust
marker. This correction takes effect only after integration on the trusted
branch. Local tests do not establish hosted old-base, token or event behavior;
a real fork run remains pending.

Runs are serialized repository-wide with cancellation disabled for running jobs.
GitHub may replace/coalesce pending runs: this is best-effort scheduling, not a
Expand Down
Loading