|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +# Script to interactively review and delete old container package versions from GitHub Packages. |
| 5 | +# By default, it will review all container packages based on the folder structure in src/. |
| 6 | +# You can specify a single container to review with --container <name>. |
| 7 | +# Use --dry-run to see what would be deleted without actually performing deletions. |
| 8 | +# To use it, you must have authenticated with github using this command |
| 9 | +# gh auth login --scopes read:packages,delete:packages |
| 10 | +# |
| 11 | + |
| 12 | +DRY_RUN=false |
| 13 | +TARGET_CONTAINER="" |
| 14 | + |
| 15 | +while [[ $# -gt 0 ]]; do |
| 16 | + case "$1" in |
| 17 | + --dry-run|-n) |
| 18 | + DRY_RUN=true |
| 19 | + shift |
| 20 | + ;; |
| 21 | + --container) |
| 22 | + if [[ $# -lt 2 || -z "$2" ]]; then |
| 23 | + echo "--container requires a value" >&2 |
| 24 | + echo "Usage: $0 [--dry-run] [--container <name>]" >&2 |
| 25 | + exit 1 |
| 26 | + fi |
| 27 | + TARGET_CONTAINER="$2" |
| 28 | + shift 2 |
| 29 | + ;; |
| 30 | + --help|-h) |
| 31 | + echo "Usage: $0 [--dry-run] [--container <name>]" |
| 32 | + echo "Interactively review every container package version and delete selected versions." |
| 33 | + exit 0 |
| 34 | + ;; |
| 35 | + *) |
| 36 | + echo "Unknown option: $1" >&2 |
| 37 | + echo "Usage: $0 [--dry-run] [--container <name>]" >&2 |
| 38 | + exit 1 |
| 39 | + ;; |
| 40 | + esac |
| 41 | +done |
| 42 | + |
| 43 | +if ! command -v gh >/dev/null 2>&1; then |
| 44 | + echo "gh CLI is required" >&2 |
| 45 | + exit 1 |
| 46 | +fi |
| 47 | + |
| 48 | +if ! command -v jq >/dev/null 2>&1; then |
| 49 | + echo "jq is required" >&2 |
| 50 | + exit 1 |
| 51 | +fi |
| 52 | + |
| 53 | +get_container_package_name() { |
| 54 | + local container_name=$1 |
| 55 | + |
| 56 | + if [[ -z "${container_name}" ]]; then |
| 57 | + echo "Container name is required" >&2 |
| 58 | + return 1 |
| 59 | + fi |
| 60 | + |
| 61 | + printf 'eps-devcontainers/%s' "${container_name}" | jq -sRr @uri |
| 62 | +} |
| 63 | + |
| 64 | +get_container_versions_json() { |
| 65 | + local container_name=$1 |
| 66 | + local package_name |
| 67 | + |
| 68 | + package_name=$(get_container_package_name "${container_name}") |
| 69 | + |
| 70 | + gh api \ |
| 71 | + -H "Accept: application/vnd.github+json" \ |
| 72 | + "/orgs/nhsdigital/packages/container/${package_name}/versions" \ |
| 73 | + --paginate |
| 74 | +} |
| 75 | + |
| 76 | +confirm_delete() { |
| 77 | + local prompt=$1 |
| 78 | + local reply |
| 79 | + |
| 80 | + if [[ -r /dev/tty ]]; then |
| 81 | + read -r -p "${prompt} [y/N]: " reply < /dev/tty |
| 82 | + else |
| 83 | + echo "No interactive terminal available; defaulting to 'No'." |
| 84 | + return 1 |
| 85 | + fi |
| 86 | + [[ "${reply}" == "y" || "${reply}" == "Y" ]] |
| 87 | +} |
| 88 | + |
| 89 | +review_and_delete_container_versions() { |
| 90 | + local container_name=$1 |
| 91 | + local package_name |
| 92 | + local versions_json |
| 93 | + local version_count |
| 94 | + |
| 95 | + package_name=$(get_container_package_name "${container_name}") |
| 96 | + versions_json=$(get_container_versions_json "${container_name}") |
| 97 | + version_count=$(jq 'length' <<<"${versions_json}") |
| 98 | + |
| 99 | + echo "" |
| 100 | + echo "=== Container: ${container_name} (${version_count} versions) ===" |
| 101 | + |
| 102 | + if [[ "${version_count}" -eq 0 ]]; then |
| 103 | + echo "No versions found, skipping." |
| 104 | + return 0 |
| 105 | + fi |
| 106 | + |
| 107 | + while IFS= read -r version; do |
| 108 | + local version_id |
| 109 | + local created_at |
| 110 | + local updated_at |
| 111 | + local tags |
| 112 | + local is_untagged |
| 113 | + local has_sha256_tag |
| 114 | + local keep_without_prompt |
| 115 | + |
| 116 | + version_id=$(jq -r '.id' <<<"${version}") |
| 117 | + created_at=$(jq -r '.created_at // "unknown"' <<<"${version}") |
| 118 | + updated_at=$(jq -r '.updated_at // "unknown"' <<<"${version}") |
| 119 | + tags=$(jq -r '(.metadata.container.tags // []) | if length == 0 then "<untagged>" else join(", ") end' <<<"${version}") |
| 120 | + is_untagged=$(jq -r '((.metadata.container.tags // []) | length) == 0' <<<"${version}") |
| 121 | + has_sha256_tag=$(jq -r 'any((.metadata.container.tags // [])[]?; test("^sha256-.+"))' <<<"${version}") |
| 122 | + keep_without_prompt=$(jq -r ' |
| 123 | + any((.metadata.container.tags // [])[]?; |
| 124 | + test("^githubactions-ci-.+") or |
| 125 | + test("^ci-.+") or |
| 126 | + test("^githubactions-latest$") or |
| 127 | + test("^latest$") or |
| 128 | + test("^githubactions-v.+") or |
| 129 | + test("^v.+") |
| 130 | + ) |
| 131 | + ' <<<"${version}") |
| 132 | + |
| 133 | + echo "" |
| 134 | + echo "Container: ${container_name}" |
| 135 | + echo "Version ID: ${version_id}" |
| 136 | + echo "Created: ${created_at}" |
| 137 | + echo "Updated: ${updated_at}" |
| 138 | + echo "Tags: ${tags}" |
| 139 | + |
| 140 | + if [[ "${is_untagged}" == "true" ]]; then |
| 141 | + if [[ "${DRY_RUN}" == "true" ]]; then |
| 142 | + echo "[DRY RUN] Would auto-delete untagged version ID ${version_id} from ${container_name}." |
| 143 | + else |
| 144 | + echo "Auto-deleting untagged version ID ${version_id} from ${container_name}..." |
| 145 | + gh api \ |
| 146 | + -H "Accept: application/vnd.github+json" \ |
| 147 | + -X DELETE \ |
| 148 | + "/orgs/nhsdigital/packages/container/${package_name}/versions/${version_id}" |
| 149 | + fi |
| 150 | + elif [[ "${has_sha256_tag}" == "true" ]]; then |
| 151 | + if [[ "${DRY_RUN}" == "true" ]]; then |
| 152 | + echo "[DRY RUN] Would auto-delete sha256-tagged version ID ${version_id} from ${container_name}." |
| 153 | + else |
| 154 | + echo "Auto-deleting sha256-tagged version ID ${version_id} from ${container_name}..." |
| 155 | + gh api \ |
| 156 | + -H "Accept: application/vnd.github+json" \ |
| 157 | + -X DELETE \ |
| 158 | + "/orgs/nhsdigital/packages/container/${package_name}/versions/${version_id}" |
| 159 | + fi |
| 160 | + elif [[ "${keep_without_prompt}" == "true" ]]; then |
| 161 | + echo "Keeping protected version ID ${version_id} (matching keep-tag rule)." |
| 162 | + elif confirm_delete "Delete this version?"; then |
| 163 | + if [[ "${DRY_RUN}" == "true" ]]; then |
| 164 | + echo "[DRY RUN] Would delete version ID ${version_id} from ${container_name}." |
| 165 | + else |
| 166 | + echo "Deleting version ID ${version_id} from ${container_name}..." |
| 167 | + gh api \ |
| 168 | + -H "Accept: application/vnd.github+json" \ |
| 169 | + -X DELETE \ |
| 170 | + "/orgs/nhsdigital/packages/container/${package_name}/versions/${version_id}" |
| 171 | + fi |
| 172 | + else |
| 173 | + echo "Skipping version ID ${version_id}." |
| 174 | + fi |
| 175 | + done < <(jq -c '.[]' <<<"${versions_json}") |
| 176 | +} |
| 177 | + |
| 178 | +base_node_folders=$(find src/base_node -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | jq -R -s -c 'split("\n")[:-1]') |
| 179 | +language_folders=$(find src/languages -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | jq -R -s -c 'split("\n")[:-1]') |
| 180 | +project_folders=$(find src/projects -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | jq -R -s -c 'split("\n")[:-1]') |
| 181 | + |
| 182 | +if [[ -n "${TARGET_CONTAINER}" ]]; then |
| 183 | + review_and_delete_container_versions "${TARGET_CONTAINER}" |
| 184 | + exit 0 |
| 185 | +fi |
| 186 | + |
| 187 | +for container_name in $(jq -r '.[]' <<<"${project_folders}"); do |
| 188 | + review_and_delete_container_versions "${container_name}" |
| 189 | +done |
| 190 | + |
| 191 | +for container_name in $(jq -r '.[]' <<<"${base_node_folders}"); do |
| 192 | + review_and_delete_container_versions "${container_name}" |
| 193 | +done |
| 194 | + |
| 195 | +for container_name in $(jq -r '.[]' <<<"${language_folders}"); do |
| 196 | + review_and_delete_container_versions "${container_name}" |
| 197 | +done |
| 198 | + |
| 199 | +review_and_delete_container_versions "base" |
0 commit comments