Skip to content

Commit 81ee964

Browse files
committed
refactor: move art board workflows to .github/workflows and deduplicate
Workflows were in src/pages/workflows/ which Gatsby processes as pages (breaking the build) and GitHub Actions never picks up. Consolidated 4 files (550 lines) into 3 (233 lines): - art-board-reminder.yml: reusable workflow_call for stale-issue reminders - art-board-reminders.yml: cron caller for feedback (10d) and no-status (7d) - art-board-status-change.yml: combines close-on-done + sync-assignees
1 parent f12e808 commit 81ee964

File tree

7 files changed

+233
-550
lines changed

7 files changed

+233
-550
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
name: Art board project reminder (reusable)
2+
3+
# Reusable workflow: paginates a Projects V2 board, finds issues stuck in a
4+
# given column for too long, and posts a one-time reminder comment.
5+
6+
on:
7+
workflow_call:
8+
inputs:
9+
column:
10+
required: true
11+
type: string
12+
stale_days:
13+
required: true
14+
type: number
15+
match_missing_status:
16+
description: Also match items with no Status field set (for "No Status" columns)
17+
required: false
18+
type: boolean
19+
default: false
20+
reminder_tag:
21+
description: HTML comment used to deduplicate reminders
22+
required: true
23+
type: string
24+
message:
25+
description: 'Comment body — use {days} as a placeholder'
26+
required: true
27+
type: string
28+
secrets:
29+
PROJECT_PAT:
30+
required: true
31+
32+
jobs:
33+
remind:
34+
runs-on: ubuntu-latest
35+
steps:
36+
- name: Find stale issues and post reminders
37+
uses: actions/github-script@v7
38+
env:
39+
INPUT_COLUMN: ${{ inputs.column }}
40+
INPUT_STALE_DAYS: ${{ inputs.stale_days }}
41+
INPUT_MATCH_MISSING: ${{ inputs.match_missing_status }}
42+
INPUT_TAG: ${{ inputs.reminder_tag }}
43+
INPUT_MESSAGE: ${{ inputs.message }}
44+
with:
45+
github-token: ${{ secrets.PROJECT_PAT }}
46+
script: |
47+
const column = process.env.INPUT_COLUMN;
48+
const staleDays = parseInt(process.env.INPUT_STALE_DAYS);
49+
const matchMissing = process.env.INPUT_MATCH_MISSING === 'true';
50+
const reminderTag = process.env.INPUT_TAG;
51+
const msgTemplate = process.env.INPUT_MESSAGE;
52+
const snoozed = 'All projects (snoozed projects)';
53+
const staleMs = staleDays * 86_400_000;
54+
const now = Date.now();
55+
56+
// ── Paginate all project items ────────────────────────────────
57+
const QUERY = `
58+
query($org: String!, $num: Int!, $cursor: String) {
59+
organization(login: $org) {
60+
projectV2(number: $num) {
61+
items(first: 100, after: $cursor) {
62+
pageInfo { hasNextPage endCursor }
63+
nodes {
64+
updatedAt
65+
fieldValues(first: 20) {
66+
nodes {
67+
... on ProjectV2ItemFieldSingleSelectValue {
68+
name
69+
field { ... on ProjectV2FieldCommon { name } }
70+
}
71+
}
72+
}
73+
content {
74+
... on Issue { number state repository { nameWithOwner } }
75+
}
76+
}
77+
}
78+
}
79+
}
80+
}`;
81+
82+
let items = [], cursor = null;
83+
do {
84+
const res = await github.graphql(QUERY, { org: 'PostHog', num: 65, cursor });
85+
const page = res.organization.projectV2.items;
86+
items = items.concat(page.nodes);
87+
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
88+
} while (cursor);
89+
90+
// ── Filter stale items ────────────────────────────────────────
91+
const stale = items.filter(item => {
92+
const issue = item.content;
93+
if (!issue?.number || issue.state === 'CLOSED') return false;
94+
95+
const status = item.fieldValues.nodes.find(f => f.field?.name === 'Status');
96+
if (status?.name === snoozed) return false;
97+
98+
const matches = matchMissing
99+
? (!status || status.name === column)
100+
: (status?.name === column);
101+
if (!matches) return false;
102+
103+
return (now - new Date(item.updatedAt).getTime()) >= staleMs;
104+
});
105+
106+
console.log(`${stale.length} stale item(s) in "${column}".`);
107+
108+
// ── Post one-time reminders ───────────────────────────────────
109+
for (const item of stale) {
110+
const issue = item.content;
111+
const [owner, repo] = issue.repository.nameWithOwner.split('/');
112+
113+
const { data: comments } = await github.rest.issues.listComments({
114+
owner, repo, issue_number: issue.number, per_page: 100,
115+
});
116+
if (comments.some(c => c.body.includes(reminderTag))) continue;
117+
118+
const days = Math.floor((now - new Date(item.updatedAt).getTime()) / 86_400_000);
119+
await github.rest.issues.createComment({
120+
owner, repo, issue_number: issue.number,
121+
body: `${reminderTag}\n${msgTemplate.replace('{days}', days)}`,
122+
});
123+
console.log(`Reminded #${issue.number} (${days} days).`);
124+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Art board reminders
2+
3+
on:
4+
schedule:
5+
- cron: '0 9 * * *' # daily 9 AM UTC
6+
workflow_dispatch:
7+
8+
jobs:
9+
feedback-review:
10+
uses: ./.github/workflows/art-board-reminder.yml
11+
with:
12+
column: 'Feedback/Review'
13+
stale_days: 10
14+
reminder_tag: '<!-- art-board-feedback-reminder -->'
15+
message: 'This issue has been in **Feedback/Review** for {days} days. Any feedback needed to move it forward?'
16+
secrets:
17+
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
18+
19+
no-status:
20+
uses: ./.github/workflows/art-board-reminder.yml
21+
with:
22+
column: 'No Status'
23+
stale_days: 7
24+
match_missing_status: true
25+
reminder_tag: '<!-- art-board-no-status-reminder -->'
26+
message: 'This issue has been sitting without an owner for {days} days. Can someone pick this up or assign it to a column on the board?'
27+
secrets:
28+
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Art board status change
2+
3+
# Handles two actions when an issue is moved on the art request board:
4+
# 1. Moved to "Done" → close the issue
5+
# 2. Moved to "Assigned: X" → remove the other default assignees
6+
# (exception: internal requests from team members keep everyone assigned)
7+
8+
on:
9+
projects_v2_item:
10+
types: [edited]
11+
12+
jobs:
13+
handle:
14+
runs-on: ubuntu-latest
15+
if: github.event.projects_v2_item.field_name == 'Status'
16+
steps:
17+
- name: Close issue or sync assignees
18+
uses: actions/github-script@v7
19+
with:
20+
github-token: ${{ secrets.PROJECT_PAT }}
21+
script: |
22+
const ASSIGNEE_MAP = {
23+
'Assigned: Daniel': { keep: 'dphawkins1617', remove: ['lottiecoxon', 'heidiberton'] },
24+
'Assigned: Lottie': { keep: 'lottiecoxon', remove: ['dphawkins1617', 'heidiberton'] },
25+
'Assigned: Heidi': { keep: 'heidiberton', remove: ['lottiecoxon', 'dphawkins1617'] },
26+
};
27+
const TEAM = ['lottiecoxon', 'dphawkins1617', 'heidiberton'];
28+
29+
const { node: item } = await github.graphql(`
30+
query($id: ID!) {
31+
node(id: $id) {
32+
... on ProjectV2Item {
33+
fieldValues(first: 20) {
34+
nodes {
35+
... on ProjectV2ItemFieldSingleSelectValue {
36+
name
37+
field { ... on ProjectV2FieldCommon { name } }
38+
}
39+
}
40+
}
41+
content {
42+
... on Issue {
43+
number state author { login }
44+
repository { nameWithOwner }
45+
}
46+
}
47+
}
48+
}
49+
}`, { id: context.payload.projects_v2_item.node_id });
50+
51+
const status = item.fieldValues.nodes.find(f => f.field?.name === 'Status');
52+
if (!status) return;
53+
54+
const issue = item.content;
55+
if (!issue?.number || issue.state === 'CLOSED') return;
56+
57+
const [owner, repo] = issue.repository.nameWithOwner.split('/');
58+
59+
// ── Done → close ──────────────────────────────────────────────
60+
if (status.name === 'Done') {
61+
await github.rest.issues.update({
62+
owner, repo, issue_number: issue.number,
63+
state: 'closed', state_reason: 'completed',
64+
});
65+
console.log(`Closed #${issue.number}`);
66+
return;
67+
}
68+
69+
// ── Assigned column → sync assignees ──────────────────────────
70+
const mapping = ASSIGNEE_MAP[status.name];
71+
if (!mapping) return;
72+
73+
if (TEAM.includes(issue.author?.login?.toLowerCase())) {
74+
console.log(`#${issue.number} is an internal request — keeping all assignees.`);
75+
return;
76+
}
77+
78+
await github.rest.issues.removeAssignees({
79+
owner, repo, issue_number: issue.number, assignees: mapping.remove,
80+
});
81+
console.log(`Synced assignees on #${issue.number}: kept @${mapping.keep}`);

src/pages/workflows/close-on-done.yml

Lines changed: 0 additions & 94 deletions
This file was deleted.

0 commit comments

Comments
 (0)