Skip to content

One-time baseline — move closed issues to their terminal column #1

One-time baseline — move closed issues to their terminal column

One-time baseline — move closed issues to their terminal column #1

name: One-time baseline — move closed issues to their terminal column
# Run this ONCE (dry_run first) before enabling board-reopen-reconcile's
# schedule. It moves any already-closed issue that is sitting in a
# non-terminal column into its terminal column (Org Kanban -> Done,
# Open Roles -> Filled). That establishes a clean baseline so the reopen
# reconciler never resurrects issues that were closed before this automation
# existed. Safe to delete after a successful run, or keep as a manual repair
# tool. See docs/active-spikes/board-sync-redesign.md.
on:
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run (log without changing status)'
required: false
default: 'true'
type: boolean
env:
GH_TOKEN: ${{ secrets.ACTIONS_TOKEN }}
jobs:
baseline-cleanup:
runs-on: ubuntu-latest
steps:
- name: Move closed issues in non-terminal columns to their terminal column
run: |
DRY_RUN="${{ github.event.inputs.dry_run || 'true' }}"
echo "Dry run: $DRY_RUN"
echo "---"
cleanup_board() {
PROJECT_ID="$1"
STATUS_FIELD_ID="$2"
TERMINAL_OPTION="$3"
BOARD_NAME="$4"
CURSOR=""
while true; do
if [ -n "$CURSOR" ]; then
RESULT=$(gh api graphql -f query='
query($project: ID!, $cursor: String!) {
node(id: $project) {
... on ProjectV2 {
items(first: 100, after: $cursor) {
nodes {
id
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue { optionId }
}
content { ... on Issue { number state } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' -f project="$PROJECT_ID" -f cursor="$CURSOR")
else
RESULT=$(gh api graphql -f query='
query($project: ID!) {
node(id: $project) {
... on ProjectV2 {
items(first: 100) {
nodes {
id
fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue { optionId }
}
content { ... on Issue { number state } }
}
pageInfo { hasNextPage endCursor }
}
}
}
}' -f project="$PROJECT_ID")
fi
HAS_NEXT=$(echo "$RESULT" | jq -r '.data.node.items.pageInfo.hasNextPage')
CURSOR=$(echo "$RESULT" | jq -r '.data.node.items.pageInfo.endCursor')
while IFS= read -r item; do
ITEM_ID=$(echo "$item" | jq -r '.id')
OPTION_ID=$(echo "$item" | jq -r '.fieldValueByName.optionId // ""')
NUMBER=$(echo "$item" | jq -r '.content.number // ""')
STATE=$(echo "$item" | jq -r '.content.state // ""')
# Only closed issues, with a status set, not already terminal
[ "$STATE" != "CLOSED" ] && continue
[ -z "$OPTION_ID" ] && continue
[ "$OPTION_ID" == "$TERMINAL_OPTION" ] && continue
echo "Would move #$NUMBER ($BOARD_NAME) to its terminal column."
if [ "$DRY_RUN" != "true" ]; then
gh api graphql -f query='
mutation($project: ID!, $item: ID!, $field: ID!, $option: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $project,
itemId: $item,
fieldId: $field,
value: { singleSelectOptionId: $option }
}) {
projectV2Item { id }
}
}' -f project="$PROJECT_ID" -f item="$ITEM_ID" \
-f field="$STATUS_FIELD_ID" -f option="$TERMINAL_OPTION" > /dev/null
echo " → Moved."
fi
done < <(echo "$RESULT" | jq -c '.data.node.items.nodes[]')
[ "$HAS_NEXT" != "true" ] && break
done
}
# Org Kanban -> Done, Open Roles -> Filled
cleanup_board "PVT_kwDOAA7NZM4AqQF8" "PVTSSF_lADOAA7NZM4AqQF8zghh1hc" "98236657" "Org Kanban"
cleanup_board "PVT_kwDOAA7NZM4AmdbW" "PVTSSF_lADOAA7NZM4AmdbWzgeWrcw" "47fc9ee4" "Open Roles"
echo "---"
if [ "$DRY_RUN" == "true" ]; then
echo "Dry run complete. Run with dry_run=false to apply."
fi