Skip to content

fix(csrf): reject null Origin header on state-changing API requests #3673

fix(csrf): reject null Origin header on state-changing API requests

fix(csrf): reject null Origin header on state-changing API requests #3673

name: Issue Commands
# Single source of truth for all issue comment commands:
# /assign → contributor requests assignment
# /assign @user → maintainer assigns a user
# /duplicate #N → maintainer marks duplicate
# /already-implemented → maintainer marks already implemented
# Also handles label triggers (duplicate, already-implemented) and cleanup.
on:
issue_comment:
types: [created]
issues:
types: [assigned, labeled]
permissions: {}
jobs:
# ── Contributor requests assignment (/assign without @user) ──────────────────
contributor-request:
runs-on: ubuntu-latest
permissions:
issues: write
if: |
github.event_name == 'issue_comment' &&
!github.event.issue.pull_request &&
(contains(github.event.comment.body, 'assign') || contains(github.event.comment.body, 'Assign') || contains(github.event.comment.body, '/assign')) &&
!contains(github.event.comment.body, '/assign @') &&
!contains(github.event.comment.body, '/duplicate') &&
!contains(github.event.comment.body, '/already-implemented')
steps:
- name: Acknowledge assignment request (idempotent)
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const commenter = context.payload.comment.user.login;
const issueNumber = context.issue.number;
// Skip if commenter is a maintainer
try {
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commenter,
});
if (['admin', 'write'].includes(perm.permission)) {
console.log(`${commenter} is a maintainer — skipping contributor flow.`);
return;
}
} catch (e) {
console.log('Could not fetch permission level:', e.message);
}
// Check if already assigned to someone
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
if (issue.assignees && issue.assignees.length > 0) {
const assigneeNames = issue.assignees.map(a => `@${a.login}`).join(', ');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `Hi @${commenter}, this issue is already assigned to ${assigneeNames}. Please look for other open issues to work on! 🙏`,
});
return;
}
// Check idempotency: don't post duplicate ack if we already replied to this user
const marker = `<!-- assign-ack-${commenter} -->`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
if (comments.some(c => c.body.includes(marker))) {
console.log(`Already acknowledged assignment request from ${commenter}.`);
return;
}
// Add needs-maintainer-approval label
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['needs-maintainer-approval'],
});
} catch (e) {
console.log('Could not add label:', e.message);
}
// Post acknowledgement
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `${marker}\nHello @${commenter}! 👋 Thanks for your interest in working on this issue.\n\nWe have automatically notified the maintainers for you. They will review your request and officially assign the issue to you shortly if it's available.\n\n**Note:** Please do not start working or raise a pull request until you are officially assigned!`,
});
# ── Maintainer assigns user (/assign @username) ─────────────────────────────
maintainer-assign:
runs-on: ubuntu-latest
permissions:
issues: write
if: |
github.event_name == 'issue_comment' &&
!github.event.issue.pull_request &&
contains(github.event.comment.body, '/assign @')
steps:
- name: Assign user via maintainer command
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const commenter = context.payload.comment.user.login;
// Verify maintainer
try {
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commenter,
});
if (!['admin', 'write'].includes(perm.permission)) {
console.log(`${commenter} is not a maintainer.`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `@${commenter} Only maintainers can use the \`/assign @user\` command.`,
});
return;
}
} catch (e) {
return;
}
const match = context.payload.comment.body.match(/\/assign\s+@([a-zA-Z0-9_-]+)/);
if (!match) return;
const assignee = match[1];
try {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
assignees: [assignee],
});
} catch (e) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `❌ Could not assign @${assignee}: ${e.message}`,
});
return;
}
// Remove approval label
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'needs-maintainer-approval',
});
} catch (e) { /* label may not be present */ }
// Removed success comment here as cleanup-on-assign will now handle notifying the user.
# ── Maintainer closes issue as duplicate or already-implemented ────────────────────────
maintainer-close-issue:
runs-on: ubuntu-latest
permissions:
issues: write
if: |
(github.event_name == 'issue_comment' && !github.event.issue.pull_request &&
(contains(github.event.comment.body, '/duplicate') || contains(github.event.comment.body, '/already-implemented'))) ||
(github.event_name == 'issues' && github.event.action == 'labeled' &&
(github.event.label.name == 'duplicate' || github.event.label.name == 'already-implemented'))
steps:
- name: Process duplicate or already-implemented close
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const commenter = context.payload.sender.login;
// Verify maintainer for comment events
if (context.eventName === 'issue_comment') {
try {
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: commenter,
});
if (!['admin', 'write'].includes(perm.permission)) {
console.log(`${commenter} is not a maintainer.`);
return;
}
} catch (e) {
return;
}
}
let targetLabel = null;
let closeReason = 'not_planned';
let commentBody = '';
if (context.eventName === 'issue_comment') {
const body = context.payload.comment.body;
if (body.includes('/duplicate')) {
const match = body.match(/\/duplicate\s+#(\d+)/);
const dupNumber = match ? match[1] : '';
targetLabel = 'duplicate';
commentBody = dupNumber
? `This issue has been marked as a **duplicate** of #${dupNumber}. Closing to keep our tracker organized. Please search existing issues before opening new ones!`
: `This issue has been marked as a **duplicate**. Closing to keep our tracker organized. Please search existing issues before opening new ones!`;
} else if (body.includes('/already-implemented')) {
targetLabel = 'already-implemented';
commentBody = `This issue has been closed because the requested features or bug fixes are already implemented in the repository.`;
}
} else if (context.eventName === 'issues' && context.payload.action === 'labeled') {
const labelName = context.payload.label.name;
if (labelName === 'duplicate') {
targetLabel = 'duplicate';
commentBody = `This issue has been marked as a **duplicate**. Closing to keep our tracker organized. Please search existing issues before opening new ones!`;
} else if (labelName === 'already-implemented') {
targetLabel = 'already-implemented';
commentBody = `This issue has been closed because the requested features or bug fixes are already implemented in the repository.`;
}
}
if (targetLabel) {
// 1. Ensure target label exists
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: targetLabel,
color: targetLabel === 'duplicate' ? 'cfd3d7' : '0e8a16',
description: targetLabel === 'duplicate' ? 'This issue is a duplicate' : 'This issue is already implemented',
});
} catch (e) {
// Ignore if it already exists
}
// 2. Set labels (clears other labels)
await github.rest.issues.setLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: [targetLabel],
});
// 3. Post comment if new
if (commentBody) {
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const alreadyCommented = comments.some(c => c.body.includes(commentBody.substring(0, 30)));
if (!alreadyCommented) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody,
});
}
}
// 4. Close issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed',
state_reason: closeReason,
});
}
# ── Issue assigned → remove approval label ──────────────────────────────────
cleanup-on-assign:
runs-on: ubuntu-latest
permissions:
issues: write
if: github.event_name == 'issues' && github.event.action == 'assigned'
steps:
- name: Remove needs-maintainer-approval label
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'needs-maintainer-approval',
});
console.log('Removed needs-maintainer-approval label.');
} catch (e) {
console.log('Label already removed or not present.');
}
// Post assignment notification
if (context.payload.assignee) {
const assignee = context.payload.assignee.login;
const marker = `<!-- assignment-notification-${assignee} -->`;
try {
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
if (!comments.some(c => c.body && c.body.includes(marker))) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `${marker}\n✅ Hi @${assignee}, you have been assigned to this issue! You have 7 days to work on this issue and create a PR. Happy coding! 🚀`,
});
console.log(`Posted assignment notification for @${assignee}`);
}
} catch (e) {
console.log(`Failed to post notification for @${assignee}: ${e.message}`);
}
}