From b531c69d9048b9ecccbbeafd1e4d11b1789a08d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:29:58 +0000 Subject: [PATCH 1/3] Initial plan From bb9a228852492a125195b3e0caec97b1ad2a9eb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:50:37 +0000 Subject: [PATCH 2/3] Automate cleanup of attestations alongside image cleanup Co-authored-by: rjaegers <45816308+rjaegers@users.noreply.github.com> --- .github/workflows/image-cleanup.yml | 80 ++++++++++++++++++++++++++ .github/workflows/pr-image-cleanup.yml | 44 ++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/.github/workflows/image-cleanup.yml b/.github/workflows/image-cleanup.yml index 880e04b6..f8a7ae5e 100644 --- a/.github/workflows/image-cleanup.yml +++ b/.github/workflows/image-cleanup.yml @@ -9,8 +9,88 @@ on: permissions: {} jobs: + cleanup-attestations: + name: ๐Ÿ” Cleanup Attestations (${{ matrix.package }}) + runs-on: ubuntu-latest + permissions: + attestations: write # is needed to delete attestations + packages: read # is needed to list package versions to find digests + pull-requests: read # is needed to determine which pull requests are open + strategy: + fail-fast: false + matrix: + package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust] + steps: + - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + disable-sudo-and-containers: true + egress-policy: audit + allowed-endpoints: api.github.com:443 + - name: Delete outdated attestations + run: | + set -Eeuo pipefail + + ORG="${GH_REPO%%/*}" + + # Get all open PR numbers to determine which pr-N tags to keep + open_pr_numbers=$(gh api "/repos/${GH_REPO}/pulls?state=open&per_page=100" \ + --paginate \ + --jq '.[].number' 2>/dev/null || true) + + # Get all package versions with their digests and tags + while IFS=$'\t' read -r digest tags_csv; do + [[ -z "$digest" ]] && continue + + keep=false + + # Check each tag to determine if this digest should be kept + IFS=',' read -r -a tags <<< "$tags_csv" + for tag in "${tags[@]}"; do + [[ -z "$tag" ]] && continue + + # Keep release semver tags (e.g., v6.5.2, 6.5.2, 6.5, 6) + if [[ "$tag" =~ ^v?[0-9]+(\.[0-9]+)*$ ]]; then + keep=true + break + fi + + # Keep edge tag + if [[ "$tag" == "edge" ]]; then + keep=true + break + fi + + # Keep pr-N tags for open pull requests + if [[ "$tag" =~ ^pr-([0-9]+)$ ]]; then + pr_number="${BASH_REMATCH[1]}" + if echo "$open_pr_numbers" | grep -qx "$pr_number"; then + keep=true + break + fi + fi + done + + if [[ "$keep" == "false" ]]; then + echo "Deleting attestations for ${GH_PACKAGE}@${digest} (tags: ${tags_csv})" + encoded_digest="${digest//:/%3A}" + gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \ + 2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)" + fi + done < <( + gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \ + --paginate \ + --jq '.[] | "\(.name)\t\(.metadata.container.tags // [] | join(","))"' \ + 2>/dev/null || true + ) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + GH_PACKAGE: ${{ matrix.package }} + cleanup-images: name: ๐Ÿงน Clean Images + if: always() + needs: cleanup-attestations runs-on: ubuntu-latest permissions: packages: write # is needed by dataaxiom/ghcr-cleanup-action to delete untagged and orphaned images diff --git a/.github/workflows/pr-image-cleanup.yml b/.github/workflows/pr-image-cleanup.yml index 59ca7105..0d6140e9 100644 --- a/.github/workflows/pr-image-cleanup.yml +++ b/.github/workflows/pr-image-cleanup.yml @@ -8,6 +8,50 @@ on: permissions: {} jobs: + delete-attestations: + name: ๐Ÿ” Delete PR Attestations (${{ matrix.package }}) + runs-on: ubuntu-latest + permissions: + attestations: write # is needed to delete attestations + packages: read # is needed to list package versions to find the PR digest + strategy: + fail-fast: false + matrix: + package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust] + steps: + - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + disable-sudo-and-containers: true + egress-policy: audit + allowed-endpoints: api.github.com:443 + - name: Delete attestations for PR ${{ github.event.pull_request.number }} + run: | + set -Eeuo pipefail + + ORG="${GH_REPO%%/*}" + PR_TAG="pr-${PR_NUMBER}" + + # Find the digest for the PR tag + digest=$(gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \ + --paginate \ + --jq ".[] | select((.metadata.container.tags // []) | contains([\"${PR_TAG}\"]) ) | .name" \ + 2>/dev/null | head -1 || true) + + if [[ -z "$digest" ]]; then + echo "No version found with tag ${PR_TAG} for ${GH_PACKAGE}, skipping" + exit 0 + fi + + echo "Deleting attestations for ${GH_PACKAGE}@${digest} (tag: ${PR_TAG})" + encoded_digest="${digest//:/%3A}" + gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \ + 2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + GH_PACKAGE: ${{ matrix.package }} + PR_NUMBER: ${{ github.event.pull_request.number }} + delete-images: name: ๐Ÿ—‘๏ธ Delete PR Images runs-on: ubuntu-latest From 6419753c095283bccc97b34723b9066ed23b57b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:27:17 +0000 Subject: [PATCH 3/3] Refactor: make image deletion leading, then clean orphaned attestations Co-authored-by: rjaegers <45816308+rjaegers@users.noreply.github.com> --- .github/workflows/image-cleanup.yml | 121 +++++++++++++------------ .github/workflows/pr-image-cleanup.yml | 76 ++++++++++++---- 2 files changed, 118 insertions(+), 79 deletions(-) diff --git a/.github/workflows/image-cleanup.yml b/.github/workflows/image-cleanup.yml index f8a7ae5e..57504fca 100644 --- a/.github/workflows/image-cleanup.yml +++ b/.github/workflows/image-cleanup.yml @@ -9,88 +9,43 @@ on: permissions: {} jobs: - cleanup-attestations: - name: ๐Ÿ” Cleanup Attestations (${{ matrix.package }}) + collect-digests: + name: ๐Ÿ“ฆ Collect Digests (${{ matrix.package }}) runs-on: ubuntu-latest - permissions: - attestations: write # is needed to delete attestations - packages: read # is needed to list package versions to find digests - pull-requests: read # is needed to determine which pull requests are open strategy: fail-fast: false matrix: package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust] + permissions: + packages: read # is needed to list package versions steps: - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: disable-sudo-and-containers: true egress-policy: audit allowed-endpoints: api.github.com:443 - - name: Delete outdated attestations + - name: Collect package digests run: | set -Eeuo pipefail - ORG="${GH_REPO%%/*}" - - # Get all open PR numbers to determine which pr-N tags to keep - open_pr_numbers=$(gh api "/repos/${GH_REPO}/pulls?state=open&per_page=100" \ + gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \ --paginate \ - --jq '.[].number' 2>/dev/null || true) - - # Get all package versions with their digests and tags - while IFS=$'\t' read -r digest tags_csv; do - [[ -z "$digest" ]] && continue - - keep=false - - # Check each tag to determine if this digest should be kept - IFS=',' read -r -a tags <<< "$tags_csv" - for tag in "${tags[@]}"; do - [[ -z "$tag" ]] && continue - - # Keep release semver tags (e.g., v6.5.2, 6.5.2, 6.5, 6) - if [[ "$tag" =~ ^v?[0-9]+(\.[0-9]+)*$ ]]; then - keep=true - break - fi - - # Keep edge tag - if [[ "$tag" == "edge" ]]; then - keep=true - break - fi - - # Keep pr-N tags for open pull requests - if [[ "$tag" =~ ^pr-([0-9]+)$ ]]; then - pr_number="${BASH_REMATCH[1]}" - if echo "$open_pr_numbers" | grep -qx "$pr_number"; then - keep=true - break - fi - fi - done - - if [[ "$keep" == "false" ]]; then - echo "Deleting attestations for ${GH_PACKAGE}@${digest} (tags: ${tags_csv})" - encoded_digest="${digest//:/%3A}" - gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \ - 2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)" - fi - done < <( - gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \ - --paginate \ - --jq '.[] | "\(.name)\t\(.metadata.container.tags // [] | join(","))"' \ - 2>/dev/null || true - ) + --jq '.[].name' 2>/dev/null > digests.txt || touch digests.txt env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} GH_PACKAGE: ${{ matrix.package }} + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: digests-before-cleanup-${{ matrix.package }} + path: digests.txt + if-no-files-found: warn + retention-days: 1 cleanup-images: name: ๐Ÿงน Clean Images if: always() - needs: cleanup-attestations + needs: collect-digests runs-on: ubuntu-latest permissions: packages: write # is needed by dataaxiom/ghcr-cleanup-action to delete untagged and orphaned images @@ -106,3 +61,51 @@ jobs: delete-orphaned-images: true delete-untagged: true packages: amp-devcontainer,amp-devcontainer-cpp,amp-devcontainer-rust + + cleanup-attestations: + name: ๐Ÿ” Cleanup Orphaned Attestations (${{ matrix.package }}) + needs: cleanup-images + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust] + permissions: + attestations: write # is needed to delete attestations + packages: read # is needed to list remaining package versions after cleanup + steps: + - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + disable-sudo-and-containers: true + egress-policy: audit + allowed-endpoints: api.github.com:443 + - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + id: download-digests + continue-on-error: true + with: + name: digests-before-cleanup-${{ matrix.package }} + - name: Delete orphaned attestations + if: steps.download-digests.outcome == 'success' + run: | + set -Eeuo pipefail + ORG="${GH_REPO%%/*}" + + # Get remaining digests after image cleanup + current_digests=$(gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \ + --paginate \ + --jq '.[].name' 2>/dev/null || echo "") + + # Delete attestations for digests that no longer have a package version + while read -r digest; do + [[ -z "$digest" ]] && continue + if ! echo "$current_digests" | grep -qx "$digest"; then + echo "Deleting attestations for removed digest: ${digest}" + encoded_digest="${digest//:/%3A}" + gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \ + 2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)" + fi + done < digests.txt + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + GH_PACKAGE: ${{ matrix.package }} diff --git a/.github/workflows/pr-image-cleanup.yml b/.github/workflows/pr-image-cleanup.yml index 0d6140e9..c9224f11 100644 --- a/.github/workflows/pr-image-cleanup.yml +++ b/.github/workflows/pr-image-cleanup.yml @@ -8,52 +8,47 @@ on: permissions: {} jobs: - delete-attestations: - name: ๐Ÿ” Delete PR Attestations (${{ matrix.package }}) + collect-pr-digests: + name: ๐Ÿ“ฆ Collect PR Digests (${{ matrix.package }}) runs-on: ubuntu-latest - permissions: - attestations: write # is needed to delete attestations - packages: read # is needed to list package versions to find the PR digest strategy: fail-fast: false matrix: package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust] + permissions: + packages: read # is needed to find the digest for the PR tag steps: - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: disable-sudo-and-containers: true egress-policy: audit allowed-endpoints: api.github.com:443 - - name: Delete attestations for PR ${{ github.event.pull_request.number }} + - name: Find PR image digest run: | set -Eeuo pipefail - ORG="${GH_REPO%%/*}" PR_TAG="pr-${PR_NUMBER}" - - # Find the digest for the PR tag digest=$(gh api "/orgs/${ORG}/packages/container/${GH_PACKAGE}/versions" \ --paginate \ --jq ".[] | select((.metadata.container.tags // []) | contains([\"${PR_TAG}\"]) ) | .name" \ - 2>/dev/null | head -1 || true) - - if [[ -z "$digest" ]]; then - echo "No version found with tag ${PR_TAG} for ${GH_PACKAGE}, skipping" - exit 0 - fi - - echo "Deleting attestations for ${GH_PACKAGE}@${digest} (tag: ${PR_TAG})" - encoded_digest="${digest//:/%3A}" - gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \ - 2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)" + 2>/dev/null | head -1 || echo "") + echo "${digest:-}" > digest.txt env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} GH_PACKAGE: ${{ matrix.package }} PR_NUMBER: ${{ github.event.pull_request.number }} + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: pr-digest-${{ matrix.package }} + path: digest.txt + if-no-files-found: warn + retention-days: 1 delete-images: name: ๐Ÿ—‘๏ธ Delete PR Images + if: always() + needs: collect-pr-digests runs-on: ubuntu-latest permissions: packages: write # is needed by dataaxiom/ghcr-cleanup-action to delete images @@ -67,6 +62,47 @@ jobs: delete-tags: pr-${{ github.event.pull_request.number }} packages: amp-devcontainer,amp-devcontainer-cpp,amp-devcontainer-rust + delete-attestations: + name: ๐Ÿ” Delete PR Attestations (${{ matrix.package }}) + needs: [collect-pr-digests, delete-images] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + package: [amp-devcontainer-base, amp-devcontainer-cpp, amp-devcontainer-rust] + permissions: + attestations: write # is needed to delete attestations + steps: + - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + with: + disable-sudo-and-containers: true + egress-policy: audit + allowed-endpoints: api.github.com:443 + - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + id: download-digest + continue-on-error: true + with: + name: pr-digest-${{ matrix.package }} + - name: Delete attestations for PR ${{ github.event.pull_request.number }} + if: steps.download-digest.outcome == 'success' + run: | + set -Eeuo pipefail + ORG="${GH_REPO%%/*}" + digest=$(cat digest.txt) + if [[ -z "$digest" ]]; then + echo "No digest found for pr-${PR_NUMBER} in ${GH_PACKAGE}, skipping" + exit 0 + fi + echo "Deleting attestations for ${GH_PACKAGE}@${digest}" + encoded_digest="${digest//:/%3A}" + gh api --method DELETE "/orgs/${ORG}/attestations/digest/${encoded_digest}" \ + 2>/dev/null && echo "Deleted" || echo "No attestations found (already cleaned up)" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + GH_PACKAGE: ${{ matrix.package }} + PR_NUMBER: ${{ github.event.pull_request.number }} + cleanup-cache: name: ๐Ÿงน Cleanup Cache runs-on: ubuntu-latest