Cleanup Workflow #62
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Manual workflow to cleanup deleted workflows runs. | |
| # | |
| # Github keeps workflows runs around even if the workflow is deleted. | |
| # This has the side effect that these still display in the UI which gets cluttered. | |
| # Once the runs of a workflow are deleted, they also get removed from the UI. | |
| name: Cleanup Workflow | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: "Choose 'dry run' to preview or 'execute' to delete runs" | |
| required: true | |
| default: "dry run" | |
| type: choice | |
| options: | |
| - "dry run" | |
| - "execute" | |
| jobs: | |
| cleanup: | |
| name: Cleanup deleted workflows | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: write # required for deleting workflow runs | |
| contents: read | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Workflows on main | |
| id: main | |
| run: | | |
| git fetch origin main | |
| WORKFLOWS=$(git ls-tree -r origin/main --name-only | grep '^.github/workflows/') | |
| printf "%s\n" $WORKFLOWS | |
| { | |
| echo "workflows<<EOF" | |
| echo "$WORKFLOWS" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Workflows on next | |
| id: next | |
| run: | | |
| git fetch origin next | |
| WORKFLOWS=$(git ls-tree -r origin/next --name-only | grep '^.github/workflows/') | |
| printf "%s\n" $WORKFLOWS | |
| { | |
| echo "workflows<<EOF" | |
| echo "$WORKFLOWS" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Filter for deleted workflows | |
| id: deleted | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| # Union of `main` and `next` workflows as a JSON array of strings (paths) | |
| EXISTING=$(printf "%s\n%s\n" \ | |
| "${{ steps.main.outputs.workflows }}" \ | |
| "${{ steps.next.outputs.workflows }}" \ | |
| ) | |
| EXISTING=$(echo "$EXISTING" | sort -u | jq -R . | jq -s .) | |
| echo "Existing workflows:" | |
| echo "$EXISTING" | |
| # Get workflows currently on GitHub as JSON array of objects | |
| GITHUB=$(gh api repos/{owner}/{repo}/actions/workflows \ | |
| --jq '.workflows[] | select(.path | startswith(".github")) | { name, node_id, path }' \ | |
| | jq -s '.') | |
| echo "Workflows on GitHub:" | |
| echo "$GITHUB" | |
| # Find deleted workflows: present on GitHub but not in main/next | |
| DELETED=$(echo "$GITHUB" | jq -c \ | |
| --argjson existing "$EXISTING" ' | |
| map(select(.path as $p | $existing | index($p) | not)) | |
| ' | |
| ) | |
| echo "Deleted workflows:" | |
| echo "$DELETED" | |
| # Output to GitHub Actions | |
| { | |
| echo "workflows<<EOF" | |
| echo "$DELETED" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Delete runs from deleted workflows | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| MODE: ${{ inputs.mode }} | |
| WORKFLOWS: ${{ steps.deleted.outputs.workflows }} | |
| OWNER: ${{ github.repository_owner }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$WORKFLOWS" ]; then | |
| echo "No workflows to delete." | |
| exit 0 | |
| fi | |
| TOTAL_AFFECTED=0 | |
| SUMMARY=() | |
| CURRENT_INDEX=0 | |
| # Column widths | |
| INDEX_WIDTH=9 | |
| MAX_WF_LENGTH=30 | |
| WORKFLOW_COUNT_WIDTH=14 | |
| GLOBAL_TOTAL_WIDTH=12 | |
| TOTAL_WIDTH=$(( INDEX_WIDTH + 3 + MAX_WF_LENGTH + 3 + WORKFLOW_COUNT_WIDTH + 3 + GLOBAL_TOTAL_WIDTH )) | |
| # Count total workflows for progress display | |
| TOTAL_WORKFLOWS=$(echo "$WORKFLOWS" | jq -r '. | length') | |
| # Function to print dynamic table headers padded with '=' and spaces | |
| print_header() { | |
| local name="$1" | |
| local name_len=${#name} | |
| local pad=$(( (TOTAL_WIDTH - name_len - 2) / 2 )) | |
| local left_pad=$pad | |
| local right_pad=$(( TOTAL_WIDTH - name_len - 2 - left_pad )) | |
| left_str=$(printf '=%0.s' $(seq 1 $left_pad)) | |
| right_str=$(printf '=%0.s' $(seq 1 $right_pad)) | |
| printf "\n%s %s %s\n" "$left_str" "$name" "$right_str" | |
| } | |
| # === Progress Header === | |
| print_header "Workflow Cleanup Progress" | |
| printf "%*s | %-*s | %-*s | %-*s\n" \ | |
| "$INDEX_WIDTH" "Index" \ | |
| "$MAX_WF_LENGTH" "Workflow" \ | |
| "$WORKFLOW_COUNT_WIDTH" "Workflow Count" \ | |
| "$GLOBAL_TOTAL_WIDTH" "Global Total" | |
| printf "%*s-+-%-*s-+-%-*s-+-%-*s\n" \ | |
| "$INDEX_WIDTH" "---------" \ | |
| "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" \ | |
| "$WORKFLOW_COUNT_WIDTH" "$(printf '%.0s-' $(seq 1 $WORKFLOW_COUNT_WIDTH))" \ | |
| "$GLOBAL_TOTAL_WIDTH" "$(printf '%.0s-' $(seq 1 $GLOBAL_TOTAL_WIDTH))" | |
| # Loop over deleted workflows JSON | |
| echo "$WORKFLOWS" | jq -c '.[]' | while read -r wf; do | |
| CURRENT_INDEX=$((CURRENT_INDEX + 1)) | |
| WORKFLOW_NAME=$(echo "$wf" | jq -r '.name') | |
| WORKFLOW_ID=$(echo "$wf" | jq -r '.node_id') | |
| WORKFLOW_PATH=$(echo "$wf" | jq -r '.path') | |
| # Safety checks | |
| if [ -z "$WORKFLOW_NAME" ]; then | |
| echo "::error title=Workflow name empty::Resolved workflow name is empty at index $CURRENT_INDEX" | |
| exit 1 | |
| fi | |
| if [ -z "$WORKFLOW_ID" ]; then | |
| echo "::error title=Workflow ID missing::Workflow '$WORKFLOW_NAME' (path: $WORKFLOW_PATH) has no ID" | |
| exit 1 | |
| fi | |
| WORKFLOW_COUNT=0 | |
| AFTER_CURSOR="" | |
| # Paginate over workflow runs | |
| while true; do | |
| RESPONSE=$(gh api graphql -F workflowId="$WORKFLOW_ID" -F after="$AFTER_CURSOR" \ | |
| -f query='query($workflowId: ID!, $after: String) { | |
| node(id: $workflowId) { | |
| ... on Workflow { | |
| runs(first: 100, after: $after) { | |
| pageInfo { hasNextPage endCursor } | |
| nodes { databaseId } | |
| } | |
| } | |
| } | |
| }') | |
| RUN_IDS=$(echo "$RESPONSE" | jq -r '.data.node.runs.nodes[].databaseId') | |
| HAS_NEXT=$(echo "$RESPONSE" | jq -r '.data.node.runs.pageInfo.hasNextPage') | |
| AFTER_CURSOR=$(echo "$RESPONSE" | jq -r '.data.node.runs.pageInfo.endCursor') | |
| [ -z "$RUN_IDS" ] && break | |
| BATCH_COUNT=$(echo "$RUN_IDS" | wc -l | tr -d ' ') | |
| WORKFLOW_COUNT=$((WORKFLOW_COUNT + BATCH_COUNT)) | |
| TOTAL_AFFECTED=$((TOTAL_AFFECTED + BATCH_COUNT)) | |
| # Print progress | |
| printf "%*s | %-*s | %*s | %*s\n" \ | |
| "$INDEX_WIDTH" "[$CURRENT_INDEX/$TOTAL_WORKFLOWS]" \ | |
| "$MAX_WF_LENGTH" "$WORKFLOW_NAME" \ | |
| "$WORKFLOW_COUNT_WIDTH" "$WORKFLOW_COUNT" \ | |
| "$GLOBAL_TOTAL_WIDTH" "$TOTAL_AFFECTED" | |
| if [ "$MODE" = "execute" ]; then | |
| for RUN_ID in $RUN_IDS; do | |
| gh run delete "$RUN_ID" >/dev/null | |
| done | |
| fi | |
| [ "$HAS_NEXT" != "true" ] && break | |
| # TEMPORARY safety break | |
| if [ "$WORKFLOW_COUNT" -gt 200 ]; then | |
| echo " ⚠️ Temporary break: workflow count exceeded 200, stopping early." | |
| break | |
| fi | |
| done | |
| SUMMARY+=("$WORKFLOW_NAME|$WORKFLOW_COUNT") | |
| done | |
| # === Summary Header === | |
| print_header "Workflow Cleanup Summary" | |
| printf "%*s | %-*s | %-*s | %-*s\n" \ | |
| "$INDEX_WIDTH" "" \ | |
| "$MAX_WF_LENGTH" "Workflow" \ | |
| "$WORKFLOW_COUNT_WIDTH" "Runs" \ | |
| "$GLOBAL_TOTAL_WIDTH" "" | |
| printf "%*s-+-%-*s-+-%-*s-+-%-*s\n" \ | |
| "$INDEX_WIDTH" "$(printf '%.0s-' $(seq 1 $INDEX_WIDTH))" \ | |
| "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" \ | |
| "$WORKFLOW_COUNT_WIDTH" "$(printf '%.0s-' $(seq 1 $WORKFLOW_COUNT_WIDTH))" \ | |
| "$GLOBAL_TOTAL_WIDTH" "$(printf '%.0s-' $(seq 1 $GLOBAL_TOTAL_WIDTH))" | |
| # Print summary rows | |
| for entry in "${SUMMARY[@]}"; do | |
| wf="${entry%%|*}" | |
| count="${entry##*|}" | |
| printf "%*s | %-*s | %*s | %-*s\n" \ | |
| "$INDEX_WIDTH" "" \ | |
| "$MAX_WF_LENGTH" "$wf" \ | |
| "$WORKFLOW_COUNT_WIDTH" "$count" \ | |
| "$GLOBAL_TOTAL_WIDTH" "" | |
| done | |
| # Footer separator | |
| printf "%*s-+-%-*s-+-%-*s-+-%-*s\n" \ | |
| "$INDEX_WIDTH" "$(printf '%.0s-' $(seq 1 $INDEX_WIDTH))" \ | |
| "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" \ | |
| "$WORKFLOW_COUNT_WIDTH" "$(printf '%.0s-' $(seq 1 $WORKFLOW_COUNT_WIDTH))" \ | |
| "$GLOBAL_TOTAL_WIDTH" "$(printf '%.0s-' $(seq 1 $GLOBAL_TOTAL_WIDTH))" | |
| # TOTAL row | |
| printf "%*s | %-*s | %*s | %-*s\n" \ | |
| "$INDEX_WIDTH" "" \ | |
| "$MAX_WF_LENGTH" "TOTAL" \ | |
| "$WORKFLOW_COUNT_WIDTH" "$TOTAL_AFFECTED" \ | |
| "$GLOBAL_TOTAL_WIDTH" "" | |
| if [ "$MODE" = "dry run" ]; then | |
| echo "Dry run complete. No runs were deleted." | |
| else | |
| echo "Cleanup complete." | |
| fi |