Skip to content

Commit 03c4e38

Browse files
committed
Merge branch 'main' of https://github.com/O2sa/DevImpact into feat/localization
2 parents 04522a2 + 74c323e commit 03c4e38

18 files changed

Lines changed: 1178 additions & 1027 deletions
Lines changed: 38 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
name: auto_assign_issue
1+
name: Auto Assign Issue
22

33
on:
44
issue_comment:
55
types: [created]
66

77
permissions:
88
issues: write
9-
pull-requests: read
9+
pull-requests: write
1010
contents: read
1111

1212
jobs:
1313
assign:
14-
if: github.event.issue.pull_request == null
14+
# Ensure this only runs on actual Issues, not Pull Request comments
15+
if: ${{ !github.event.issue.pull_request }}
1516
runs-on: ubuntu-latest
1617

1718
steps:
@@ -23,196 +24,55 @@ jobs:
2324
const repo = context.repo.repo;
2425
const issue_number = context.payload.issue.number;
2526
const assignee = context.payload.comment.user.login;
26-
2727
const comment_id = context.payload.comment.id;
28-
2928
const body = (context.payload.comment.body || '').toLowerCase();
3029
31-
// keyword check for auto-assigning
32-
const hasAssignKeyword =
33-
body.includes('assign me') ||
34-
body.includes('assign it to me') ||
35-
body.includes('assign this to me') ||
36-
body.includes('assign this issue to me') ||
37-
body.includes('assign the issue to me');
30+
const keywords = ['assign me', 'assign it to me', 'assign this to me', 'assign this issue to me'];
31+
const hasAssignKeyword = keywords.some(k => body.includes(k));
3832
39-
if (!hasAssignKeyword) {
40-
core.info('No assign keyword found.');
41-
return;
42-
}
33+
if (!hasAssignKeyword) return;
4334
44-
// Helper: react to the triggering comment
45-
async function react(content) {
46-
try {
47-
await github.rest.reactions.createForIssueComment({
48-
owner,
49-
repo,
50-
comment_id,
51-
content, // "+1", "confused", "rocket", etc.
52-
});
53-
} catch (e) {
54-
// If reactions aren't permitted for some reason, don't fail the whole workflow.
55-
core.info(`Could not add reaction (${content}): ${e.message}`);
56-
}
57-
}
58-
59-
// 1) Don't reassign if already assigned
60-
const current = context.payload.issue.assignees?.map(a => a.login) || [];
61-
if (current.length > 0) {
62-
core.info(`Already assigned to: ${current.join(', ')}`);
63-
await react('confused');
35+
// 1. Check if already assigned
36+
const currentAssignees = context.payload.issue.assignees || [];
37+
if (currentAssignees.length > 0) {
38+
core.info('Already assigned.');
6439
return;
6540
}
6641
67-
// Helper: format issue as a single markdown link line (prevents duplicate link/title rendering)
68-
function formatIssueLine(i) {
69-
return `- [#${i.number} — ${i.title}](${i.url})`;
70-
}
71-
72-
// Helper: suggest other unblocked issues
73-
async function getSuggestions(limit = 5) {
74-
// open issues, unassigned, not blocked
75-
const q = `repo:${owner}/${repo} is:issue is:open no:assignee -label:blocked`;
76-
const res = await github.rest.search.issuesAndPullRequests({ q, per_page: limit });
77-
return (res.data.items || []).map(i => ({
78-
number: i.number,
79-
title: i.title,
80-
url: i.html_url,
81-
}));
82-
}
83-
84-
// 2) Block if label "blocked" exists (and suggest alternatives)
85-
const labels = (context.payload.issue.labels || [])
86-
.map(l => (typeof l === 'string' ? l : l.name))
87-
.filter(Boolean)
88-
.map(l => l.toLowerCase());
89-
90-
if (labels.includes('blocked')) {
91-
const suggestions = await getSuggestions(5);
92-
const suggestionText = suggestions.length
93-
? suggestions.map(formatIssueLine).join('\n')
94-
: '_No unblocked, unassigned issues found right now._';
95-
96-
await github.rest.issues.createComment({
97-
owner, repo, issue_number,
98-
body: [
99-
`⛔ Sorry @${assignee} — this issue is currently **blocked** and can’t be claimed yet.`,
100-
``,
101-
`Here are some other **unblocked** issues you can pick up instead:`,
102-
suggestionText,
103-
].join('\n'),
42+
try {
43+
// 2. Attempt Assignment
44+
await github.rest.issues.addAssignees({
45+
owner,
46+
repo,
47+
issue_number,
48+
assignees: [assignee],
10449
});
10550
106-
await react('confused');
107-
return;
108-
}
109-
110-
// 3) Capacity check:
111-
// If the candidate has 2 assigned issues that do NOT have PRs, then do not assign a new issue.
112-
//
113-
// "Linked" here means: a PR cross-references the issue (GitHub timeline cross-reference),
114-
// and the PR is authored by the candidate.
115-
async function hasLinkedPRForIssue(issueNum) {
116-
const query = `
117-
query($owner: String!, $repo: String!, $number: Int!) {
118-
repository(owner: $owner, name: $repo) {
119-
issueOrPullRequest(number: $number) {
120-
__typename
121-
... on Issue {
122-
timelineItems(last: 50, itemTypes: [CROSS_REFERENCED_EVENT]) {
123-
nodes {
124-
__typename
125-
... on CrossReferencedEvent {
126-
source {
127-
__typename
128-
... on PullRequest {
129-
number
130-
url
131-
state
132-
isDraft
133-
author { login }
134-
}
135-
}
136-
}
137-
}
138-
}
139-
}
140-
}
141-
}
142-
}
143-
`;
144-
145-
const data = await github.graphql(query, { owner, repo, number: issueNum });
146-
147-
const iop = data?.repository?.issueOrPullRequest;
148-
if (!iop || iop.__typename !== 'Issue') return false;
149-
150-
const nodes = iop.timelineItems?.nodes || [];
151-
for (const n of nodes) {
152-
if (n?.__typename !== 'CrossReferencedEvent') continue;
153-
const pr = n?.source;
154-
if (pr?.__typename !== 'PullRequest') continue;
51+
// 3. Verify if assignment actually stuck
52+
const { data: updatedIssue } = await github.rest.issues.get({
53+
owner, repo, issue_number
54+
});
55+
56+
const isAssigned = updatedIssue.assignees.some(a => a.login === assignee);
15557
156-
if ((pr?.author?.login || '').toLowerCase() === assignee.toLowerCase()) {
157-
// Counts any PR state (OPEN/MERGED/CLOSED) as "has a PR for it".
158-
return true;
159-
}
58+
if (!isAssigned) {
59+
await github.rest.issues.createComment({
60+
owner, repo, issue_number,
61+
body: `⚠️ @${assignee}, I couldn't assign you. You must be a collaborator or a member of this organization to be assigned to issues.`
62+
});
63+
return;
16064
}
161-
return false;
162-
}
163-
164-
async function getOpenAssignedIssuesForUser() {
165-
// Find open issues assigned to this user in this repo
166-
const q = `repo:${owner}/${repo} is:issue is:open assignee:${assignee}`;
167-
const res = await github.rest.search.issuesAndPullRequests({ q, per_page: 20 });
168-
return (res.data.items || []).map(i => ({
169-
number: i.number,
170-
title: i.title,
171-
url: i.html_url,
172-
}));
173-
}
174-
175-
const assignedIssues = await getOpenAssignedIssuesForUser();
176-
177-
// Count assigned issues that do NOT have PRs by this assignee
178-
const issuesWithoutPR = [];
179-
for (const it of assignedIssues) {
180-
const hasPR = await hasLinkedPRForIssue(it.number);
181-
if (!hasPR) issuesWithoutPR.push(it);
182-
if (issuesWithoutPR.length >= 2) break; // early exit
183-
}
184-
185-
if (issuesWithoutPR.length >= 2) {
186-
const noPRText = issuesWithoutPR
187-
.slice(0, 10)
188-
.map(formatIssueLine)
189-
.join('\n');
19065
66+
// 4. Success Actions
19167
await github.rest.issues.createComment({
19268
owner, repo, issue_number,
193-
body: [
194-
`⛔ Sorry @${assignee} — you already have **2 or more** assigned issues with **no pull request(s)**.`,
195-
``,
196-
`Please open a PR for one of your assigned issues before claiming a new one:`,
197-
noPRText || '_Could not list the issues (unexpected)._',
198-
``,
199-
`Tip: opening a **draft PR** is fine—once a PR is linked, you'll be able to claim another issue.`,
200-
].join('\n'),
69+
body: `🎉 Issue assigned to @${assignee}! Happy coding!`
70+
});
71+
72+
await github.rest.reactions.createForIssueComment({
73+
owner, repo, comment_id, content: 'rocket'
20174
});
20275
203-
await react('confused');
204-
return;
76+
} catch (error) {
77+
core.setFailed(`Workflow failed: ${error.message}`);
20578
}
206-
207-
// 4) Assign
208-
await github.rest.issues.addAssignees({
209-
owner, repo, issue_number,
210-
assignees: [assignee],
211-
});
212-
213-
await github.rest.issues.createComment({
214-
owner, repo, issue_number,
215-
body: `🎉 Thank you for your interest in contributing!\n\nThe issue has been assigned to @${assignee}. Happy coding!`,
216-
});
217-
218-
await react('rocket');

0 commit comments

Comments
 (0)