From 339e766e2e4e7932e73d638144fc51e09cb705dd Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:17:35 +0200 Subject: [PATCH 1/9] Fix github multiline --- .github/workflows/cleanup-workflows.yml | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cleanup-workflows.yml b/.github/workflows/cleanup-workflows.yml index a10133f1f3..50b0497505 100644 --- a/.github/workflows/cleanup-workflows.yml +++ b/.github/workflows/cleanup-workflows.yml @@ -37,7 +37,11 @@ jobs: git fetch origin main WORKFLOWS=$(git ls-tree -r origin/main --name-only | grep '^.github/workflows/') echo $WORKFLOWS - echo "workflows=$WORKFLOWS" >> "$GITHUB_OUTPUT" + { + echo "workflows<> "$GITHUB_OUTPUT" - name: Workflows on next id: next @@ -45,7 +49,11 @@ jobs: git fetch origin next WORKFLOWS=$(git ls-tree -r origin/next --name-only | grep '^.github/workflows/') echo $WORKFLOWS - echo "workflows=$WORKFLOWS" >> "$GITHUB_OUTPUT" + { + echo "workflows<> "$GITHUB_OUTPUT" - name: Workflows on github id: github @@ -61,7 +69,11 @@ jobs: --jq '.[] | select(.path | startswith(".github")) | .path' \ ) echo $WORKFLOWS - echo "workflows=$WORKFLOWS" >> "$GITHUB_OUTPUT" + { + echo "workflows<> "$GITHUB_OUTPUT" - name: Filter for deleted workflows id: deleted @@ -84,7 +96,11 @@ jobs: ) DELETED_FILES=$(echo "$DELETED_FILES" | sort | uniq -u) echo $DELETED_FILES - echo "workflows=$DELETED_FILES" >> "$GITHUB_OUTPUT" + { + echo "workflows<> "$GITHUB_OUTPUT" - name: Delete runs from deleted workflows env: From 7b7ded7def04eff4ca32dfc51d6d68ee3effc52f Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:20:40 +0200 Subject: [PATCH 2/9] gh api requires GITHUB_TOKEN --- .github/workflows/cleanup-workflows.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cleanup-workflows.yml b/.github/workflows/cleanup-workflows.yml index 50b0497505..fb346c30ba 100644 --- a/.github/workflows/cleanup-workflows.yml +++ b/.github/workflows/cleanup-workflows.yml @@ -57,6 +57,8 @@ jobs: - 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. # From f87f653ae8ed046ceb7208fd9f0ab17399eb69b2 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:21:55 +0200 Subject: [PATCH 3/9] Print items in lines --- .github/workflows/cleanup-workflows.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cleanup-workflows.yml b/.github/workflows/cleanup-workflows.yml index fb346c30ba..181ab90e52 100644 --- a/.github/workflows/cleanup-workflows.yml +++ b/.github/workflows/cleanup-workflows.yml @@ -36,7 +36,7 @@ jobs: run: | git fetch origin main WORKFLOWS=$(git ls-tree -r origin/main --name-only | grep '^.github/workflows/') - echo $WORKFLOWS + printf "%s\n" $WORKFLOWS { echo "workflows< Date: Mon, 16 Feb 2026 11:27:19 +0200 Subject: [PATCH 4/9] Log progress --- .github/workflows/cleanup-workflows.yml | 89 ++++++++++++++++++------- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/.github/workflows/cleanup-workflows.yml b/.github/workflows/cleanup-workflows.yml index 181ab90e52..2cd87ccb56 100644 --- a/.github/workflows/cleanup-workflows.yml +++ b/.github/workflows/cleanup-workflows.yml @@ -87,7 +87,7 @@ jobs: "${{ steps.next.outputs.workflows }}" \ ) EXISTING_FILES=$(echo "$EXISTING_FILES" | sort -u) - echo $EXISTING_FILES + 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`. @@ -113,48 +113,91 @@ jobs: set -euo pipefail TOTAL_AFFECTED=0 - - echo "" - echo "=== Workflow Cleanup Summary ===" - echo "" - - while IFS= read -r workflow; do + SUMMARY=() + + # Read workflows into an array for indexing + mapfile -t WORKFLOWS_ARRAY <<< "$DELETED_WORKFLOWS" + TOTAL_WORKFLOWS=${#WORKFLOWS_ARRAY[@]} + + # Determine max workflow name length for alignment + MAX_WF_LENGTH=0 + for wf in "${WORKFLOWS_ARRAY[@]}"; do + len=${#wf} + (( len > MAX_WF_LENGTH )) && MAX_WF_LENGTH=$len + done + (( MAX_WF_LENGTH < 40 )) && MAX_WF_LENGTH=40 # minimum width + + # Index column width: enough for [100/100] → 9 chars including brackets + INDEX_WIDTH=9 + + printf " \n" + printf "=== Workflow Cleanup Progress ===\n" + printf "%*s | %-*s | %-14s | %-12s\n" "$INDEX_WIDTH" "Index" "$MAX_WF_LENGTH" "Workflow" "Workflow Count" "Global Total" + printf "%*s-+-%-*s-+-%-14s-+-%-12s\n" "$INDEX_WIDTH" "---------" "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" "--------------" "------------" + + for i in "${!WORKFLOWS_ARRAY[@]}"; do + workflow=${WORKFLOWS_ARRAY[$i]} [ -z "$workflow" ] && continue - - WF_COUNT=0 + CURRENT_INDEX=$((i + 1)) + WORKFLOW_COUNT=0 while true; do + # List workflow runs. + # + # Note that github will return 404 once all runs are deleted. + # We use `|| true` to ignore this error since it is expected when all runs are deleted. RUN_IDS=$(gh run list \ --workflow "$workflow" \ --limit 100 \ --json databaseId \ - --jq '.[].databaseId') + --jq '.[].databaseId' 2>/dev/null || true) - if [ -z "$RUN_IDS" ]; then - break - fi + [ -z "$RUN_IDS" ] && break # stop when no runs remain BATCH_COUNT=$(echo "$RUN_IDS" | wc -l | tr -d ' ') - WF_COUNT=$((WF_COUNT + BATCH_COUNT)) + WORKFLOW_COUNT=$((WORKFLOW_COUNT + BATCH_COUNT)) + TOTAL_AFFECTED=$((TOTAL_AFFECTED + BATCH_COUNT)) + + # Print aligned progress per batch + printf "%*s | %-*s | %14s | %12s\n" \ + "$INDEX_WIDTH" "[$CURRENT_INDEX/$TOTAL_WORKFLOWS]" \ + "$MAX_WF_LENGTH" "$workflow" \ + "$WORKFLOW_COUNT" "$TOTAL_AFFECTED" if [ "$MODE" = "execute" ]; then for RUN_ID in $RUN_IDS; do - gh run delete "$RUN_ID" --yes >/dev/null + gh run delete "$RUN_ID" >/dev/null done fi + + # TEMPORARY BREAK for testing: stop if workflow count > 20 + if [ "$WORKFLOW_COUNT" -gt 20 ]; then + echo "⚠️ Temporary break: workflow count exceeded 20, stopping early for safety." + break + fi done - echo "$workflow → $WF_COUNT runs" - TOTAL_AFFECTED=$((TOTAL_AFFECTED + WF_COUNT)) + # Save summary + SUMMARY+=("$workflow|$WORKFLOW_COUNT") + done + + # Print final aligned summary table + printf " \n" + printf "=== Workflow Cleanup Summary ===\n" + printf "%-*s | %14s\n" "$MAX_WF_LENGTH" "Workflow" "Runs" + printf "%-*s-+-%14s\n" "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" "--------------" - done <<< "$DELETED_WORKFLOWS" + for entry in "${SUMMARY[@]}"; do + wf="${entry%%|*}" + count="${entry##*|}" + printf "%-*s | %14s\n" "$MAX_WF_LENGTH" "$wf" "$count" + done - echo "" - echo "--------------------------------------" - echo "Total runs affected: $TOTAL_AFFECTED" + printf "%-*s-+-%14s\n" "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" "--------------" + printf "%-*s | %14s\n" "$MAX_WF_LENGTH" "TOTAL" "$TOTAL_AFFECTED" if [ "$MODE" = "dry run" ]; then - echo "Dry run complete. No runs were deleted." + printf "Dry run complete. No runs were deleted.\n" else - echo "Cleanup complete." + printf "Cleanup complete.\n" fi From f7cd1fd4f26c3a452e4eb527d6d9c1670dd5e1e3 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:21:17 +0200 Subject: [PATCH 5/9] Table name alignment --- .github/workflows/cleanup-workflows.yml | 98 ++++++++++++++++++++----- 1 file changed, 79 insertions(+), 19 deletions(-) diff --git a/.github/workflows/cleanup-workflows.yml b/.github/workflows/cleanup-workflows.yml index 2cd87ccb56..cc707842b8 100644 --- a/.github/workflows/cleanup-workflows.yml +++ b/.github/workflows/cleanup-workflows.yml @@ -127,14 +127,44 @@ jobs: done (( MAX_WF_LENGTH < 40 )) && MAX_WF_LENGTH=40 # minimum width - # Index column width: enough for [100/100] → 9 chars including brackets + # Column widths INDEX_WIDTH=9 + WORKFLOW_COUNT_WIDTH=14 + GLOBAL_TOTAL_WIDTH=12 - printf " \n" - printf "=== Workflow Cleanup Progress ===\n" - printf "%*s | %-*s | %-14s | %-12s\n" "$INDEX_WIDTH" "Index" "$MAX_WF_LENGTH" "Workflow" "Workflow Count" "Global Total" - printf "%*s-+-%-*s-+-%-14s-+-%-12s\n" "$INDEX_WIDTH" "---------" "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" "--------------" "------------" + # 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} + # Add 2 for the spaces around the 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 "%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 for i in "${!WORKFLOWS_ARRAY[@]}"; do workflow=${WORKFLOWS_ARRAY[$i]} [ -z "$workflow" ] && continue @@ -158,11 +188,12 @@ jobs: WORKFLOW_COUNT=$((WORKFLOW_COUNT + BATCH_COUNT)) TOTAL_AFFECTED=$((TOTAL_AFFECTED + BATCH_COUNT)) - # Print aligned progress per batch - printf "%*s | %-*s | %14s | %12s\n" \ + # Print progress line for this batch + printf "%*s | %-*s | %*s | %*s\n" \ "$INDEX_WIDTH" "[$CURRENT_INDEX/$TOTAL_WORKFLOWS]" \ "$MAX_WF_LENGTH" "$workflow" \ - "$WORKFLOW_COUNT" "$TOTAL_AFFECTED" + "$WORKFLOW_COUNT_WIDTH" "$WORKFLOW_COUNT" \ + "$GLOBAL_TOTAL_WIDTH" "$TOTAL_AFFECTED" if [ "$MODE" = "execute" ]; then for RUN_ID in $RUN_IDS; do @@ -170,9 +201,9 @@ jobs: done fi - # TEMPORARY BREAK for testing: stop if workflow count > 20 + # TEMPORARY BREAK for testing if [ "$WORKFLOW_COUNT" -gt 20 ]; then - echo "⚠️ Temporary break: workflow count exceeded 20, stopping early for safety." + echo " ⚠️ Temporary break: workflow count exceeded 20, stopping early for safety." break fi done @@ -181,23 +212,52 @@ jobs: SUMMARY+=("$workflow|$WORKFLOW_COUNT") done - # Print final aligned summary table + # === Summary Header === printf " \n" - printf "=== Workflow Cleanup Summary ===\n" - printf "%-*s | %14s\n" "$MAX_WF_LENGTH" "Workflow" "Runs" - printf "%-*s-+-%14s\n" "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" "--------------" + print_header "Workflow Cleanup Summary" + + # Summary table uses the same total width as progress table + 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 final summary table for entry in "${SUMMARY[@]}"; do wf="${entry%%|*}" count="${entry##*|}" - printf "%-*s | %14s\n" "$MAX_WF_LENGTH" "$wf" "$count" + printf "%*s | %-*s | %*s | %-*s\n" \ + "$INDEX_WIDTH" "" \ + "$MAX_WF_LENGTH" "$wf" \ + "$WORKFLOW_COUNT_WIDTH" "$count" \ + "$GLOBAL_TOTAL_WIDTH" "" done - printf "%-*s-+-%14s\n" "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" "--------------" - printf "%-*s | %14s\n" "$MAX_WF_LENGTH" "TOTAL" "$TOTAL_AFFECTED" + # 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" "" + + # Final mode message + printf " \n" if [ "$MODE" = "dry run" ]; then - printf "Dry run complete. No runs were deleted.\n" + echo "Dry run complete. No runs were deleted." else - printf "Cleanup complete.\n" + echo "Cleanup complete." fi From 123a4359ed6f51f7471887d37f1552834b621c68 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:13:03 +0200 Subject: [PATCH 6/9] Fix dry-run pagination --- .github/workflows/cleanup-workflows.yml | 127 +++++++++++------------- 1 file changed, 58 insertions(+), 69 deletions(-) diff --git a/.github/workflows/cleanup-workflows.yml b/.github/workflows/cleanup-workflows.yml index cc707842b8..f19b6065e4 100644 --- a/.github/workflows/cleanup-workflows.yml +++ b/.github/workflows/cleanup-workflows.yml @@ -109,6 +109,8 @@ jobs: 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 @@ -119,13 +121,24 @@ jobs: 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 "${WORKFLOWS_ARRAY[@]}"; do + for wf in "${WORKFLOW_NAMES_ARRAY[@]}"; do len=${#wf} (( len > MAX_WF_LENGTH )) && MAX_WF_LENGTH=$len done - (( MAX_WF_LENGTH < 40 )) && MAX_WF_LENGTH=40 # minimum width + (( MAX_WF_LENGTH < 30 )) && MAX_WF_LENGTH=30 # minimum width # Column widths INDEX_WIDTH=9 @@ -139,15 +152,12 @@ jobs: print_header() { local name="$1" local name_len=${#name} - # Add 2 for the spaces around the 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 "%s %s %s\n" "$left_str" "$name" "$right_str" + printf "\n%s %s %s\n" "$left_str" "$name" "$right_str" } # === Progress Header === @@ -157,107 +167,86 @@ jobs: "$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 - for i in "${!WORKFLOWS_ARRAY[@]}"; do - workflow=${WORKFLOWS_ARRAY[$i]} - [ -z "$workflow" ] && continue + # Process each workflow by name + for i in "${!WORKFLOW_NAMES_ARRAY[@]}"; do + + WORKFLOW_NAME=${WORKFLOW_NAMES_ARRAY[$i]} + [ -z "$WORKFLOW_NAME" ] && continue CURRENT_INDEX=$((i + 1)) WORKFLOW_COUNT=0 - while true; do - # List workflow runs. - # - # Note that github will return 404 once all runs are deleted. - # We use `|| true` to ignore this error since it is expected when all runs are deleted. - RUN_IDS=$(gh run list \ - --workflow "$workflow" \ - --limit 100 \ - --json databaseId \ - --jq '.[].databaseId' 2>/dev/null || true) + WORKFLOW_ID=$(gh workflow list --all --json id,name \ + --jq ".[] | select(.name==\"$WORKFLOW_NAME\") | .id") - [ -z "$RUN_IDS" ] && break # stop when no runs remain + # 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 for this batch + # Print progress line printf "%*s | %-*s | %*s | %*s\n" \ "$INDEX_WIDTH" "[$CURRENT_INDEX/$TOTAL_WORKFLOWS]" \ - "$MAX_WF_LENGTH" "$workflow" \ + "$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 + echo | gh run delete "$RUN_ID" >/dev/null done fi - # TEMPORARY BREAK for testing - if [ "$WORKFLOW_COUNT" -gt 20 ]; then - echo " ⚠️ Temporary break: workflow count exceeded 20, stopping early for safety." + [ "$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 - # Save summary - SUMMARY+=("$workflow|$WORKFLOW_COUNT") + SUMMARY+=("$WORKFLOW_NAME|$WORKFLOW_COUNT") done # === Summary Header === - printf " \n" print_header "Workflow Cleanup Summary" - - # Summary table uses the same total width as progress table 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 final summary table - 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" "" - - # Final mode message - printf " \n" - if [ "$MODE" = "dry run" ]; then - echo "Dry run complete. No runs were deleted." - else - echo "Cleanup complete." - fi + "$WORKFLOW_COUNT_WIDT From 2d406f13b6ffcd335561afc72e45e8b6e4aceed7 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:37:17 +0200 Subject: [PATCH 7/9] Debugging --- .github/workflows/cleanup-workflows.yml | 188 +++++++++++++----------- 1 file changed, 101 insertions(+), 87 deletions(-) diff --git a/.github/workflows/cleanup-workflows.yml b/.github/workflows/cleanup-workflows.yml index f19b6065e4..07a071cc2e 100644 --- a/.github/workflows/cleanup-workflows.yml +++ b/.github/workflows/cleanup-workflows.yml @@ -55,52 +55,45 @@ jobs: echo "EOF" } >> "$GITHUB_OUTPUT" - - name: Workflows on github - id: github + - name: Filter for deleted workflows + id: deleted 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<> "$GITHUB_OUTPUT" + set -euo pipefail - - name: Filter for deleted workflows - id: deleted - run: | - # Union of `main` and `next` workflows. - EXISTING_FILES=$( \ - printf "%s\n%s\n" \ + # 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_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 }}" \ + 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)) + ' ) - DELETED_FILES=$(echo "$DELETED_FILES" | sort | uniq -u) - printf "%s\n" $DELETED_FILES + + echo "Deleted workflows:" + echo "$DELETED" + + # Output to GitHub Actions { echo "workflows<> "$GITHUB_OUTPUT" @@ -108,46 +101,31 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} MODE: ${{ inputs.mode }} - DELETED_WORKFLOWS: ${{ steps.deleted.outputs.workflows }} + 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=() - - # 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 + CURRENT_INDEX=0 # Column widths INDEX_WIDTH=9 + MAX_WF_LENGTH=30 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 )) + # 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" @@ -173,40 +151,44 @@ jobs: "$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 + # 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') - WORKFLOW_NAME=${WORKFLOW_NAMES_ARRAY[$i]} - [ -z "$WORKFLOW_NAME" ] && continue - CURRENT_INDEX=$((i + 1)) - WORKFLOW_COUNT=0 - - WORKFLOW_ID=$(gh workflow list --all --json id,name \ - --jq ".[] | select(.name==\"$WORKFLOW_NAME\") | .id") + # 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 - # GraphQL pagination variables - AFTER_CURSOR=null + 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 - } + 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') + + 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 @@ -214,7 +196,7 @@ jobs: WORKFLOW_COUNT=$((WORKFLOW_COUNT + BATCH_COUNT)) TOTAL_AFFECTED=$((TOTAL_AFFECTED + BATCH_COUNT)) - # Print progress line + # Print progress printf "%*s | %-*s | %*s | %*s\n" \ "$INDEX_WIDTH" "[$CURRENT_INDEX/$TOTAL_WORKFLOWS]" \ "$MAX_WF_LENGTH" "$WORKFLOW_NAME" \ @@ -223,13 +205,13 @@ jobs: if [ "$MODE" = "execute" ]; then for RUN_ID in $RUN_IDS; do - echo | gh run delete "$RUN_ID" >/dev/null + gh run delete "$RUN_ID" >/dev/null done fi [ "$HAS_NEXT" != "true" ] && break - # TEMPORARY break for testing large workflows + # TEMPORARY safety break if [ "$WORKFLOW_COUNT" -gt 200 ]; then echo " ⚠️ Temporary break: workflow count exceeded 200, stopping early." break @@ -249,4 +231,36 @@ jobs: 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 + "$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 From b257f7c1bd591407f25d860d5b7749892c6dc5bd Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:17:01 +0200 Subject: [PATCH 8/9] Remove temp break --- .github/workflows/cleanup-workflows.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cleanup-workflows.yml b/.github/workflows/cleanup-workflows.yml index 07a071cc2e..0446229053 100644 --- a/.github/workflows/cleanup-workflows.yml +++ b/.github/workflows/cleanup-workflows.yml @@ -97,6 +97,9 @@ jobs: 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 }} @@ -174,6 +177,9 @@ jobs: # 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) { @@ -210,12 +216,6 @@ jobs: 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") From 87ae03af45882296a7fa688f4d9436bb56d0f6e6 Mon Sep 17 00:00:00 2001 From: Mirko von Leipzig <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Mon, 16 Feb 2026 19:36:00 +0200 Subject: [PATCH 9/9] Refactor into functions --- .github/workflows/cleanup-workflows.yml | 248 +++++++++++++----------- 1 file changed, 133 insertions(+), 115 deletions(-) diff --git a/.github/workflows/cleanup-workflows.yml b/.github/workflows/cleanup-workflows.yml index 0446229053..a7a6d2b428 100644 --- a/.github/workflows/cleanup-workflows.yml +++ b/.github/workflows/cleanup-workflows.yml @@ -107,159 +107,177 @@ jobs: WORKFLOWS: ${{ steps.deleted.outputs.workflows }} OWNER: ${{ github.repository_owner }} REPO: ${{ github.repository }} + shell: bash --noprofile --norc -euo pipefail {0} 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" + # ================================================================================================ + # Utility functions + # ================================================================================================ + + # Fetches a page of workflow runs for a given workflow ID and cursor. + # + # 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. + gh_workflow_run_page() { + local id="$1" + local cursor="$2" + + gh api graphql -F workflowId="$id" -F after="$cursor" \ + -f query='query($workflowId: ID!, $after: String) { + node(id: $workflowId) { + ... on Workflow { + runs(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { databaseId } + } + } + } + }' + } + + # ================================================================================================ + # Print helpers for nice progress and table display + # ================================================================================================ + + # Column widths (table includes three spacers for ' | ' between columns) + widths_index=9 + widths_name=30 + widths_count=14 + widths_total=12 + widths_table=$(( $widths_index + 3 + $widths_name + 3 + $widths_count + 3 + $widths_total )) + + # Repeats a character a given number of times. + repeat_char() { + local char=$1 + local count=$2 + printf "%0.s$char" $(seq 1 $count) + } + + # Prints the given header as `====
====` to match the table layout. + print_table_header() { + local header="$1" + local header_len=${#header} + local left_pad=$(( ( $widths_table - header_len - 2) / 2 )) + local right_pad=$(( $widths_table - header_len - 2 - left_pad )) + printf " \n%s %s %s\n" $(repeat_char = $left_pad) "$header" $(repeat_char = $right_pad) + } + + # Prints |---+---+---+---| with appropriate widths to accomodate the table headers. + print_table_separator() { + printf "%s+%s+%s+%s\n" \ + "$(repeat_char - $((widths_index + 1)))" \ + "$(repeat_char - $((widths_name + 2)))" \ + "$(repeat_char - $((widths_count + 2)))" \ + "$(repeat_char - $((widths_total + 1)))" + } + + # Prints a row of the table (index, workflow name, workflow count, global total) + print_table_row() { + local index=$1 + local name=$2 + local count=$3 + local total=$4 + printf "%*s | %-*s | %*s | %*s\n" \ + "$widths_index" "$index" \ + "$widths_name" "$name" \ + "$widths_count" "$count" \ + "$widths_total" "$total" + } + + # Alias for print_table_row() with empty index and total columns. + print_summary_row() { + local name=$1 + local count=$2 + print_table_row "" "$name" "$count" "" } - # === 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 + # ================================================================================================ + # Print progress table header + # ================================================================================================ + print_table_header "Workflow Cleanup Progress" + print_table_row "Index" "Workflow" "Workflow Count" "Global Total" + print_table_separator + + # ================================================================================================ + # Core workflow loop, iterate over workflows + # ================================================================================================ + + n_workflows=$(echo "$WORKFLOWS" | jq -r '. | length') + total=0 + summary=() + index=0 + 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') + index=$((index + 1)) + name=$(echo "$wf" | jq -r '.name') + count=0 + id=$(echo "$wf" | jq -r '.node_id') # Safety checks - if [ -z "$WORKFLOW_NAME" ]; then - echo "::error title=Workflow name empty::Resolved workflow name is empty at index $CURRENT_INDEX" + if [ -z "$name" ]; then + echo "::error title=Workflow name empty::Resolved workflow name is empty at index $index" exit 1 fi - if [ -z "$WORKFLOW_ID" ]; then - echo "::error title=Workflow ID missing::Workflow '$WORKFLOW_NAME' (path: $WORKFLOW_PATH) has no ID" + if [ -z "$id" ]; then + echo "::error title=Workflow ID missing::Workflow '$name' has no ID" exit 1 fi - WORKFLOW_COUNT=0 - AFTER_CURSOR="" + 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 } - } - } - } - }') + response=$(gh_workflow_run_page "$id" "$cursor") - 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') + run_ids=$(echo "$response" | jq -r '.data.node.runs.nodes[].databaseId') + has_next=$(echo "$response" | jq -r '.data.node.runs.pageInfo.hasNextPage') + cursor=$(echo "$response" | jq -r '.data.node.runs.pageInfo.endCursor') - [ -z "$RUN_IDS" ] && break + [ -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)) + deleted=$(echo "$run_ids" | wc -l | tr -d ' ') + count=$((count + deleted)) + total=$((total + deleted)) # 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" + print_table_row "[$index/$n_workflows]" "$name" "$count" "$total" if [ "$MODE" = "execute" ]; then - for RUN_ID in $RUN_IDS; do - gh run delete "$RUN_ID" >/dev/null + for run_id in $run_ids; do + gh run delete "$run_id" >/dev/null done fi - [ "$HAS_NEXT" != "true" ] && break + [ "$has_next" != "true" ] && break done - SUMMARY+=("$WORKFLOW_NAME|$WORKFLOW_COUNT") + summary+=("$name|$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 + # ================================================================================================ + # Print a summary table + # ================================================================================================ + print_table_header "Workflow Cleanup Summary" + print_summary_row "Workflow" "Runs" + print_table_separator + 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" "" + print_summary_row "$wf" "$count" 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 + # ================================================================================================ + # Print totals as a footer + # ================================================================================================ + print_table_separator + print_summary_row "TOTAL" "$total" + + if [ "$MODE" != "execute" ]; then echo "Dry run complete. No runs were deleted." else echo "Cleanup complete."