Skip to content

Commit 0b8faec

Browse files
Merge branch 'master' into q1-adlet
2 parents a7a485c + 5863b58 commit 0b8faec

File tree

4 files changed

+271
-0
lines changed

4 files changed

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

contents/handbook/brand/art-requests.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ They get a lot of work requests, so they use [a request template](https://github
2222

2323
You can see what they are working on via the [Art & Brand Planning project board](https://github.com/orgs/PostHog/projects/65/views/2).
2424

25+
### Art board automations
26+
27+
The Art & Brand Planning board uses GitHub Actions to keep work moving:
28+
29+
- **Reminders** — A daily job (9 AM UTC) posts one-time comments on issues that have been stuck in...
30+
- **Feedback/Review** for 10+ days: asks if any feedback is needed to move the task forward.
31+
- **No Status** for 7+ days: asks someone to pick it up or assign it to a column.
32+
- **Status changes** — When an issue’s Status is changed on the board:
33+
- **Moved to "Done"** → the issue is automatically closed (as completed).
34+
- **Moved to "Assigned: Daniel", "Assigned: Lottie", or "Assigned: Heidi"** → other default assignees are removed so only the assigned person is on the issue. Internal requests (from the design team) keep all assignees.
35+
- These changes do not impact the "Assigned: Cleo" column, as Cleo has a different workload.
36+
- Workflows run under the **Art Board Bot** GitHub App and live in `.github/workflows/` (`art-board-reminder.yml`, `art-board-reminders.yml`, `art-board-status-change.yml`).
37+
2538
To establish a clear connection between the task and the working file, designers will create a frame containing a link to the task. They should then add a link to that frame within the task for easy reference.
2639

2740
Lottie and Daniel usually ask for two weeks minimum notice, but can often work faster on things if needed. If your request is genuinely urgent, please share your request issue in [#team-marketing channel](https://posthog.slack.com/archives/C08CG24E3SR) and mention Lottie, Daniel, and/or [Cory](https://posthog.com/community/profiles/30200).

0 commit comments

Comments
 (0)