@@ -55,99 +55,76 @@ 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=$(echo "$EXISTING" | sort -u | jq -R . | jq -s .)
71+
72+ echo "Existing workflows:"
73+ echo "$EXISTING"
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 -c '[.[] | 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=$(echo "$GITHUB" | jq -c \
84+ --argjson existing "$EXISTING" '
85+ map(select(.path as $p | $existing | index($p) | not))
86+ '
9887 )
99- DELETED_FILES=$(echo "$DELETED_FILES" | sort | uniq -u)
100- printf "%s\n" $DELETED_FILES
88+
89+ echo "Deleted workflows:"
90+ echo "$DELETED"
91+
92+ # Output to GitHub Actions
10193 {
10294 echo "workflows<<EOF"
103- echo "$DELETED_FILES "
95+ echo "$DELETED "
10496 echo "EOF"
10597 } >> "$GITHUB_OUTPUT"
10698
10799 - name : Delete runs from deleted workflows
108100 env :
109101 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
110102 MODE : ${{ inputs.mode }}
111- DELETED_WORKFLOWS : ${{ steps.deleted.outputs.workflows }}
103+ WORKFLOWS : ${{ steps.deleted.outputs.workflows }}
112104 OWNER : ${{ github.repository_owner }}
113105 REPO : ${{ github.repository }}
114106 run : |
115107 set -euo pipefail
116108
109+ if [ -z "$WORKFLOWS" ]; then
110+ echo "No workflows to delete."
111+ exit 0
112+ fi
113+
117114 TOTAL_AFFECTED=0
118115 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
116+ CURRENT_INDEX=0
142117
143118 # Column widths
144119 INDEX_WIDTH=9
120+ MAX_WF_LENGTH=30
145121 WORKFLOW_COUNT_WIDTH=14
146122 GLOBAL_TOTAL_WIDTH=12
147-
148- # Total table width
149123 TOTAL_WIDTH=$(( INDEX_WIDTH + 3 + MAX_WF_LENGTH + 3 + WORKFLOW_COUNT_WIDTH + 3 + GLOBAL_TOTAL_WIDTH ))
150124
125+ # Count total workflows for progress display
126+ TOTAL_WORKFLOWS=$(echo "$WORKFLOWS" | jq -r '. | length')
127+
151128 # Function to print dynamic table headers padded with '=' and spaces
152129 print_header() {
153130 local name="$1"
@@ -173,48 +150,51 @@ jobs:
173150 "$WORKFLOW_COUNT_WIDTH" "$(printf '%.0s-' $(seq 1 $WORKFLOW_COUNT_WIDTH))" \
174151 "$GLOBAL_TOTAL_WIDTH" "$(printf '%.0s-' $(seq 1 $GLOBAL_TOTAL_WIDTH))"
175152
176- # Process each workflow by name
177- for i in "${!WORKFLOW_NAMES_ARRAY[@]}"; do
153+ # Loop over deleted workflows JSON
154+ echo "$WORKFLOWS" | jq -c '.[]' | while read -r wf; do
155+ CURRENT_INDEX=$((CURRENT_INDEX + 1))
156+ WORKFLOW_NAME=$(echo "$wf" | jq -r '.name')
157+ WORKFLOW_ID=$(echo "$wf" | jq -r '.databaseId')
158+ WORKFLOW_PATH=$(echo "$wf" | jq -r '.path')
178159
179- WORKFLOW_NAME=${WORKFLOW_NAMES_ARRAY[$i]}
180- [ -z "$WORKFLOW_NAME" ] && continue
181- CURRENT_INDEX=$((i + 1))
182- WORKFLOW_COUNT=0
183-
184- WORKFLOW_ID=$(gh workflow list --all --json id,name \
185- --jq ".[] | select(.name==\"$WORKFLOW_NAME\") | .id")
160+ # Safety checks
161+ if [ -z "$WORKFLOW_NAME" ]; then
162+ echo "::error title=Workflow name empty::Resolved workflow name is empty at index $CURRENT_INDEX"
163+ exit 1
164+ fi
165+ if [ -z "$WORKFLOW_ID" ]; then
166+ echo "::error title=Workflow ID missing::Workflow '$WORKFLOW_NAME' (path: $WORKFLOW_PATH) has no ID"
167+ exit 1
168+ fi
186169
187- # GraphQL pagination variables
188- AFTER_CURSOR=null
170+ WORKFLOW_COUNT=0
171+ AFTER_CURSOR=""
189172
173+ # Paginate over workflow runs
190174 while true; do
191175 RESPONSE=$(gh api graphql -F workflowId="$WORKFLOW_ID" -F after="$AFTER_CURSOR" \
192176 -f query='query($workflowId: ID!, $after: String) {
193177 node(id: $workflowId) {
194178 ... on Workflow {
195179 runs(first: 100, after: $after) {
196- pageInfo {
197- hasNextPage
198- endCursor
199- }
200- nodes {
201- databaseId
202- }
180+ pageInfo { hasNextPage endCursor }
181+ nodes { databaseId }
203182 }
204183 }
205184 }
206185 }')
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')
186+
187+ RUN_IDS=$(echo "$RESPONSE" | jq -r '.data.node.runs.nodes[].databaseId')
188+ HAS_NEXT=$(echo "$RESPONSE" | jq -r '.data.node.runs.pageInfo.hasNextPage')
189+ AFTER_CURSOR=$(echo "$RESPONSE" | jq -r '.data.node.runs.pageInfo.endCursor')
210190
211191 [ -z "$RUN_IDS" ] && break
212192
213193 BATCH_COUNT=$(echo "$RUN_IDS" | wc -l | tr -d ' ')
214194 WORKFLOW_COUNT=$((WORKFLOW_COUNT + BATCH_COUNT))
215195 TOTAL_AFFECTED=$((TOTAL_AFFECTED + BATCH_COUNT))
216196
217- # Print progress line
197+ # Print progress
218198 printf "%*s | %-*s | %*s | %*s\n" \
219199 "$INDEX_WIDTH" "[$CURRENT_INDEX/$TOTAL_WORKFLOWS]" \
220200 "$MAX_WF_LENGTH" "$WORKFLOW_NAME" \
@@ -223,13 +203,13 @@ jobs:
223203
224204 if [ "$MODE" = "execute" ]; then
225205 for RUN_ID in $RUN_IDS; do
226- echo | gh run delete "$RUN_ID" >/dev/null
206+ gh run delete "$RUN_ID" >/dev/null
227207 done
228208 fi
229209
230210 [ "$HAS_NEXT" != "true" ] && break
231211
232- # TEMPORARY break for testing large workflows
212+ # TEMPORARY safety break
233213 if [ "$WORKFLOW_COUNT" -gt 200 ]; then
234214 echo " ⚠️ Temporary break: workflow count exceeded 200, stopping early."
235215 break
@@ -249,4 +229,36 @@ jobs:
249229 printf "%*s-+-%-*s-+-%-*s-+-%-*s\n" \
250230 "$INDEX_WIDTH" "$(printf '%.0s-' $(seq 1 $INDEX_WIDTH))" \
251231 "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" \
252- "$WORKFLOW_COUNT_WIDT
232+ "$WORKFLOW_COUNT_WIDTH" "$(printf '%.0s-' $(seq 1 $WORKFLOW_COUNT_WIDTH))" \
233+ "$GLOBAL_TOTAL_WIDTH" "$(printf '%.0s-' $(seq 1 $GLOBAL_TOTAL_WIDTH))"
234+
235+ # Print summary rows
236+ for entry in "${SUMMARY[@]}"; do
237+ wf="${entry%%|*}"
238+ count="${entry##*|}"
239+ printf "%*s | %-*s | %*s | %-*s\n" \
240+ "$INDEX_WIDTH" "" \
241+ "$MAX_WF_LENGTH" "$wf" \
242+ "$WORKFLOW_COUNT_WIDTH" "$count" \
243+ "$GLOBAL_TOTAL_WIDTH" ""
244+ done
245+
246+ # Footer separator
247+ printf "%*s-+-%-*s-+-%-*s-+-%-*s\n" \
248+ "$INDEX_WIDTH" "$(printf '%.0s-' $(seq 1 $INDEX_WIDTH))" \
249+ "$MAX_WF_LENGTH" "$(printf '%.0s-' $(seq 1 $MAX_WF_LENGTH))" \
250+ "$WORKFLOW_COUNT_WIDTH" "$(printf '%.0s-' $(seq 1 $WORKFLOW_COUNT_WIDTH))" \
251+ "$GLOBAL_TOTAL_WIDTH" "$(printf '%.0s-' $(seq 1 $GLOBAL_TOTAL_WIDTH))"
252+
253+ # TOTAL row
254+ printf "%*s | %-*s | %*s | %-*s\n" \
255+ "$INDEX_WIDTH" "" \
256+ "$MAX_WF_LENGTH" "TOTAL" \
257+ "$WORKFLOW_COUNT_WIDTH" "$TOTAL_AFFECTED" \
258+ "$GLOBAL_TOTAL_WIDTH" ""
259+
260+ if [ "$MODE" = "dry run" ]; then
261+ echo "Dry run complete. No runs were deleted."
262+ else
263+ echo "Cleanup complete."
264+ fi
0 commit comments