Skip to content

Commit f37ef62

Browse files
committed
troubleshooting automations
1 parent d28b09b commit f37ef62

14 files changed

Lines changed: 569 additions & 292 deletions
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
name: One-time baseline — move closed issues to their terminal column
2+
3+
# Run this ONCE (dry_run first) before enabling board-reopen-reconcile's
4+
# schedule. It moves any already-closed issue that is sitting in a
5+
# non-terminal column into its terminal column (Org Kanban -> Done,
6+
# Open Roles -> Filled). That establishes a clean baseline so the reopen
7+
# reconciler never resurrects issues that were closed before this automation
8+
# existed. Safe to delete after a successful run, or keep as a manual repair
9+
# tool. See docs/active-spikes/board-sync-redesign.md.
10+
on:
11+
workflow_dispatch:
12+
inputs:
13+
dry_run:
14+
description: 'Dry run (log without changing status)'
15+
required: false
16+
default: 'true'
17+
type: boolean
18+
19+
env:
20+
GH_TOKEN: ${{ secrets.ACTIONS_TOKEN }}
21+
22+
jobs:
23+
baseline-cleanup:
24+
runs-on: ubuntu-latest
25+
26+
steps:
27+
- name: Move closed issues in non-terminal columns to their terminal column
28+
run: |
29+
DRY_RUN="${{ github.event.inputs.dry_run || 'true' }}"
30+
echo "Dry run: $DRY_RUN"
31+
echo "---"
32+
33+
cleanup_board() {
34+
PROJECT_ID="$1"
35+
STATUS_FIELD_ID="$2"
36+
TERMINAL_OPTION="$3"
37+
BOARD_NAME="$4"
38+
CURSOR=""
39+
40+
while true; do
41+
if [ -n "$CURSOR" ]; then
42+
RESULT=$(gh api graphql -f query='
43+
query($project: ID!, $cursor: String!) {
44+
node(id: $project) {
45+
... on ProjectV2 {
46+
items(first: 100, after: $cursor) {
47+
nodes {
48+
id
49+
fieldValueByName(name: "Status") {
50+
... on ProjectV2ItemFieldSingleSelectValue { optionId }
51+
}
52+
content { ... on Issue { number state } }
53+
}
54+
pageInfo { hasNextPage endCursor }
55+
}
56+
}
57+
}
58+
}' -f project="$PROJECT_ID" -f cursor="$CURSOR")
59+
else
60+
RESULT=$(gh api graphql -f query='
61+
query($project: ID!) {
62+
node(id: $project) {
63+
... on ProjectV2 {
64+
items(first: 100) {
65+
nodes {
66+
id
67+
fieldValueByName(name: "Status") {
68+
... on ProjectV2ItemFieldSingleSelectValue { optionId }
69+
}
70+
content { ... on Issue { number state } }
71+
}
72+
pageInfo { hasNextPage endCursor }
73+
}
74+
}
75+
}
76+
}' -f project="$PROJECT_ID")
77+
fi
78+
79+
HAS_NEXT=$(echo "$RESULT" | jq -r '.data.node.items.pageInfo.hasNextPage')
80+
CURSOR=$(echo "$RESULT" | jq -r '.data.node.items.pageInfo.endCursor')
81+
82+
while IFS= read -r item; do
83+
ITEM_ID=$(echo "$item" | jq -r '.id')
84+
OPTION_ID=$(echo "$item" | jq -r '.fieldValueByName.optionId // ""')
85+
NUMBER=$(echo "$item" | jq -r '.content.number // ""')
86+
STATE=$(echo "$item" | jq -r '.content.state // ""')
87+
88+
# Only closed issues, with a status set, not already terminal
89+
[ "$STATE" != "CLOSED" ] && continue
90+
[ -z "$OPTION_ID" ] && continue
91+
[ "$OPTION_ID" == "$TERMINAL_OPTION" ] && continue
92+
93+
echo "Would move #$NUMBER ($BOARD_NAME) to its terminal column."
94+
if [ "$DRY_RUN" != "true" ]; then
95+
gh api graphql -f query='
96+
mutation($project: ID!, $item: ID!, $field: ID!, $option: String!) {
97+
updateProjectV2ItemFieldValue(input: {
98+
projectId: $project,
99+
itemId: $item,
100+
fieldId: $field,
101+
value: { singleSelectOptionId: $option }
102+
}) {
103+
projectV2Item { id }
104+
}
105+
}' -f project="$PROJECT_ID" -f item="$ITEM_ID" \
106+
-f field="$STATUS_FIELD_ID" -f option="$TERMINAL_OPTION" > /dev/null
107+
echo " → Moved."
108+
fi
109+
done < <(echo "$RESULT" | jq -c '.data.node.items.nodes[]')
110+
111+
[ "$HAS_NEXT" != "true" ] && break
112+
done
113+
}
114+
115+
# Org Kanban -> Done, Open Roles -> Filled
116+
cleanup_board "PVT_kwDOAA7NZM4AqQF8" "PVTSSF_lADOAA7NZM4AqQF8zghh1hc" "98236657" "Org Kanban"
117+
cleanup_board "PVT_kwDOAA7NZM4AmdbW" "PVTSSF_lADOAA7NZM4AmdbWzgeWrcw" "47fc9ee4" "Open Roles"
118+
119+
echo "---"
120+
if [ "$DRY_RUN" == "true" ]; then
121+
echo "Dry run complete. Run with dry_run=false to apply."
122+
fi
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
name: Reopen issues moved out of a terminal column (reconciliation)
2+
3+
# Board -> issue "reopen" is the one behavior GitHub gives no immediate path
4+
# for: projects_v2_item can't trigger a repo workflow, and there is no native
5+
# "auto-reopen" (native only ships Auto-close). So this reconciles on a timer.
6+
# See docs/decisions/0004 and docs/active-spikes/board-sync-redesign.md.
7+
on:
8+
# IMPORTANT: the schedule is intentionally left commented out until the
9+
# one-time baseline-terminal-cleanup has been run. Before that, this would
10+
# mass-reopen legacy issues closed before the automation existed. Run this
11+
# via "Run workflow" (dry_run) first, confirm it wouldn't touch legacy
12+
# issues, then uncomment the cron. See board-sync-redesign.todo.md.
13+
# schedule:
14+
# - cron: '*/5 * * * *'
15+
workflow_dispatch:
16+
inputs:
17+
dry_run:
18+
description: 'Dry run (log without reopening)'
19+
required: false
20+
default: 'true'
21+
type: boolean
22+
23+
env:
24+
GH_TOKEN: ${{ secrets.ACTIONS_TOKEN }}
25+
# Skip issues closed within this window — close-to-done / closed-to-filled
26+
# may still be moving a freshly-closed issue into its terminal column.
27+
FRESH_CLOSE_SECONDS: "600"
28+
29+
jobs:
30+
reconcile:
31+
runs-on: ubuntu-latest
32+
33+
steps:
34+
- name: Reopen closed issues whose card sits in a non-terminal column
35+
run: |
36+
DRY_RUN="${{ github.event.inputs.dry_run || 'false' }}"
37+
NOW=$(date +%s)
38+
echo "Dry run: $DRY_RUN"
39+
echo "---"
40+
41+
reconcile_board() {
42+
PROJECT_ID="$1"
43+
TERMINAL_OPTION="$2"
44+
BOARD_NAME="$3"
45+
CURSOR=""
46+
47+
while true; do
48+
if [ -n "$CURSOR" ]; then
49+
RESULT=$(gh api graphql -f query='
50+
query($project: ID!, $cursor: String!) {
51+
node(id: $project) {
52+
... on ProjectV2 {
53+
items(first: 100, after: $cursor) {
54+
nodes {
55+
fieldValueByName(name: "Status") {
56+
... on ProjectV2ItemFieldSingleSelectValue { optionId }
57+
}
58+
content {
59+
... on Issue {
60+
number
61+
state
62+
closedAt
63+
repository { nameWithOwner }
64+
}
65+
}
66+
}
67+
pageInfo { hasNextPage endCursor }
68+
}
69+
}
70+
}
71+
}' -f project="$PROJECT_ID" -f cursor="$CURSOR")
72+
else
73+
RESULT=$(gh api graphql -f query='
74+
query($project: ID!) {
75+
node(id: $project) {
76+
... on ProjectV2 {
77+
items(first: 100) {
78+
nodes {
79+
fieldValueByName(name: "Status") {
80+
... on ProjectV2ItemFieldSingleSelectValue { optionId }
81+
}
82+
content {
83+
... on Issue {
84+
number
85+
state
86+
closedAt
87+
repository { nameWithOwner }
88+
}
89+
}
90+
}
91+
pageInfo { hasNextPage endCursor }
92+
}
93+
}
94+
}
95+
}' -f project="$PROJECT_ID")
96+
fi
97+
98+
HAS_NEXT=$(echo "$RESULT" | jq -r '.data.node.items.pageInfo.hasNextPage')
99+
CURSOR=$(echo "$RESULT" | jq -r '.data.node.items.pageInfo.endCursor')
100+
101+
while IFS= read -r item; do
102+
OPTION_ID=$(echo "$item" | jq -r '.fieldValueByName.optionId // ""')
103+
NUMBER=$(echo "$item" | jq -r '.content.number // ""')
104+
STATE=$(echo "$item" | jq -r '.content.state // ""')
105+
CLOSED_AT=$(echo "$item" | jq -r '.content.closedAt // ""')
106+
REPO=$(echo "$item" | jq -r '.content.repository.nameWithOwner // ""')
107+
108+
# Only closed issues, with a status set, not already terminal
109+
[ "$STATE" != "CLOSED" ] && continue
110+
[ -z "$OPTION_ID" ] && continue
111+
[ "$OPTION_ID" == "$TERMINAL_OPTION" ] && continue
112+
[ -z "$NUMBER" ] || [ -z "$REPO" ] && continue
113+
114+
# Fresh-close guard
115+
if [ -n "$CLOSED_AT" ]; then
116+
CLOSED_TS=$(date -d "$CLOSED_AT" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%SZ" "$CLOSED_AT" +%s)
117+
if [ $(( NOW - CLOSED_TS )) -lt "$FRESH_CLOSE_SECONDS" ]; then
118+
echo "Skip #$NUMBER ($BOARD_NAME) — closed less than ${FRESH_CLOSE_SECONDS}s ago."
119+
continue
120+
fi
121+
fi
122+
123+
echo "Would reopen: #$NUMBER in $REPO ($BOARD_NAME — non-terminal column, issue closed)"
124+
if [ "$DRY_RUN" != "true" ]; then
125+
gh issue reopen "$NUMBER" --repo "$REPO"
126+
echo " → Reopened."
127+
fi
128+
done < <(echo "$RESULT" | jq -c '.data.node.items.nodes[]')
129+
130+
[ "$HAS_NEXT" != "true" ] && break
131+
done
132+
}
133+
134+
# Org Kanban: terminal column is Done. Open Roles: terminal is Filled.
135+
reconcile_board "PVT_kwDOAA7NZM4AqQF8" "98236657" "Org Kanban"
136+
reconcile_board "PVT_kwDOAA7NZM4AmdbW" "47fc9ee4" "Open Roles"
137+
138+
echo "---"
139+
if [ "$DRY_RUN" == "true" ]; then
140+
echo "Dry run complete. Run with dry_run=false to apply."
141+
fi

.github/workflows/kanban-status-reopen.yaml

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

0 commit comments

Comments
 (0)