Skip to content
Open
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
104 changes: 104 additions & 0 deletions .github/workflows/unassign-unlinked-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: Unassign Issues Without Linked PR

on:
schedule:
- cron: '0 9 */5 * *' # Runs every 5 days at 9:00 AM UTC
workflow_dispatch: # Also allows manual triggering

jobs:
unassign-issues:
runs-on: ubuntu-latest
permissions:
issues: write

steps:
- name: Unassign issues with no linked PR and notify
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
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.

Hey @Harxhit , can we move this script to separate file? That's better practice.

const owner = context.repo.owner;
const repo = context.repo.repo;

// Fetch all open issues (excluding PRs)
let page = 1;
let issues = [];

while (true) {
const { data } = await github.rest.issues.listForRepo({
owner,
repo,
state: 'open',
per_page: 100,
page,
});

// Filter out pull requests
const onlyIssues = data.filter(i => !i.pull_request);
issues = issues.concat(onlyIssues);

if (data.length < 100) break;
page++;
}

console.log(`Found ${issues.length} open issue(s) to check.`);

for (const issue of issues) {
const issueNumber = issue.number;

// Skip issues with no assignees
if (!issue.assignees || issue.assignees.length === 0) {
console.log(`Issue #${issueNumber} has no assignees — skipping.`);
continue;
}

// Search for PRs that reference this issue
const query = `repo:${owner}/${repo} is:pr is:open linked:issue`;
let linkedPRFound = false;

try {
// Check timeline for cross-referenced PRs
const timeline = await github.rest.issues.listEventsForTimeline({
owner,
repo,
issue_number: issueNumber,
per_page: 100,
});

linkedPRFound = timeline.data.some(event =>
event.event === 'cross-referenced' &&
event.source?.issue?.pull_request &&
event.source?.issue?.state === 'open'
);
} catch (err) {
console.log(`Could not fetch timeline for issue #${issueNumber}: ${err.message}`);
}

if (!linkedPRFound) {
console.log(`Issue #${issueNumber} has no linked PR — unassigning and notifying.`);

// Get current assignees before removing
const assigneeLogins = issue.assignees.map(a => a.login);

// Unassign all current assignees
await github.rest.issues.removeAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: assigneeLogins,
});

// Post a comment tagging the specified users
const assigneesMention = assigneeLogins.map(u => `@${u}`).join(', ');
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: `Hey @ShantKhatri(Project Admin) and @Harxhit(Maintainer),\n\nThis issue (previously assigned to ${assigneesMention}) has been **automatically unassigned** because no linked pull request was found after the scheduled check.\n\nIf work is in progress, please open a PR and link it to this issue to keep the assignment.`,
});

console.log(`Unassigned and commented on issue #${issueNumber}.`);
} else {
console.log(`Issue #${issueNumber} has a linked PR — leaving as-is.`);
}
}