Cleanup Workflow #64
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" | |
| # Performs the actual run deletion. | |
| # | |
| # This contains a lot of code, but the vast majority is just pretty-printing. | |
| - 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 | |
| mapfile -t WF_ARRAY < <(echo "$WORKFLOWS" | jq -c '.[]') | |
| for wf in "${WF_ARRAY[@]}"; 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 | |
| # We use github's graphql API here which allows us to paginate over workflow runs. | |
| # | |
| # Unfortunately `gh run list` does not support pagination, so we use the graphql API instead. | |
| 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 | |
| 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 |