Skip to content

Commit bb69215

Browse files
author
Himanshu Singhal
committed
AIRA-64: Project Automation Action Workflo Update
1 parent 226a8cd commit bb69215

File tree

1 file changed

+311
-33
lines changed

1 file changed

+311
-33
lines changed

.github/workflows/project-automation.yml

Lines changed: 311 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,339 @@ name: Project Board Automation
22

33
on:
44
issues:
5-
types: [opened, closed, assigned]
5+
types: [opened, closed, assigned, labeled]
66
pull_request:
7-
types: [opened, closed, ready_for_review]
7+
types: [opened, closed, ready_for_review, converted_to_draft]
8+
9+
env:
10+
PROJECT_ID: PVT_kwHOAGEyUM4A_SBA # GitHub Project V2 ID
811

912
jobs:
1013
update-project:
1114
runs-on: ubuntu-latest
1215
steps:
1316
- name: Auto-move items
14-
uses: actions/github-script@v6
17+
uses: actions/github-script@v7
1518
with:
1619
github-token: ${{ secrets.GITHUB_TOKEN }}
1720
script: |
18-
// This is a simplified version - you'll need to adapt based on your project board setup
19-
console.log('Event:', context.eventName);
20-
console.log('Action:', context.payload.action);
21+
const PROJECT_ID = process.env.PROJECT_ID;
22+
23+
// Helper function to get project info by ID
24+
async function getProjectInfo() {
25+
const query = `
26+
query($projectId: ID!) {
27+
node(id: $projectId) {
28+
... on ProjectV2 {
29+
id
30+
fields(first: 20) {
31+
nodes {
32+
... on ProjectV2Field {
33+
id
34+
name
35+
}
36+
... on ProjectV2SingleSelectField {
37+
id
38+
name
39+
options {
40+
id
41+
name
42+
}
43+
}
44+
}
45+
}
46+
}
47+
}
48+
}
49+
`;
50+
51+
try {
52+
const result = await github.graphql(query, {
53+
projectId: PROJECT_ID
54+
});
55+
return result.node;
56+
} catch (error) {
57+
console.log('Error fetching project info:', error);
58+
return null;
59+
}
60+
}
61+
62+
// Helper function to get item ID for issue/PR
63+
async function getProjectItemId(contentId) {
64+
const query = `
65+
query($projectId: ID!, $contentId: ID!) {
66+
node(id: $projectId) {
67+
... on ProjectV2 {
68+
items(first: 100) {
69+
nodes {
70+
id
71+
content {
72+
... on Issue {
73+
id
74+
}
75+
... on PullRequest {
76+
id
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
`;
85+
86+
try {
87+
const result = await github.graphql(query, {
88+
projectId: PROJECT_ID,
89+
contentId
90+
});
91+
92+
const item = result.node.items.nodes.find(
93+
item => item.content && item.content.id === contentId
94+
);
95+
return item ? item.id : null;
96+
} catch (error) {
97+
console.log('Error finding project item:', error);
98+
return null;
99+
}
100+
}
101+
102+
// Helper function to add item to project
103+
async function addToProject(contentId) {
104+
const projectInfo = await getProjectInfo();
105+
if (!projectInfo) return null;
106+
107+
const mutation = `
108+
mutation($projectId: ID!, $contentId: ID!) {
109+
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
110+
item {
111+
id
112+
}
113+
}
114+
}
115+
`;
116+
117+
try {
118+
const result = await github.graphql(mutation, {
119+
projectId: projectInfo.id,
120+
contentId
121+
});
122+
return result.addProjectV2ItemById.item.id;
123+
} catch (error) {
124+
console.log('Error adding to project:', error);
125+
return null;
126+
}
127+
}
128+
129+
// Helper function to update item status
130+
async function updateItemStatus(itemId, statusName) {
131+
const projectInfo = await getProjectInfo();
132+
if (!projectInfo) return;
133+
134+
const statusField = projectInfo.fields.nodes.find(
135+
field => field.name.toLowerCase() === 'status'
136+
);
137+
138+
if (!statusField || !statusField.options) {
139+
console.log('Status field not found');
140+
return;
141+
}
142+
143+
const statusOption = statusField.options.find(
144+
option => option.name.toLowerCase() === statusName.toLowerCase()
145+
);
146+
147+
if (!statusOption) {
148+
console.log(`Status option "${statusName}" not found`);
149+
return;
150+
}
151+
152+
const mutation = `
153+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: ProjectV2FieldValue!) {
154+
updateProjectV2ItemFieldValue(input: {
155+
projectId: $projectId,
156+
itemId: $itemId,
157+
fieldId: $fieldId,
158+
value: $value
159+
}) {
160+
projectV2Item {
161+
id
162+
}
163+
}
164+
}
165+
`;
166+
167+
try {
168+
await github.graphql(mutation, {
169+
projectId: projectInfo.id,
170+
itemId,
171+
fieldId: statusField.id,
172+
value: {
173+
singleSelectOptionId: statusOption.id
174+
}
175+
});
176+
console.log(`✅ Updated item status to: ${statusName}`);
177+
} catch (error) {
178+
console.log(`❌ Failed to update status to ${statusName}:`, error);
179+
}
180+
}
21181
182+
// Helper function to get current item status
183+
async function getCurrentItemStatus(itemId) {
184+
const projectInfo = await getProjectInfo();
185+
if (!projectInfo) return null;
186+
187+
const query = `
188+
query($itemId: ID!) {
189+
node(id: $itemId) {
190+
... on ProjectV2Item {
191+
fieldValues(first: 20) {
192+
nodes {
193+
... on ProjectV2ItemFieldSingleSelectValue {
194+
name
195+
field {
196+
... on ProjectV2SingleSelectField {
197+
name
198+
}
199+
}
200+
}
201+
}
202+
}
203+
}
204+
}
205+
}
206+
`;
207+
208+
try {
209+
const result = await github.graphql(query, { itemId });
210+
const statusField = result.node.fieldValues.nodes.find(
211+
field => field.field && field.field.name.toLowerCase() === 'status'
212+
);
213+
return statusField ? statusField.name : null;
214+
} catch (error) {
215+
console.log('Error getting current status:', error);
216+
return null;
217+
}
218+
}
219+
220+
// Main automation logic
22221
if (context.eventName === 'issues') {
23222
const action = context.payload.action;
24223
const issue = context.payload.issue;
224+
const issueId = issue.node_id;
225+
226+
console.log(`📝 Processing issue #${issue.number} - Action: ${action}`);
25227
26-
if (action === 'opened') {
27-
console.log(`📝 New issue #${issue.number} - would move to Todo`);
28-
} else if (action === 'assigned') {
29-
console.log(`👤 Issue #${issue.number} assigned - would move to In Progress`);
30-
} else if (action === 'closed') {
31-
console.log(`✅ Issue #${issue.number} closed - would move to Done`);
228+
// Ensure issue is added to project
229+
let itemId = await getProjectItemId(issueId);
230+
if (!itemId) {
231+
itemId = await addToProject(issueId);
232+
if (itemId) {
233+
console.log(`✅ Added issue #${issue.number} to project`);
234+
}
235+
}
236+
237+
if (itemId) {
238+
// Smart status transitions based on action
239+
if (action === 'opened') {
240+
// New issues go to backlog for triage
241+
await updateItemStatus(itemId, 'Backlog');
242+
} else if (action === 'assigned') {
243+
// Assignment moves to Ready (not automatically In Progress)
244+
// Team member decides when to start work
245+
await updateItemStatus(itemId, 'Ready');
246+
} else if (action === 'closed') {
247+
await updateItemStatus(itemId, 'Done');
248+
} else if (action === 'labeled') {
249+
// Label-based transitions for more granular control
250+
const label = context.payload.label;
251+
if (label.name === 'ready') {
252+
await updateItemStatus(itemId, 'Ready');
253+
} else if (label.name === 'in-progress') {
254+
await updateItemStatus(itemId, 'In Progress');
255+
} else if (label.name === 'blocked') {
256+
await updateItemStatus(itemId, 'Blocked');
257+
} else if (label.name === 'needs-review') {
258+
await updateItemStatus(itemId, 'In Review');
259+
} else if (label.name === 'priority-high') {
260+
// High priority items can jump to Ready if not already started
261+
const currentStatus = await getCurrentItemStatus(itemId);
262+
if (currentStatus === 'Backlog') {
263+
await updateItemStatus(itemId, 'Ready');
264+
}
265+
}
266+
}
32267
}
33268
}
34269
35270
if (context.eventName === 'pull_request') {
36271
const action = context.payload.action;
37272
const pr = context.payload.pull_request;
273+
const prId = pr.node_id;
38274
39-
if (action === 'opened' || action === 'ready_for_review') {
40-
console.log(`🔍 PR #${pr.number} ready for review - would move to In Review`);
41-
} else if (action === 'closed' && pr.merged) {
42-
console.log(`🎉 PR #${pr.number} merged - would move to Done`);
43-
44-
// Auto-close linked issues
45-
const body = pr.body || '';
46-
const issueNumbers = [...body.matchAll(/(?:closes|fixes|resolves)\s+#(\d+)/gi)]
47-
.map(match => parseInt(match[1]));
48-
49-
for (const issueNumber of issueNumbers) {
50-
try {
51-
await github.rest.issues.update({
52-
owner: context.repo.owner,
53-
repo: context.repo.repo,
54-
issue_number: issueNumber,
55-
state: 'closed'
56-
});
57-
console.log(`✅ Auto-closed issue #${issueNumber}`);
58-
} catch (error) {
59-
console.log(`❌ Failed to close issue #${issueNumber}`);
275+
console.log(`🔍 Processing PR #${pr.number} - Action: ${action}`);
276+
277+
// Ensure PR is added to project
278+
let itemId = await getProjectItemId(prId);
279+
if (!itemId) {
280+
itemId = await addToProject(prId);
281+
if (itemId) {
282+
console.log(`✅ Added PR #${pr.number} to project`);
283+
}
284+
}
285+
286+
if (itemId) {
287+
// Move based on action
288+
if (action === 'opened' || action === 'ready_for_review') {
289+
await updateItemStatus(itemId, 'In Review');
290+
} else if (action === 'converted_to_draft') {
291+
await updateItemStatus(itemId, 'In Progress');
292+
} else if (action === 'closed') {
293+
if (pr.merged) {
294+
await updateItemStatus(itemId, 'Done');
295+
console.log(`🎉 PR #${pr.number} merged and moved to Done`);
296+
297+
// Auto-close and move linked issues
298+
const body = pr.body || '';
299+
const issueNumbers = [...body.matchAll(/(?:closes|fixes|resolves)\s+#(\d+)/gi)]
300+
.map(match => parseInt(match[1]));
301+
302+
for (const issueNumber of issueNumbers) {
303+
try {
304+
// Close the issue
305+
await github.rest.issues.update({
306+
owner: context.repo.owner,
307+
repo: context.repo.repo,
308+
issue_number: issueNumber,
309+
state: 'closed'
310+
});
311+
312+
// Get issue details and move to Done
313+
const issueResponse = await github.rest.issues.get({
314+
owner: context.repo.owner,
315+
repo: context.repo.repo,
316+
issue_number: issueNumber
317+
});
318+
319+
const linkedIssueId = issueResponse.data.node_id;
320+
let linkedItemId = await getProjectItemId(linkedIssueId);
321+
322+
if (!linkedItemId) {
323+
linkedItemId = await addToProject(linkedIssueId);
324+
}
325+
326+
if (linkedItemId) {
327+
await updateItemStatus(linkedItemId, 'Done');
328+
}
329+
330+
console.log(`✅ Auto-closed and moved issue #${issueNumber} to Done`);
331+
} catch (error) {
332+
console.log(`❌ Failed to process linked issue #${issueNumber}:`, error);
333+
}
334+
}
335+
} else {
336+
await updateItemStatus(itemId, 'Todo');
337+
console.log(`📝 PR #${pr.number} closed without merge, moved back to Todo`);
60338
}
61339
}
62340
}

0 commit comments

Comments
 (0)