Skip to content

Commit 6419753

Browse files
Copilotrjaegers
andcommitted
Refactor: make image deletion leading, then clean orphaned attestations
Co-authored-by: rjaegers <45816308+rjaegers@users.noreply.github.com>
1 parent bb9a228 commit 6419753

2 files changed

Lines changed: 118 additions & 79 deletions

File tree

.github/workflows/image-cleanup.yml

Lines changed: 62 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,88 +9,43 @@ on:
99
permissions: {}
1010

1111
jobs:
12-
cleanup-attestations:
13-
name: 🔏 Cleanup Attestations (${{ matrix.package }})
12+
collect-digests:
13+
name: 📦 Collect Digests (${{ matrix.package }})
1414
runs-on: ubuntu-latest
15-
permissions:
16-
attestations: write # is needed to delete attestations
17-
packages: read # is needed to list package versions to find digests
18-
pull-requests: read # is needed to determine which pull requests are open
1915
strategy:
2016
fail-fast: false
2117
matrix:
2218
package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust]
19+
permissions:
20+
packages: read # is needed to list package versions
2321
steps:
2422
- uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
2523
with:
2624
disable-sudo-and-containers: true
2725
egress-policy: audit
2826
allowed-endpoints: api.github.com:443
29-
- name: Delete outdated attestations
27+
- name: Collect package digests
3028
run: |
3129
set -Eeuo pipefail
32-
3330
ORG="${GH_REPO%%/*}"
34-
35-
# Get all open PR numbers to determine which pr-N tags to keep
36-
open_pr_numbers=$(gh api "/repos/${GH_REPO}/pulls?state=open&per_page=100" \
31+
gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \
3732
--paginate \
38-
--jq '.[].number' 2>/dev/null || true)
39-
40-
# Get all package versions with their digests and tags
41-
while IFS=$'\t' read -r digest tags_csv; do
42-
[[ -z "$digest" ]] && continue
43-
44-
keep=false
45-
46-
# Check each tag to determine if this digest should be kept
47-
IFS=',' read -r -a tags <<< "$tags_csv"
48-
for tag in "${tags[@]}"; do
49-
[[ -z "$tag" ]] && continue
50-
51-
# Keep release semver tags (e.g., v6.5.2, 6.5.2, 6.5, 6)
52-
if [[ "$tag" =~ ^v?[0-9]+(\.[0-9]+)*$ ]]; then
53-
keep=true
54-
break
55-
fi
56-
57-
# Keep edge tag
58-
if [[ "$tag" == "edge" ]]; then
59-
keep=true
60-
break
61-
fi
62-
63-
# Keep pr-N tags for open pull requests
64-
if [[ "$tag" =~ ^pr-([0-9]+)$ ]]; then
65-
pr_number="${BASH_REMATCH[1]}"
66-
if echo "$open_pr_numbers" | grep -qx "$pr_number"; then
67-
keep=true
68-
break
69-
fi
70-
fi
71-
done
72-
73-
if [[ "$keep" == "false" ]]; then
74-
echo "Deleting attestations for ${GH_PACKAGE}@${digest} (tags: ${tags_csv})"
75-
encoded_digest="${digest//:/%3A}"
76-
gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \
77-
2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)"
78-
fi
79-
done < <(
80-
gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \
81-
--paginate \
82-
--jq '.[] | "\(.name)\t\(.metadata.container.tags // [] | join(","))"' \
83-
2>/dev/null || true
84-
)
33+
--jq '.[].name' 2>/dev/null > digests.txt || touch digests.txt
8534
env:
8635
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8736
GH_REPO: ${{ github.repository }}
8837
GH_PACKAGE: ${{ matrix.package }}
38+
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
39+
with:
40+
name: digests-before-cleanup-${{ matrix.package }}
41+
path: digests.txt
42+
if-no-files-found: warn
43+
retention-days: 1
8944

9045
cleanup-images:
9146
name: 🧹 Clean Images
9247
if: always()
93-
needs: cleanup-attestations
48+
needs: collect-digests
9449
runs-on: ubuntu-latest
9550
permissions:
9651
packages: write # is needed by dataaxiom/ghcr-cleanup-action to delete untagged and orphaned images
@@ -106,3 +61,51 @@ jobs:
10661
delete-orphaned-images: true
10762
delete-untagged: true
10863
packages: amp-devcontainer,amp-devcontainer-cpp,amp-devcontainer-rust
64+
65+
cleanup-attestations:
66+
name: 🔏 Cleanup Orphaned Attestations (${{ matrix.package }})
67+
needs: cleanup-images
68+
runs-on: ubuntu-latest
69+
strategy:
70+
fail-fast: false
71+
matrix:
72+
package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust]
73+
permissions:
74+
attestations: write # is needed to delete attestations
75+
packages: read # is needed to list remaining package versions after cleanup
76+
steps:
77+
- uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
78+
with:
79+
disable-sudo-and-containers: true
80+
egress-policy: audit
81+
allowed-endpoints: api.github.com:443
82+
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
83+
id: download-digests
84+
continue-on-error: true
85+
with:
86+
name: digests-before-cleanup-${{ matrix.package }}
87+
- name: Delete orphaned attestations
88+
if: steps.download-digests.outcome == 'success'
89+
run: |
90+
set -Eeuo pipefail
91+
ORG="${GH_REPO%%/*}"
92+
93+
# Get remaining digests after image cleanup
94+
current_digests=$(gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \
95+
--paginate \
96+
--jq '.[].name' 2>/dev/null || echo "")
97+
98+
# Delete attestations for digests that no longer have a package version
99+
while read -r digest; do
100+
[[ -z "$digest" ]] && continue
101+
if ! echo "$current_digests" | grep -qx "$digest"; then
102+
echo "Deleting attestations for removed digest: ${digest}"
103+
encoded_digest="${digest//:/%3A}"
104+
gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \
105+
2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)"
106+
fi
107+
done < digests.txt
108+
env:
109+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
110+
GH_REPO: ${{ github.repository }}
111+
GH_PACKAGE: ${{ matrix.package }}

.github/workflows/pr-image-cleanup.yml

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,52 +8,47 @@ on:
88
permissions: {}
99

1010
jobs:
11-
delete-attestations:
12-
name: 🔏 Delete PR Attestations (${{ matrix.package }})
11+
collect-pr-digests:
12+
name: 📦 Collect PR Digests (${{ matrix.package }})
1313
runs-on: ubuntu-latest
14-
permissions:
15-
attestations: write # is needed to delete attestations
16-
packages: read # is needed to list package versions to find the PR digest
1714
strategy:
1815
fail-fast: false
1916
matrix:
2017
package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust]
18+
permissions:
19+
packages: read # is needed to find the digest for the PR tag
2120
steps:
2221
- uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
2322
with:
2423
disable-sudo-and-containers: true
2524
egress-policy: audit
2625
allowed-endpoints: api.github.com:443
27-
- name: Delete attestations for PR ${{ github.event.pull_request.number }}
26+
- name: Find PR image digest
2827
run: |
2928
set -Eeuo pipefail
30-
3129
ORG="${GH_REPO%%/*}"
3230
PR_TAG="pr-${PR_NUMBER}"
33-
34-
# Find the digest for the PR tag
3531
digest=$(gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \
3632
--paginate \
3733
--jq ".[] | select((.metadata.container.tags // []) | contains([\"${PR_TAG}\"]) ) | .name" \
38-
2>/dev/null | head -1 || true)
39-
40-
if [[ -z "$digest" ]]; then
41-
echo "No version found with tag ${PR_TAG} for ${GH_PACKAGE}, skipping"
42-
exit 0
43-
fi
44-
45-
echo "Deleting attestations for ${GH_PACKAGE}@${digest} (tag: ${PR_TAG})"
46-
encoded_digest="${digest//:/%3A}"
47-
gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \
48-
2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)"
34+
2>/dev/null | head -1 || echo "")
35+
echo "${digest:-}" > digest.txt
4936
env:
5037
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5138
GH_REPO: ${{ github.repository }}
5239
GH_PACKAGE: ${{ matrix.package }}
5340
PR_NUMBER: ${{ github.event.pull_request.number }}
41+
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
42+
with:
43+
name: pr-digest-${{ matrix.package }}
44+
path: digest.txt
45+
if-no-files-found: warn
46+
retention-days: 1
5447

5548
delete-images:
5649
name: 🗑️ Delete PR Images
50+
if: always()
51+
needs: collect-pr-digests
5752
runs-on: ubuntu-latest
5853
permissions:
5954
packages: write # is needed by dataaxiom/ghcr-cleanup-action to delete images
@@ -67,6 +62,47 @@ jobs:
6762
delete-tags: pr-${{ github.event.pull_request.number }}
6863
packages: amp-devcontainer,amp-devcontainer-cpp,amp-devcontainer-rust
6964

65+
delete-attestations:
66+
name: 🔏 Delete PR Attestations (${{ matrix.package }})
67+
needs: [collect-pr-digests, delete-images]
68+
runs-on: ubuntu-latest
69+
strategy:
70+
fail-fast: false
71+
matrix:
72+
package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust]
73+
permissions:
74+
attestations: write # is needed to delete attestations
75+
steps:
76+
- uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
77+
with:
78+
disable-sudo-and-containers: true
79+
egress-policy: audit
80+
allowed-endpoints: api.github.com:443
81+
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
82+
id: download-digest
83+
continue-on-error: true
84+
with:
85+
name: pr-digest-${{ matrix.package }}
86+
- name: Delete attestations for PR ${{ github.event.pull_request.number }}
87+
if: steps.download-digest.outcome == 'success'
88+
run: |
89+
set -Eeuo pipefail
90+
ORG="${GH_REPO%%/*}"
91+
digest=$(cat digest.txt)
92+
if [[ -z "$digest" ]]; then
93+
echo "No digest found for pr-${PR_NUMBER} in ${GH_PACKAGE}, skipping"
94+
exit 0
95+
fi
96+
echo "Deleting attestations for ${GH_PACKAGE}@${digest}"
97+
encoded_digest="${digest//:/%3A}"
98+
gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \
99+
2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)"
100+
env:
101+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
102+
GH_REPO: ${{ github.repository }}
103+
GH_PACKAGE: ${{ matrix.package }}
104+
PR_NUMBER: ${{ github.event.pull_request.number }}
105+
70106
cleanup-cache:
71107
name: 🧹 Cleanup Cache
72108
runs-on: ubuntu-latest

0 commit comments

Comments
 (0)