Cleanup Workflow #37
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: Workflows on github | |
| id: github | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Note that we filter by `.github` path prefix to ensure we only get locally defined workflows. | |
| # | |
| # Examples of non-local workflows are `dependabot` and `copilot` which have paths: | |
| # - dynamic/dependabot/dependabot-updates | |
| # - dynamic/copilot-pull-request-reviewer/copilot-pull-request-reviewer | |
| WORKFLOWS=$(gh workflow list \ | |
| --all \ | |
| --json path \ | |
| --jq '.[] | select(.path | startswith(".github")) | .path' \ | |
| ) | |
| printf "%s\n" $WORKFLOWS | |
| { | |
| echo "workflows<<EOF" | |
| echo "$WORKFLOWS" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Filter for deleted workflows | |
| id: deleted | |
| run: | | |
| # Union of `main` and `next` workflows. | |
| EXISTING_FILES=$( \ | |
| printf "%s\n%s\n" \ | |
| "${{ steps.main.outputs.workflows }}" \ | |
| "${{ steps.next.outputs.workflows }}" \ | |
| ) | |
| EXISTING_FILES=$(echo "$EXISTING_FILES" | sort -u) | |
| printf "%s\n" $EXISTING_FILES | |
| # Find deleted workflows as the items in `WORKFLOWS` but not in the union of main and next. | |
| # This assumes that _all_ items in main and next are present in `WORKFLOWS`. | |
| DELETED_FILES=$( \ | |
| printf "%s\n%s\n" \ | |
| "$EXISTING_FILES" \ | |
| "${{ steps.github.outputs.workflows }}" \ | |
| ) | |
| DELETED_FILES=$(echo "$DELETED_FILES" | sort | uniq -u) | |
| printf "%s\n" $DELETED_FILES | |
| { | |
| echo "workflows<<EOF" | |
| echo "$DELETED_FILES" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Delete runs from deleted workflows | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| MODE: ${{ inputs.mode }} | |
| DELETED_WORKFLOWS: ${{ steps.deleted.outputs.workflows }} | |
| OWNER: ${{ github.repository_owner }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| TOTAL_AFFECTED=0 | |
| SUMMARY=() | |
| # Read workflows into an array for indexing | |
| mapfile -t WORKFLOWS_ARRAY <<< "$DELETED_WORKFLOWS" | |
| TOTAL_WORKFLOWS=${#WORKFLOWS_ARRAY[@]} | |
| # Convert workflow paths to workflow names | |
| WORKFLOW_NAMES_ARRAY=() | |
| for wf_path in "${WORKFLOWS_ARRAY[@]}"; do | |
| # Extract 'name' from YAML, fallback to filename | |
| name=$(yq -r '.name // ""' "$wf_path" 2>/dev/null || true) | |
| if [ -z "$name" ]; then | |
| name=$(basename "$wf_path") | |
| fi | |
| WORKFLOW_NAMES_ARRAY+=("$name") | |
| done | |
| # Determine max workflow name length for alignment | |
| MAX_WF_LENGTH=0 | |
| for wf in "${WORKFLOW_NAMES_ARRAY[@]}"; do | |
| len=${#wf} | |
| (( len > MAX_WF_LENGTH )) && MAX_WF_LENGTH=$len | |
| done | |
| (( MAX_WF_LENGTH < 30 )) && MAX_WF_LENGTH=30 # minimum width | |
| # Column widths | |
| INDEX_WIDTH=9 | |
| WORKFLOW_COUNT_WIDTH=14 | |
| GLOBAL_TOTAL_WIDTH=12 | |
| # Total table width | |
| TOTAL_WIDTH=$(( INDEX_WIDTH + 3 + MAX_WF_LENGTH + 3 + WORKFLOW_COUNT_WIDTH + 3 + GLOBAL_TOTAL_WIDTH )) | |
| # 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))" | |
| # Process each workflow by name | |
| for i in "${!WORKFLOW_NAMES_ARRAY[@]}"; do | |
| WORKFLOW_NAME=${WORKFLOW_NAMES_ARRAY[$i]} | |
| [ -z "\" ] && continue | |
| CURRENT_INDEX=$((i + 1)) | |
| WORKFLOW_COUNT=0 | |
| WORKFLOW_ID=$(gh workflow list --json id,name \ | |
| --jq ".[] | select(.name==\"\\") | .id") | |
| # GraphQL pagination variables | |
| AFTER_CURSOR=null | |
| 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.repository.workflowRuns.nodes[].databaseId') | |
| HAS_NEXT=$(echo "$RESPONSE" | jq -r '.data.repository.workflowRuns.pageInfo.hasNextPage') | |
| AFTER_CURSOR=$(echo "$RESPONSE" | jq -r '.data.repository.workflowRuns.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 line | |
| printf "%*s | %-*s | %*s | %*s\n" \ | |
| "$INDEX_WIDTH" "[$CURRENT_INDEX/$TOTAL_WORKFLOWS]" \ | |
| "$MAX_WF_LENGTH" "\" \ | |
| "$WORKFLOW_COUNT_WIDTH" "$WORKFLOW_COUNT" \ | |
| "$GLOBAL_TOTAL_WIDTH" "$TOTAL_AFFECTED" | |
| if [ "$MODE" = "execute" ]; then | |
| for RUN_ID in $RUN_IDS; do | |
| echo | gh run delete "$RUN_ID" >/dev/null | |
| done | |
| fi | |
| [ "$HAS_NEXT" != "true" ] && break | |
| # TEMPORARY break for testing large workflows | |
| if [ "$WORKFLOW_COUNT" -gt 200 ]; then | |
| echo " ⚠️ Temporary break: workflow count exceeded 200, stopping early." | |
| break | |
| fi | |
| done | |
| SUMMARY+=("\|$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_WIDT |