|
| 1 | +#!/usr/bin/env bash |
| 2 | +#─────────────────────────────────────────────────────────────────────────────── |
| 3 | +# Script : cleanup.sh |
| 4 | +# Author : Mihai Criveti |
| 5 | +# Purpose: Prune old or unused GHCR container versions for IBM's MCP Context Forge |
| 6 | +# Copyright 2025 |
| 7 | +# SPDX-License-Identifier: Apache-2.0 |
| 8 | +# |
| 9 | +# Description: |
| 10 | +# This script safely manages container versions in GitHub Container Registry |
| 11 | +# (ghcr.io) under the IBM organization, specifically targeting the |
| 12 | +# `mcp-context-forge` package. It supports interactive and non-interactive |
| 13 | +# deletion modes to help you keep the container registry clean. |
| 14 | +# |
| 15 | +# Features: |
| 16 | +# • Dry-run by default to avoid accidental deletion |
| 17 | +# • Tag whitelisting with regular expression matching |
| 18 | +# • GitHub CLI integration with scope validation |
| 19 | +# • CI/CD-compatible via environment overrides |
| 20 | +# |
| 21 | +# Requirements: |
| 22 | +# • GitHub CLI (gh) v2.x with appropriate scopes |
| 23 | +# • jq (command-line JSON processor) |
| 24 | +# |
| 25 | +# Required Token Scopes: |
| 26 | +# delete:packages |
| 27 | +# |
| 28 | +# Authentication Notes: |
| 29 | +# Authenticate with: |
| 30 | +# gh auth refresh -h github.com -s read:packages,delete:packages |
| 31 | +# Or: |
| 32 | +# gh auth logout |
| 33 | +# gh auth login --scopes "read:packages,delete:packages,write:packages,repo,read:org,gist" |
| 34 | +# |
| 35 | +# Verify authentication with: |
| 36 | +# gh auth status -t |
| 37 | +# |
| 38 | +# Environment Variables: |
| 39 | +# GITHUB_TOKEN / GH_TOKEN : GitHub token with required scopes |
| 40 | +# DRY_RUN : Set to "false" to enable actual deletions (default: true) |
| 41 | +# |
| 42 | +# Usage: |
| 43 | +# ./cleanup.sh # Dry-run with confirmation prompt |
| 44 | +# DRY_RUN=false ./cleanup.sh --yes # Actual deletion without prompt (for CI) |
| 45 | +# |
| 46 | +#─────────────────────────────────────────────────────────────────────────────── |
| 47 | + |
| 48 | +set -euo pipefail |
| 49 | + |
| 50 | +############################################################################## |
| 51 | +# 1. PICK A TOKEN |
| 52 | +############################################################################## |
| 53 | +NEEDED_SCOPES="delete:packages" |
| 54 | + |
| 55 | +if [[ -n "${GITHUB_TOKEN:-}" ]]; then |
| 56 | + TOKEN="$GITHUB_TOKEN" |
| 57 | +elif [[ -n "${GH_TOKEN:-}" ]]; then |
| 58 | + TOKEN="$GH_TOKEN" |
| 59 | +else |
| 60 | + # fall back to whatever gh already has |
| 61 | + if ! TOKEN=$(gh auth token 2>/dev/null); then |
| 62 | + echo "❌ No token exported and gh not logged in. Fix with:" |
| 63 | + echo " gh auth login (or export GITHUB_TOKEN)" |
| 64 | + exit 1 |
| 65 | + fi |
| 66 | +fi |
| 67 | +export GH_TOKEN="$TOKEN" # gh api uses this |
| 68 | + |
| 69 | +# Fixed scope checking - check for both required scopes individually |
| 70 | +if scopes=$(gh auth status --show-token 2>/dev/null | grep -oP 'Token scopes: \K.*' || echo ""); then |
| 71 | + missing_scopes=() |
| 72 | + |
| 73 | + # if ! echo "$scopes" | grep -q "read:packages"; then |
| 74 | + # missing_scopes+=("read:packages") |
| 75 | + # fi |
| 76 | + |
| 77 | + if ! echo "$scopes" | grep -q "delete:packages"; then |
| 78 | + missing_scopes+=("delete:packages") |
| 79 | + fi |
| 80 | + |
| 81 | + if [[ ${#missing_scopes[@]} -gt 0 ]]; then |
| 82 | + echo "⚠️ Your token scopes are [$scopes] – but you're missing: [$(IFS=','; echo "${missing_scopes[*]}")]" |
| 83 | + echo " Run: gh auth refresh -h github.com -s $NEEDED_SCOPES" |
| 84 | + exit 1 |
| 85 | + fi |
| 86 | +else |
| 87 | + echo "⚠️ Could not verify token scopes. Proceeding anyway..." |
| 88 | +fi |
| 89 | + |
| 90 | +############################################################################## |
| 91 | +# 2. CONFIG |
| 92 | +############################################################################## |
| 93 | +ORG="ibm" |
| 94 | +PKG="mcp-context-forge" |
| 95 | +KEEP_TAGS=( "0.1.0" "v0.1.0" "0.1.1" "v0.1.1" "latest" ) |
| 96 | +PER_PAGE=100 |
| 97 | + |
| 98 | +DRY_RUN=${DRY_RUN:-true} # default safe |
| 99 | +ASK_CONFIRM=true |
| 100 | +[[ ${1:-} == "--yes" ]] && ASK_CONFIRM=false |
| 101 | +KEEP_REGEX="^($(IFS='|'; echo "${KEEP_TAGS[*]}"))$" |
| 102 | + |
| 103 | +############################################################################## |
| 104 | +# 3. MAIN |
| 105 | +############################################################################## |
| 106 | +delete_ids=() |
| 107 | + |
| 108 | +echo "📦 Scanning ghcr.io/${ORG}/${PKG} …" |
| 109 | + |
| 110 | +# Process versions and collect IDs to delete |
| 111 | +while IFS= read -r row; do |
| 112 | + id=$(jq -r '.id' <<<"$row") |
| 113 | + digest=$(jq -r '.digest' <<<"$row") |
| 114 | + tags_csv=$(jq -r '.tags | join(",")' <<<"$row") |
| 115 | + keep=$(jq -e --arg re "$KEEP_REGEX" 'any(.tags[]?; test($re))' <<<"$row" 2>/dev/null) || keep=false |
| 116 | + |
| 117 | + if [[ $keep == true ]]; then |
| 118 | + printf "✅ KEEP %s [%s]\n" "$digest" "$tags_csv" |
| 119 | + else |
| 120 | + printf "🗑️ DELETE %s [%s]\n" "$digest" "$tags_csv" |
| 121 | + delete_ids+=("$id") |
| 122 | + fi |
| 123 | +done < <(gh api -H "Accept: application/vnd.github+json" \ |
| 124 | + "/orgs/${ORG}/packages/container/${PKG}/versions?per_page=${PER_PAGE}" \ |
| 125 | + --paginate | \ |
| 126 | + jq -cr --arg re "$KEEP_REGEX" ' |
| 127 | + .[] | |
| 128 | + { |
| 129 | + id, |
| 130 | + digest: .metadata.container.digest, |
| 131 | + tags: (.metadata.container.tags // []) |
| 132 | + } |
| 133 | + ') |
| 134 | + |
| 135 | +############################################################################## |
| 136 | +# 4. CONFIRMATION & DELETION |
| 137 | +############################################################################## |
| 138 | +if [[ ${#delete_ids[@]} -eq 0 ]]; then |
| 139 | + echo "✨ Nothing to delete!" |
| 140 | + exit 0 |
| 141 | +fi |
| 142 | + |
| 143 | +if [[ $DRY_RUN == true ]]; then |
| 144 | + if [[ $ASK_CONFIRM == true ]]; then |
| 145 | + echo |
| 146 | + read -rp "Proceed to delete the ${#delete_ids[@]} versions listed above? (y/N) " reply |
| 147 | + [[ $reply =~ ^[Yy]$ ]] || { echo "Aborted – nothing deleted."; exit 0; } |
| 148 | + fi |
| 149 | + echo "🚀 Re-running in destructive mode …" |
| 150 | + DRY_RUN=false exec "$0" --yes |
| 151 | +else |
| 152 | + echo "🗑️ Deleting ${#delete_ids[@]} versions..." |
| 153 | + for id in "${delete_ids[@]}"; do |
| 154 | + if gh api -X DELETE -H "Accept: application/vnd.github+json" \ |
| 155 | + "/orgs/${ORG}/packages/container/${PKG}/versions/${id}" >/dev/null 2>&1; then |
| 156 | + echo "✅ Deleted version ID: $id" |
| 157 | + else |
| 158 | + echo "❌ Failed to delete version ID: $id" |
| 159 | + fi |
| 160 | + done |
| 161 | + echo "Done." |
| 162 | +fi |
0 commit comments