Files
calendar/.github/workflows/run-ci.yml
T
Keith WilliamsGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
3d73238c00 fix: remove installation requirement from trusted bot check (#26466)
* fix: remove installation requirement from trusted bot check

The installation object is not present in the webhook payload when
GitHub Apps add labels via pull_request_target events. This caused
graphite-app[bot] to fail the authorization check and fall through
to the human permission check, which doesn't work for bots.

The fix removes the installation requirement and relies on:
- sender.type === 'Bot'
- sender.login matching the trusted bot list

This is secure because the sender fields come from GitHub's webhook
payload and cannot be forged by contributors.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* chore: add extra logging about sender type

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* chore: remove senderId from logging

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Update run-ci.yml

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-05 15:19:39 +00:00

126 lines
4.4 KiB
YAML

name: Run CI
on:
pull_request_target:
types: [labeled]
permissions:
actions: write
contents: read
jobs:
trigger:
name: Trigger CI
if: github.event.label.name == 'run-ci' || github.event.label.name == 'ready-for-e2e'
runs-on: ubuntu-latest
steps:
- name: Verify and trigger CI
uses: actions/github-script@v7
with:
script: |
const adder = context.payload.sender.login;
const senderType = context.payload.sender.type;
const pr = context.payload.pull_request;
console.log(`Sender: ${adder} (type: ${senderType})`);
const trustedBotLogins = ['graphite-app[bot]'];
let isAuthorized = false;
if (senderType === 'Bot' && trustedBotLogins.includes(adder)) {
console.log(`Authorized: trusted GitHub App`);
isAuthorized = true;
}
if (!isAuthorized) {
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: adder,
});
if (!['admin', 'maintain', 'write'].includes(perm.permission)) {
core.setFailed(`${adder} does not have write access`);
return;
}
console.log(`Label added by ${adder}`);
}
// Find the latest pr.yml run for this PR's head SHA
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'pr.yml',
head_sha: pr.head.sha,
per_page: 5,
});
// Filter runs to this PR (in case same SHA exists in multiple PRs)
const matchingRuns = runs.workflow_runs.filter(run =>
!run.pull_requests?.length || run.pull_requests.some(p => p.number === pr.number)
);
if (matchingRuns.length === 0) {
core.setFailed(`No PR workflow found for SHA ${pr.head.sha}`);
return;
}
const latestRun = matchingRuns[0];
// If workflow is still running, cancel it first then re-run
if (latestRun.status === 'in_progress' || latestRun.status === 'queued') {
console.log(`Workflow is running (status: ${latestRun.status}). Cancelling it first...`);
await github.rest.actions.cancelWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: latestRun.id,
});
// Wait for the workflow to be cancelled (poll with timeout)
const maxWaitTime = 60000; // 60 seconds
const pollInterval = 2000; // 2 seconds
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const { data: updatedRun } = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: latestRun.id,
});
if (updatedRun.status === 'completed') {
console.log(`Workflow cancelled successfully (conclusion: ${updatedRun.conclusion})`);
break;
}
console.log(`Waiting for workflow to cancel... (status: ${updatedRun.status})`);
}
// Check final status
const { data: finalRun } = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: latestRun.id,
});
if (finalRun.status !== 'completed') {
core.setFailed(`Timed out waiting for workflow to cancel (status: ${finalRun.status})`);
return;
}
}
console.log(`Re-running workflow ${latestRun.id} (was: ${latestRun.conclusion || latestRun.status})`);
// Re-run preserves original context (PR, SHA, etc.)
await github.rest.actions.reRunWorkflow({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: latestRun.id,
});
console.log(`Triggered: ${latestRun.html_url}`);