diff --git a/.github/scripts/delete_unused_images.sh b/.github/scripts/delete_unused_images.sh new file mode 100755 index 0000000..8b49f0d --- /dev/null +++ b/.github/scripts/delete_unused_images.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -e + +get_container_package_name() { + local container_name=$1 + + if [[ -z "${container_name}" ]]; then + echo "Container name is required" >&2 + return 1 + fi + + # URL-encode the package path (eps-devcontainers/${container_name}) for the GH API + printf 'eps-devcontainers/%s' "${container_name}" | jq -sRr @uri +} + +get_container_versions_json() { + local container_name=$1 + local package_name + + package_name=$(get_container_package_name "${container_name}") + + gh api \ + -H "Accept: application/vnd.github+json" \ + "/orgs/nhsdigital/packages/container/${package_name}/versions" \ + --paginate +} + +delete_pr_images() { + local container_name=$1 + local package_name + local versions_json + local tags + + if [[ -z "${container_name}" ]]; then + echo "Container name is required" >&2 + return 1 + fi + + package_name=$(get_container_package_name "${container_name}") + versions_json=$(get_container_versions_json "${container_name}") + tags=$(jq -r '[.[].metadata.container.tags[]?] | unique | .[]' <<<"${versions_json}") + + if [[ -z "${tags}" ]]; then + return 0 + fi + + while IFS= read -r tag; do + if [[ "${tag}" =~ ^pr-[0-9]+- ]]; then + local pull_request + local pr_json + local pr_state + + pull_request=${tag#pr-} + pull_request=${pull_request%%-*} + + if ! pr_json=$(gh api \ + -H "Accept: application/vnd.github+json" \ + "/repos/NHSDigital/eps-devcontainers/pulls/${pull_request}"); then + continue + fi + echo "Checking PR #${pull_request} for tag ${tag} in container ${container_name}..." + pr_state=$(jq -r '.state // empty' <<<"${pr_json}") + if [[ "${pr_state}" != "closed" ]]; then + echo "State is not closed - not deleting images" + continue + fi + + jq -r --arg tag "${tag}" '.[] | select(.metadata.container.tags[]? == $tag) | .id' \ + <<<"${versions_json}" \ + | while IFS= read -r version_id; do + if [[ -n "${version_id}" ]]; then + echo "Deleting image with tag ${tag} (version ID: ${version_id}) from container ${container_name}..." + gh api \ + -H "Accept: application/vnd.github+json" \ + -X DELETE \ + "/orgs/nhsdigital/packages/container/${package_name}/versions/${version_id}" + fi + done + fi + done <<<"${tags}" +} + + +language_folders=$(find src/languages -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | jq -R -s -c 'split("\n")[:-1]') +project_folders=$(find src/projects -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | jq -R -s -c 'split("\n")[:-1]') + +for container_name in $(jq -r '.[]' <<<"${project_folders}"); do + delete_pr_images "${container_name}" +done + +for container_name in $(jq -r '.[]' <<<"${language_folders}"); do + delete_pr_images "${container_name}" +done + +delete_pr_images "base" diff --git a/.github/workflows/delete_old_images.yml b/.github/workflows/delete_old_images.yml new file mode 100644 index 0000000..c6601b2 --- /dev/null +++ b/.github/workflows/delete_old_images.yml @@ -0,0 +1,55 @@ +name: "Delete old cloudformation stacks" + +# Controls when the action will run - in this case triggered manually and on schedule +on: + workflow_dispatch: + schedule: + - cron: "0 1,13 * * *" + push: + branches: [main] + +jobs: + delete-old-cloudformation-stacks: + runs-on: ubuntu-22.04 + permissions: + id-token: write + contents: read + + steps: + - name: Checkout local code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ env.BRANCH_NAME }} + fetch-depth: 0 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + role-session-name: psu-delete-old-stacks + + - name: delete stacks + shell: bash + working-directory: .github/scripts + run: ./delete_stacks.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + delete-old-proxygen-deployments: + runs-on: ubuntu-22.04 + permissions: + id-token: write + contents: read + + steps: + - name: Checkout local code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ env.BRANCH_NAME }} + fetch-depth: 0 + + - name: delete unused images + shell: bash + working-directory: .github/scripts + run: ./delete_unused_images.sh diff --git a/Makefile b/Makefile index 686c788..629085e 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ build-image: guard-CONTAINER_NAME guard-BASE_VERSION guard-BASE_FOLDER npx devcontainer build \ --workspace-folder ./src/$${BASE_FOLDER}/$${CONTAINER_NAME} \ --push false \ + --cache-from "${CONTAINER_PREFIX}$${CONTAINER_NAME}:latest" \ --label "org.opencontainers.image.revision=$$DOCKER_TAG" \ --image-name "${CONTAINER_PREFIX}$${CONTAINER_NAME}${IMAGE_TAG}" @@ -70,3 +71,9 @@ test: lint-githubactions: actionlint + +github-login: + gh auth login --scopes read:packages + +lint-githubaction-scripts: + shellcheck .github/scripts/*.sh diff --git a/poetry.lock b/poetry.lock index 30f8599..e19dfdd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -26,26 +26,26 @@ files = [ [[package]] name = "filelock" -version = "3.20.2" +version = "3.21.2" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8"}, - {file = "filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64"}, + {file = "filelock-3.21.2-py3-none-any.whl", hash = "sha256:d6cd4dbef3e1bb63bc16500fc5aa100f16e405bbff3fb4231711851be50c1560"}, + {file = "filelock-3.21.2.tar.gz", hash = "sha256:cfd218cfccf8b947fce7837da312ec3359d10ef2a47c8602edd59e0bacffb708"}, ] [[package]] name = "identify" -version = "2.6.15" +version = "2.6.16" description = "File identification library for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"}, - {file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"}, + {file = "identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0"}, + {file = "identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980"}, ] [package.extras] @@ -65,21 +65,16 @@ files = [ [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.7.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, - {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, + {file = "platformdirs-4.7.0-py3-none-any.whl", hash = "sha256:1ed8db354e344c5bb6039cd727f096af975194b508e37177719d562b2b540ee6"}, + {file = "platformdirs-4.7.0.tar.gz", hash = "sha256:fd1a5f8599c85d49b9ac7d6e450bc2f1aaf4a23f1fe86d09952fe20ad365cf36"}, ] -[package.extras] -docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] -type = ["mypy (>=1.18.2)"] - [[package]] name = "pre-commit" version = "4.5.1" @@ -184,19 +179,19 @@ files = [ [[package]] name = "virtualenv" -version = "20.35.4" +version = "20.36.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"}, - {file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"}, + {file = "virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f"}, + {file = "virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba"}, ] [package.dependencies] distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" +filelock = {version = ">=3.20.1,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" [package.extras]