Skip to content

Report worktree sidebar status in the CLI #234

Report worktree sidebar status in the CLI

Report worktree sidebar status in the CLI #234

Workflow file for this run

name: Pull request policy
# Runs against fork pull requests with a write-scoped token via `pull_request_target`.
# It NEVER checks out or executes pull-request code: it is pure GitHub API only, and all
# contributor-controlled data (body, commit messages) is read as data, never interpolated
# into a shell or script via `${{ }}`.
#
# On a pull request event the job is a status check: green when the pull request meets the
# contribution policy, red when it does not. A non-compliant pull request from a non-maintainer
# is labeled `invalid`, with one comment listing every reason. When it later becomes compliant
# the label is cleared. An approving review from a maintainer (write/admin) waives every check,
# so the job re-runs on review submit/dismiss to flip the status as the maintainer's stance changes.
#
# On a daily schedule the job sweeps open `invalid` pull requests: it re-evaluates each one, so a
# pull request made compliant by an issue-side change (a maintainer adding `ready`, or reassigning
# the issue) is cleared rather than closed, and only genuinely still-invalid, inactive ones are
# closed. The verdict is published as the `pull-request-policy` commit status on the head SHA, not
# the implicit job check, so a `pull_request_review` re-run can flip it (review-triggered runs do
# not otherwise attach to the pull request head commit). Require that status in branch protection.
on:
pull_request_target:
types: [opened, edited, reopened, synchronize, ready_for_review]
pull_request_review:
types: [submitted, dismissed]
schedule:
- cron: "17 6 * * *"
workflow_dispatch:
concurrency:
group: pr-policy-${{ github.event.pull_request.number || 'sweep' }}
cancel-in-progress: true
permissions:
contents: read
issues: read
pull-requests: write
statuses: write
jobs:
policy:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const marker = '<!-- supacode-pr-policy -->';
const staleMarker = '<!-- supacode-pr-stale -->';
const invalidLabel = 'invalid';
const statusContext = 'pull-request-policy';
const staleDays = 3;
const { owner, repo } = context.repo;
// `Co-authored-by:` / `Authored-by:` trailers are matched broadly, since naming a tool
// there is explicit. The git author name is matched narrowly, because agent brands like
// Claude and Gemini are also common human first names: only tool-specific forms count.
const trailerAgent = /\b(claude|anthropic|copilot|chatgpt|openai|gpt-[0-9]|cursor|codex|devin[ -]?ai|aider|windsurf|sourcegraph|google-labs-jules|gemini)\b/i;
const authorNameAgent = /\b(anthropic|copilot|chatgpt|openai|gpt-[0-9]|cursor|codex|devin[ -]?ai|aider|windsurf|sourcegraph|google-labs-jules|claude[ -]?code|gemini[ -]?(?:cli|code))\b/i;
const trailerLine = /^[ \t]*(?:co-authored-by|authored-by):[ \t]*(.+)$/gim;
const hasLabel = (pr, name) => (pr.labels || []).some((l) => l.name.toLowerCase() === name);
async function getComments(number) {
return github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: number, per_page: 100 });
}
// Returns { skip, reasons }. `skip` is true for bots and write-access contributors.
// Throws on an indeterminate permission error so the run fails visibly rather than
// passing or acting on the pull request while unsure of the author's access.
async function evaluate(pr) {
const author = pr.user.login;
// Only allowlisted maintenance bots bypass. Any other bot, including an AI coding bot,
// is enforced like a human so it cannot skip the linked-issue and human-author checks.
const allowlistedBots = ['dependabot[bot]', 'renovate[bot]', 'github-actions[bot]'];
if (allowlistedBots.includes(author)) return { skip: true, reasons: [] };
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: author });
if (data.permission === 'admin' || data.permission === 'write') return { skip: true, reasons: [] };
} catch (error) {
if (error.status !== 404) throw error; // 404 means "not a collaborator": enforce.
}
const reasons = [];
// One round-trip fetches both the closing-issue references and the latest opinionated
// review per write-access user (`writersOnly`), so the maintainer-approval waiver below
// costs no extra API calls.
const gql = await github.graphql(
`query($owner:String!, $repo:String!, $number:Int!) {
repository(owner:$owner, name:$repo) {
pullRequest(number:$number) {
closingIssuesReferences(first: 20) { nodes { number repository { owner { login } name } } }
latestOpinionatedReviews(first: 20, writersOnly: true) { nodes { state } }
}
}
}`,
{ owner, repo, number: pr.number },
);
const pull = gql.repository.pullRequest;
// A maintainer greenlight overrides the policy: once a write/admin collaborator approves,
// they have explicitly vetted the pull request, so every other check is waived.
// `latestOpinionatedReviews` is each reviewer's current stance, so a later rejection or
// dismissal drops the approval and revokes the waiver.
if (pull.latestOpinionatedReviews.nodes.some((r) => r.state === 'APPROVED')) {
return { skip: true, reasons: [] };
}
// Linked issues. GitHub's closing references are the authority (they ignore keywords
// inside HTML comments and code). The body regex is a fallback only when the reference
// index is empty, and it strips comments, fenced and inline code, and quotes first so a
// pasted `git commit -m "fixes #9"` is not mistaken for a link.
const isSameRepo = (node) =>
node.repository.owner.login.toLowerCase() === owner.toLowerCase() &&
node.repository.name.toLowerCase() === repo.toLowerCase();
const candidates = new Set(
pull.closingIssuesReferences.nodes.filter(isSameRepo).map((n) => n.number),
);
if (candidates.size === 0) {
const text = (pr.body || '')
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/```[\s\S]*?```/g, '')
.replace(/~~~[\s\S]*?~~~/g, '')
.replace(/`[^`]*`/g, '')
.replace(/^\s*>.*$/gm, '');
const re = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d{1,7})\b/gi;
let m;
while ((m = re.exec(text)) !== null) candidates.add(Number(m[1]));
}
let anyValidIssue = false;
for (const number of candidates) {
let issue;
try {
({ data: issue } = await github.rest.issues.get({ owner, repo, issue_number: number }));
} catch (error) {
if (error.status !== 404) throw error; // Transient error: fail, never mis-triage.
continue; // Unknown issue number, ignore.
}
if (issue.pull_request || issue.state !== 'open') continue; // Not a real, open issue.
anyValidIssue = true;
const isReady = issue.labels.some((l) => (typeof l === 'string' ? l : l.name).toLowerCase() === 'ready');
if (!isReady) reasons.push(`Linked issue #${number} is not marked \`ready\` yet. A maintainer adds \`ready\` once the issue is triaged and approved for a pull request.`);
const assignees = issue.assignees || [];
if (assignees.length > 0 && !assignees.some((a) => a.id === pr.user.id)) {
const names = [...new Set(assignees.map((a) => '@' + a.login))].join(', ');
reasons.push(`Linked issue #${number} is assigned to ${names}, so it is reserved for them. Ask a maintainer to reassign it if they have stopped.`);
}
}
if (!anyValidIssue) {
reasons.push('No linked issue. Add a line like `Closes #123` to the description, pointing at an open issue in this repository.');
}
// AI authorship. A human must be the author of record. Match only the display-name part
// of a trailer (before the email), so a human co-author at `<x@openai.com>` is not flagged.
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner, repo, pull_number: pr.number, per_page: 100,
});
if (commits.length >= 250) {
core.warning(`#${pr.number}: commit list may be truncated at ${commits.length}; AI-authorship check could miss later commits.`);
}
const aiAuthored = commits.some((c) => {
const message = c.commit.message || '';
for (const match of message.matchAll(trailerLine)) {
if (trailerAgent.test(match[1].split('<')[0])) return true;
}
return authorNameAgent.test((c.commit.author && c.commit.author.name) || '');
});
if (aiAuthored) {
reasons.push('A commit is authored or co-authored by an AI agent. A human must be the author of record: re-author the commits under your own name (reset the author, or drop the `Co-authored-by:` trailer). Using AI tools is welcome, and you are encouraged to disclose them in the description.');
}
return { skip: false, reasons };
}
// Label and comment a non-compliant pull request.
async function applyInvalid(pr, reasons, existing) {
if (!hasLabel(pr, invalidLabel)) {
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [invalidLabel] });
}
const list = reasons.map((reason) => `- ${reason}`).join('\n');
const body =
`${marker}\n\n` +
"Thanks for the pull request. It doesn't meet the contribution policy yet, so I've labeled it " +
'`invalid`. Please address the following, then push an update:\n\n' +
`${list}\n\n` +
'Nothing needs to be redone. A pull request left `invalid` is closed automatically after a few ' +
'days of inactivity. See [CONTRIBUTING.md](https://github.com/supabitapp/supacode/blob/main/CONTRIBUTING.md) for the full flow.';
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
}
}
// Clear our own intervention once a pull request is compliant. Remove the label last so a
// transient failure earlier leaves the marker in place to re-run on the next event.
async function clearIntervention(pr, existing) {
await github.rest.issues.updateComment({
owner, repo, comment_id: existing.id,
body: `${marker}\n\nThanks, this now meets the contribution policy. I've cleared the \`invalid\` label.`,
});
if (hasLabel(pr, invalidLabel)) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: invalidLabel }).catch((error) => {
if (error.status !== 404) throw error;
});
}
}
// Publish the verdict on the head commit so it lands on the SHA branch protection reads,
// regardless of which event triggered the run.
async function setStatus(pr, state, description) {
await github.rest.repos.createCommitStatus({
owner, repo, sha: pr.head.sha, state, context: statusContext, description: description.slice(0, 140),
});
}
if (context.eventName === 'schedule' || context.eventName === 'workflow_dispatch') {
// Daily sweep: re-evaluate every open `invalid` pull request.
const cutoff = Date.now() - staleDays * 24 * 60 * 60 * 1000;
const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100 });
for (const pr of prs) {
if (!hasLabel(pr, invalidLabel)) continue;
try {
const { skip, reasons } = await evaluate(pr);
const comments = await getComments(pr.number);
const existing = comments.find((c) => c.body && c.body.includes(marker));
// Now bypassed, approved, or compliant: clear the label and go green.
if (skip || reasons.length === 0) {
if (existing) await clearIntervention(pr, existing);
await setStatus(pr, 'success', 'Meets the contribution policy.');
continue;
}
// Still non-compliant: close only if inactive past the cutoff. Do not re-comment on
// the not-yet-stale ones, so the sweep never bumps `updated_at` and resets the
// staleness clock. Close before commenting so a failed close is retried next run
// rather than leaving a "closing" comment on a still-open pull request.
if (new Date(pr.updated_at).getTime() > cutoff) continue;
await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed' });
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
body:
`${staleMarker}\n\n` +
`Closing this because it has been labeled \`invalid\` and inactive for more than ${staleDays} days. ` +
'Nothing is lost: address the points in the policy comment above and reopen it, and the checks ' +
'will re-run. See [CONTRIBUTING.md](https://github.com/supabitapp/supacode/blob/main/CONTRIBUTING.md).',
});
core.info(`Closed stale invalid pull request #${pr.number}.`);
} catch (error) {
core.warning(`Sweep skipped #${pr.number}: ${error.status || error.message}`);
}
}
return;
}
// Pull request or review event: resolve the single `pull-request-policy` status check.
const pr = context.payload.pull_request;
const { skip, reasons } = await evaluate(pr);
const comments = await getComments(pr.number);
const existing = comments.find((c) => c.body && c.body.includes(marker));
// A bypass, a maintainer approval, or a fully compliant pull request all pass: clear any
// prior intervention and go green.
if (skip || reasons.length === 0) {
if (existing) await clearIntervention(pr, existing);
await setStatus(pr, 'success', 'Meets the contribution policy.');
core.info(skip ? `Passed: ${pr.user.login} is a bot, maintainer, or has an approval.` : 'Compliant: all policy checks pass.');
return;
}
await applyInvalid(pr, reasons, existing);
await setStatus(pr, 'failure', reasons[0]);
core.notice(`#${pr.number} is invalid: ${reasons[0]}`);