@@ -55,99 +55,77 @@ jobs:
5555 echo "EOF"
5656 } >> "$GITHUB_OUTPUT"
5757
58- - name : Workflows on github
59- id : github
58+ - name : Filter for deleted workflows
59+ id : deleted
6060 env :
6161 GH_TOKEN : ${{ github.token }}
6262 run : |
63- # Note that we filter by `.github` path prefix to ensure we only get locally defined workflows.
64- #
65- # Examples of non-local workflows are `dependabot` and `copilot` which have paths:
66- # - dynamic/dependabot/dependabot-updates
67- # - dynamic/copilot-pull-request-reviewer/copilot-pull-request-reviewer
68- WORKFLOWS=$(gh workflow list \
69- --all \
70- --json path \
71- --jq '.[] | select(.path | startswith(".github")) | .path' \
72- )
73- printf "%s\n" $WORKFLOWS
74- {
75- echo "workflows<<EOF"
76- echo "$WORKFLOWS"
77- echo "EOF"
78- } >> "$GITHUB_OUTPUT"
63+ set -euo pipefail
7964
80- - name : Filter for deleted workflows
81- id : deleted
82- run : |
83- # Union of `main` and `next` workflows.
84- EXISTING_FILES=$( \
85- printf "%s\n%s\n" \
65+ # Union of `main` and `next` workflows as a JSON array of strings (paths)
66+ EXISTING=$(printf "%s\n%s\n" \
8667 "${{ steps.main.outputs.workflows }}" \
8768 "${{ steps.next.outputs.workflows }}" \
8869 )
89- EXISTING_FILES=$(echo "$EXISTING_FILES" | sort -u)
90- printf "%s\n" $EXISTING_FILES
91-
92- # Find deleted workflows as the items in `WORKFLOWS` but not in the union of main and next.
93- # This assumes that _all_ items in main and next are present in `WORKFLOWS`.
94- DELETED_FILES=$( \
95- printf "%s\n%s\n" \
96- "$EXISTING_FILES" \
97- "${{ steps.github.outputs.workflows }}" \
70+ EXISTING_JSON=$(echo "$EXISTING" | sort -u | jq -R . | jq -s .)
71+
72+ echo "Existing workflows:"
73+ echo "$EXISTING_JSON"
74+
75+ # Get workflows currently on GitHub as JSON array of objects
76+ GITHUB=$(gh workflow list --all --json path,name,id)
77+ GITHUB=$(echo "$GITHUB" | jq '[.[] | select(.path | startswith(".github/"))]')
78+
79+ echo "Workflows on GitHub:"
80+ echo "$GITHUB"
81+
82+ # Find deleted workflows: present on GitHub but not in main/next
83+ DELETED=$(jq -c \
84+ --argjson github "$GITHUB" \
85+ --argjson existing "$EXISTING" '
86+ $github | map(select(.path as $p | $existing | index($p) | not))
87+ '
9888 )
99- DELETED_FILES=$(echo "$DELETED_FILES" | sort | uniq -u)
100- printf "%s\n" $DELETED_FILES
89+
90+ echo "Deleted workflows:"
91+ echo "$DELETED"
92+
93+ # Output to GitHub Actions
10194 {
10295 echo "workflows<<EOF"
103- echo "$DELETED_FILES "
96+ echo "$DELETED "
10497 echo "EOF"
10598 } >> "$GITHUB_OUTPUT"
10699
107100 - name : Delete runs from deleted workflows
108101 env :
109102 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
110103 MODE : ${{ inputs.mode }}
111- DELETED_WORKFLOWS : ${{ steps.deleted.outputs.workflows }}
104+ WORKFLOWS : ${{ steps.deleted.outputs.workflows }}
112105 OWNER : ${{ github.repository_owner }}
113106 REPO : ${{ github.repository }}
114107 run : |
115108 set -euo pipefail
116109
110+ if [[ -z "$WORKFLOWS" ]]; then
111+ echo "No workflows to delete."
112+ exit 0
113+ fi
114+
117115 TOTAL_AFFECTED=0
118116 SUMMARY=()
119-
120- # Read workflows into an array for indexing
121- mapfile -t WORKFLOWS_ARRAY <<< "$DELETED_WORKFLOWS"
122- TOTAL_WORKFLOWS=${#WORKFLOWS_ARRAY[@]}
123-
124- # Convert workflow paths to workflow names
125- WORKFLOW_NAMES_ARRAY=()
126- for wf_path in "${WORKFLOWS_ARRAY[@]}"; do
127- # Extract 'name' from YAML, fallback to filename
128- name=$(yq -r '.name // ""' "$wf_path" 2>/dev/null || true)
129- if [ -z "$name" ]; then
130- name=$(basename "$wf_path")
131- fi
132- WORKFLOW_NAMES_ARRAY+=("$name")
133- done
134-
135- # Determine max workflow name length for alignment
136- MAX_WF_LENGTH=0
137- for wf in "${WORKFLOW_NAMES_ARRAY[@]}"; do
138- len=${#wf}
139- (( len > MAX_WF_LENGTH )) && MAX_WF_LENGTH=$len
140- done
141- (( MAX_WF_LENGTH < 30 )) && MAX_WF_LENGTH=30 # minimum width
117+ CURRENT_INDEX=0
142118
143119 # Column widths
144120 INDEX_WIDTH=9
121+ MAX_WF_LENGTH=30
145122 WORKFLOW_COUNT_WIDTH=14
146123 GLOBAL_TOTAL_WIDTH=12
147-
148- # Total table width
149124 TOTAL_WIDTH=$(( INDEX_WIDTH + 3 + MAX_WF_LENGTH + 3 + WORKFLOW_COUNT_WIDTH + 3 + GLOBAL_TOTAL_WIDTH ))
150125
126+ # Count total workflows for progress display
127+ TOTAL_WORKFLOWS=$(echo "$WORKFLOWS" | jq -r '. | length')
128+
151129 # Function to print dynamic table headers padded with '=' and spaces
152130 print_header() {
153131 local name="$1"
@@ -173,48 +151,51 @@ jobs:
173151 "$WORKFLOW_COUNT_WIDTH" "$(printf '%.0s-' $(seq 1 $WORKFLOW_COUNT_WIDTH))" \
174152 "$GLOBAL_TOTAL_WIDTH" "$(printf '%.0s-' $(seq 1 $GLOBAL_TOTAL_WIDTH))"
175153
176- # Process each workflow by name
177- for i in "${!WORKFLOW_NAMES_ARRAY[@]}"; do
154+ # Loop over deleted workflows JSON
155+ echo "$WORKFLOWS" | jq -c '.[]' | while read -r wf; do
156+ CURRENT_INDEX=$((CURRENT_INDEX + 1))
157+ WORKFLOW_NAME=$(echo "$wf" | jq -r '.name')
158+ WORKFLOW_ID=$(echo "$wf" | jq -r '.databaseId')
159+ WORKFLOW_PATH=$(echo "$wf" | jq -r '.path')
160+
161+ # Safety checks
162+ if [ -z "$WORKFLOW_NAME" ]; then
163+ echo "::error title=Workflow name empty::Resolved workflow name is empty at index $CURRENT_INDEX"
164+ exit 1
165+ fi
166+ if [ -z "$WORKFLOW_ID" ]; then
167+ echo "::error title=Workflow ID missing::Workflow '$WORKFLOW_NAME' (path: $WORKFLOW_PATH) has no ID"
168+ exit 1
169+ fi
178170
179- WORKFLOW_NAME=${WORKFLOW_NAMES_ARRAY[$i]}
180- [ -z "$WORKFLOW_NAME" ] && continue
181- CURRENT_INDEX=$((i + 1))
182171 WORKFLOW_COUNT=0
183-
184- WORKFLOW_ID=$(gh workflow list --all --json id,name \
185- --jq ".[] | select(.name==\"$WORKFLOW_NAME\") | .id")
186-
187- # GraphQL pagination variables
188172 AFTER_CURSOR=null
189173
174+ # Paginate over workflow runs
190175 while true; do
191176 RESPONSE=$(gh api graphql -F workflowId="$WORKFLOW_ID" -F after="$AFTER_CURSOR" \
192177 -f query='query($workflowId: ID!, $after: String) {
193178 node(id: $workflowId) {
194179 ... on Workflow {
195180 runs(first: 100, after: $after) {
196- pageInfo {
197- hasNextPage
198- endCursor
199- }
200- nodes {
201- databaseId
202- }
181+ pageInfo { hasNextPage endCursor }
182+ nodes { databaseId }
203183 }
204184 }
205185 }
206186 }')
207- RUN_IDS=$(echo "$RESPONSE" | jq -r '.data.repository.workflowRuns.nodes[].databaseId')
208- HAS_NEXT=$(echo "$RESPONSE" | jq -r '.data.repository.workflowRuns.pageInfo.hasNextPage')
209- AFTER_CURSOR=$(echo "$RESPONSE" | jq -r '.data.repository.workflowRuns.pageInfo.endCursor')
187+
188+ RUN_IDS=$(echo "$RESPONSE" | jq -r '.data.node.runs.nodes[].databaseId')
189+ HAS_NEXT=$(echo "$RESPONSE" | jq -r '.data.node.runs.pageInfo.hasNextPage')
190+ AFTER_CURSOR=$(echo "$RESPONSE" | jq -r '.data.node.runs.pageInfo.endCursor')
210191
211192 [ -z "$RUN_IDS" ] && break
212193
213194 BATCH_COUNT=$(echo "$RUN_IDS" | wc -l | tr -d ' ')
214195 WORKFLOW_COUNT=$((WORKFLOW_COUNT + BATCH_COUNT))
215196 TOTAL_AFFECTED=$((TOTAL_AFFECTED + BATCH_COUNT))
216197
217- # Print progress line
198+ # Print progress
218199 printf "%*s | %-*s | %*s | %*s\n" \
219200 "$INDEX_WIDTH" "[$CURRENT_INDEX/$TOTAL_WORKFLOWS]" \
220201 "$MAX_WF_LENGTH" "$WORKFLOW_NAME" \
@@ -223,30 +204,15 @@ jobs:
223204
224205 if [ "$MODE" = "execute" ]; then
225206 for RUN_ID in $RUN_IDS; do
226- echo | gh run delete "$RUN_ID" >/dev/null
207+ gh run delete "$RUN_ID" >/dev/null
227208 done
228209 fi
229210
230211 [ "$HAS_NEXT" != "true" ] && break
231212
232- # TEMPORARY break for testing large workflows
213+ # TEMPORARY safety break
233214 if [ "$WORKFLOW_COUNT" -gt 200 ]; then
234215 echo " ⚠️ Temporary break: workflow count exceeded 200, stopping early."
235216 break
236217 fi
237218 done
238-
239- SUMMARY+=("$WORKFLOW_NAME|$WORKFLOW_COUNT")
240- done
241-
242- # === Summary Header ===
243- print_header "Workflow Cleanup Summary"
244- printf "%*s | %-*s | %-*s | %-*s\n" \
245- "$INDEX_WIDTH" "" \
246- "$MAX_WF_LENGTH" "Workflow" \
247- "$WORKFLOW_COUNT_WIDTH" "Runs" \
248- "$GLOBAL_TOTAL_WIDTH" ""
249- printf "%*s-+-%-*s-+-%-*s-+-%-*s\n" \
250- "$INDEX_WIDTH" "$(printf '%.0s-' $(seq 1 $INDEX_WIDTH))" \
251- "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" \
252- "$WORKFLOW_COUNT_WIDT
0 commit comments