diff --git a/.github/scripts/delete_unused_images.sh b/.github/scripts/delete_unused_images.sh index a569b99..d16919d 100755 --- a/.github/scripts/delete_unused_images.sh +++ b/.github/scripts/delete_unused_images.sh @@ -3,6 +3,7 @@ DRY_RUN=false DELETE_PR=false DELETE_CI=false +DELETE_UNTAGGED=false while [[ $# -gt 0 ]]; do case "$1" in @@ -18,13 +19,17 @@ while [[ $# -gt 0 ]]; do DELETE_CI=true shift ;; + --delete-untagged) + DELETE_UNTAGGED=true + shift + ;; --help|-h) - echo "Usage: $0 [--dry-run] [--delete-pr] [--delete-ci]" + echo "Usage: $0 [--dry-run] [--delete-pr] [--delete-ci] [--delete-untagged]" exit 0 ;; *) echo "Unknown option: $1" >&2 - echo "Usage: $0 [--dry-run] [--delete-pr] [--delete-ci]" >&2 + echo "Usage: $0 [--dry-run] [--delete-pr] [--delete-ci] [--delete-untagged]" >&2 exit 1 ;; esac @@ -166,7 +171,37 @@ delete_ci_images() { done <<<"${tags}" } +delete_untagged_images() { + local container_name=$1 + local package_name + local versions_json + 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}") + + jq -r '.[] | select(((.metadata.container.tags // []) | length) == 0) | .id' \ + <<<"${versions_json}" \ + | while IFS= read -r version_id; do + if [[ -n "${version_id}" ]]; then + if [[ "${DRY_RUN}" == "true" ]]; then + echo "[DRY RUN] Would delete untagged image version ID ${version_id} from container ${container_name}." + else + echo "Deleting untagged image 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 + fi + done +} + +base_node_folders=$(find src/base_node -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | jq -R -s -c 'split("\n")[:-1]') 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]') @@ -177,6 +212,21 @@ for container_name in $(jq -r '.[]' <<<"${project_folders}"); do if [[ "${DELETE_CI}" == "true" ]]; then delete_ci_images "${container_name}" fi + if [[ "${DELETE_UNTAGGED}" == "true" ]]; then + delete_untagged_images "${container_name}" + fi +done + +for container_name in $(jq -r '.[]' <<<"${base_node_folders}"); do + if [[ "${DELETE_PR}" == "true" ]]; then + delete_pr_images "${container_name}" + fi + if [[ "${DELETE_CI}" == "true" ]]; then + delete_ci_images "${container_name}" + fi + if [[ "${DELETE_UNTAGGED}" == "true" ]]; then + delete_untagged_images "${container_name}" + fi done for container_name in $(jq -r '.[]' <<<"${language_folders}"); do @@ -186,6 +236,9 @@ for container_name in $(jq -r '.[]' <<<"${language_folders}"); do if [[ "${DELETE_CI}" == "true" ]]; then delete_ci_images "${container_name}" fi + if [[ "${DELETE_UNTAGGED}" == "true" ]]; then + delete_untagged_images "${container_name}" + fi done if [[ "${DELETE_PR}" == "true" ]]; then @@ -194,3 +247,6 @@ fi if [[ "${DELETE_CI}" == "true" ]]; then delete_ci_images "base" fi +if [[ "${DELETE_UNTAGGED}" == "true" ]]; then + delete_untagged_images "base" +fi diff --git a/.github/workflows/build_multi_arch_image.yml b/.github/workflows/build_multi_arch_image.yml index 36e3c0f..9d1f801 100644 --- a/.github/workflows/build_multi_arch_image.yml +++ b/.github/workflows/build_multi_arch_image.yml @@ -88,6 +88,7 @@ jobs: IMAGE_TAG: "${{ inputs.docker_tag }}-${{ matrix.arch }}" BASE_FOLDER: "${{ inputs.base_folder }}" NO_CACHE: '${{ inputs.NO_CACHE }}' + BUILDX_NO_DEFAULT_ATTESTATIONS: "1" - name: Check docker vulnerabilities - json output run: | make scan-image-json @@ -136,6 +137,55 @@ jobs: DOCKER_TAG: ${{ inputs.docker_tag }} CONTAINER_NAME: '${{ inputs.container_name }}' ARCHITECTURE: '${{ matrix.arch }}' + BUILDX_NO_DEFAULT_ATTESTATIONS: "1" + - name: Resolve image digest + id: resolve_arch_digest + run: | + DIGEST=$(docker buildx imagetools inspect "ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:${DOCKER_TAG}-${ARCHITECTURE}" | awk '/^Digest:/ {print $2; exit}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + echo "Resolved digest ${DIGEST} for ${DOCKER_TAG}-${ARCHITECTURE}" + env: + DOCKER_TAG: ${{ inputs.docker_tag }} + CONTAINER_NAME: '${{ inputs.container_name }}' + ARCHITECTURE: '${{ matrix.arch }}' + - name: Attest image + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a + with: + subject-name: ghcr.io/nhsdigital/eps-devcontainers/${{ inputs.container_name }} + subject-digest: ${{ steps.resolve_arch_digest.outputs.digest }} + push-to-registry: false + - name: Summarise attested image + run: | + echo "## ATTESTED IMAGE : ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:${DOCKER_TAG}-${ARCHITECTURE}@${DIGEST}" >> "$GITHUB_STEP_SUMMARY" + env: + DOCKER_TAG: ${{ inputs.docker_tag }} + CONTAINER_NAME: '${{ inputs.container_name }}' + ARCHITECTURE: '${{ matrix.arch }}' + DIGEST: ${{ steps.resolve_arch_digest.outputs.digest }} + - name: Resolve github actions image digest + id: resolve_githubactions_arch_digest + run: | + DIGEST=$(docker buildx imagetools inspect "ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:githubactions-${DOCKER_TAG}-${ARCHITECTURE}" | awk '/^Digest:/ {print $2; exit}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + echo "Resolved digest ${DIGEST} for githubactions-${DOCKER_TAG}-${ARCHITECTURE}" + env: + DOCKER_TAG: ${{ inputs.docker_tag }} + CONTAINER_NAME: '${{ inputs.container_name }}' + ARCHITECTURE: '${{ matrix.arch }}' + - name: Attest github actions image + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a + with: + subject-name: ghcr.io/nhsdigital/eps-devcontainers/${{ inputs.container_name }} + subject-digest: ${{ steps.resolve_githubactions_arch_digest.outputs.digest }} + push-to-registry: false + - name: Summarise attested github actions image + run: | + echo "## ATTESTED IMAGE : ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:githubactions-${DOCKER_TAG}-${ARCHITECTURE}@${DIGEST}" >> "$GITHUB_STEP_SUMMARY" + env: + DOCKER_TAG: ${{ inputs.docker_tag }} + CONTAINER_NAME: '${{ inputs.container_name }}' + ARCHITECTURE: '${{ matrix.arch }}' + DIGEST: ${{ steps.resolve_githubactions_arch_digest.outputs.digest }} - name: Push latest image if: ${{ inputs.tag_latest }} run: | @@ -152,6 +202,56 @@ jobs: DOCKER_TAG: ${{ inputs.docker_tag }} CONTAINER_NAME: '${{ inputs.container_name }}' ARCHITECTURE: '${{ matrix.arch }}' + - name: Resolve github actions latest image digest + if: ${{ inputs.tag_latest }} + id: resolve_githubactions_latest_arch_digest + run: | + DIGEST=$(docker buildx imagetools inspect "ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:githubactions-latest-${ARCHITECTURE}" | awk '/^Digest:/ {print $2; exit}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + echo "Resolved digest ${DIGEST} for githubactions-latest-${ARCHITECTURE}" + env: + CONTAINER_NAME: '${{ inputs.container_name }}' + ARCHITECTURE: '${{ matrix.arch }}' + - name: Attest github actions latest image + if: ${{ inputs.tag_latest }} + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a + with: + subject-name: ghcr.io/nhsdigital/eps-devcontainers/${{ inputs.container_name }} + subject-digest: ${{ steps.resolve_githubactions_latest_arch_digest.outputs.digest }} + push-to-registry: false + - name: Summarise attested github actions latest image + if: ${{ inputs.tag_latest }} + run: | + echo "## ATTESTED IMAGE : ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:githubactions-latest-${ARCHITECTURE}@${DIGEST}" >> "$GITHUB_STEP_SUMMARY" + env: + CONTAINER_NAME: '${{ inputs.container_name }}' + ARCHITECTURE: '${{ matrix.arch }}' + DIGEST: ${{ steps.resolve_githubactions_latest_arch_digest.outputs.digest }} + - name: Resolve latest image digest + if: ${{ inputs.tag_latest }} + id: resolve_latest_arch_digest + run: | + DIGEST=$(docker buildx imagetools inspect "ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:latest-${ARCHITECTURE}" | awk '/^Digest:/ {print $2; exit}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + echo "Resolved digest ${DIGEST} for latest-${ARCHITECTURE}" + env: + CONTAINER_NAME: '${{ inputs.container_name }}' + ARCHITECTURE: '${{ matrix.arch }}' + - name: Attest latest image + if: ${{ inputs.tag_latest }} + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a + with: + subject-name: ghcr.io/nhsdigital/eps-devcontainers/${{ inputs.container_name }} + subject-digest: ${{ steps.resolve_latest_arch_digest.outputs.digest }} + push-to-registry: false + - name: Summarise attested latest image + if: ${{ inputs.tag_latest }} + run: | + echo "## ATTESTED IMAGE : ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:latest-${ARCHITECTURE}@${DIGEST}" >> "$GITHUB_STEP_SUMMARY" + env: + CONTAINER_NAME: '${{ inputs.container_name }}' + ARCHITECTURE: '${{ matrix.arch }}' + DIGEST: ${{ steps.resolve_latest_arch_digest.outputs.digest }} publish_combined_image: name: Publish combined image for ${{ inputs.container_name }} runs-on: ubuntu-22.04 @@ -222,3 +322,101 @@ jobs: env: DOCKER_TAG: ${{ inputs.docker_tag }} CONTAINER_NAME: '${{ inputs.container_name }}' + + - name: Resolve combined image digest + id: resolve_combined_digest + run: | + DIGEST=$(docker buildx imagetools inspect "ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:${DOCKER_TAG}" | awk '/^Digest:/ {print $2; exit}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + echo "Resolved digest ${DIGEST} for ${DOCKER_TAG}" + env: + DOCKER_TAG: ${{ inputs.docker_tag }} + CONTAINER_NAME: '${{ inputs.container_name }}' + + - name: Attest combined image + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a + with: + subject-name: ghcr.io/nhsdigital/eps-devcontainers/${{ inputs.container_name }} + subject-digest: ${{ steps.resolve_combined_digest.outputs.digest }} + push-to-registry: false + - name: Summarise attested combined image + run: | + echo "## ATTESTED IMAGE : ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:${DOCKER_TAG}@${DIGEST}" >> "$GITHUB_STEP_SUMMARY" + env: + DOCKER_TAG: ${{ inputs.docker_tag }} + CONTAINER_NAME: '${{ inputs.container_name }}' + DIGEST: ${{ steps.resolve_combined_digest.outputs.digest }} + + - name: Resolve combined github actions image digest + id: resolve_githubactions_combined_digest + run: | + DIGEST=$(docker buildx imagetools inspect "ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:githubactions-${DOCKER_TAG}" | awk '/^Digest:/ {print $2; exit}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + echo "Resolved digest ${DIGEST} for githubactions-${DOCKER_TAG}" + env: + DOCKER_TAG: ${{ inputs.docker_tag }} + CONTAINER_NAME: '${{ inputs.container_name }}' + + - name: Attest combined github actions image + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a + with: + subject-name: ghcr.io/nhsdigital/eps-devcontainers/${{ inputs.container_name }} + subject-digest: ${{ steps.resolve_githubactions_combined_digest.outputs.digest }} + push-to-registry: false + - name: Summarise attested combined github actions image + run: | + echo "## ATTESTED IMAGE : ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:githubactions-${DOCKER_TAG}@${DIGEST}" >> "$GITHUB_STEP_SUMMARY" + env: + DOCKER_TAG: ${{ inputs.docker_tag }} + CONTAINER_NAME: '${{ inputs.container_name }}' + DIGEST: ${{ steps.resolve_githubactions_combined_digest.outputs.digest }} + + - name: Resolve latest github actions image digest + if: ${{ inputs.tag_latest }} + id: resolve_githubactions_latest_digest + run: | + DIGEST=$(docker buildx imagetools inspect "ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:githubactions-latest" | awk '/^Digest:/ {print $2; exit}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + echo "Resolved digest ${DIGEST} for githubactions-latest" + env: + CONTAINER_NAME: '${{ inputs.container_name }}' + + - name: Attest latest github actions image + if: ${{ inputs.tag_latest }} + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a + with: + subject-name: ghcr.io/nhsdigital/eps-devcontainers/${{ inputs.container_name }} + subject-digest: ${{ steps.resolve_githubactions_latest_digest.outputs.digest }} + push-to-registry: false + - name: Summarise attested latest github actions image + if: ${{ inputs.tag_latest }} + run: | + echo "## ATTESTED IMAGE : ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:githubactions-latest@${DIGEST}" >> "$GITHUB_STEP_SUMMARY" + env: + CONTAINER_NAME: '${{ inputs.container_name }}' + DIGEST: ${{ steps.resolve_githubactions_latest_digest.outputs.digest }} + + - name: Resolve latest image digest + if: ${{ inputs.tag_latest }} + id: resolve_latest_digest + run: | + DIGEST=$(docker buildx imagetools inspect "ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:latest" | awk '/^Digest:/ {print $2; exit}') + echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" + echo "Resolved digest ${DIGEST} for latest" + env: + CONTAINER_NAME: '${{ inputs.container_name }}' + + - name: Attest latest image + if: ${{ inputs.tag_latest }} + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a + with: + subject-name: ghcr.io/nhsdigital/eps-devcontainers/${{ inputs.container_name }} + subject-digest: ${{ steps.resolve_latest_digest.outputs.digest }} + push-to-registry: false + - name: Summarise attested latest image + if: ${{ inputs.tag_latest }} + run: | + echo "## ATTESTED IMAGE : ghcr.io/nhsdigital/eps-devcontainers/${CONTAINER_NAME}:latest@${DIGEST}" >> "$GITHUB_STEP_SUMMARY" + env: + CONTAINER_NAME: '${{ inputs.container_name }}' + DIGEST: ${{ steps.resolve_latest_digest.outputs.digest }} diff --git a/.github/workflows/delete_old_images.yml b/.github/workflows/delete_old_images.yml index 382ed5f..96438b5 100644 --- a/.github/workflows/delete_old_images.yml +++ b/.github/workflows/delete_old_images.yml @@ -4,7 +4,7 @@ name: "Delete old images" on: workflow_dispatch: schedule: - - cron: "0 1,13 * * *" + - cron: "0 1 * * 6" push: branches: [main] @@ -30,7 +30,7 @@ jobs: if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then .github/scripts/delete_unused_images.sh --delete-pr elif [[ "${{ github.event_name }}" == "schedule" ]]; then - .github/scripts/delete_unused_images.sh --delete-ci + .github/scripts/delete_unused_images.sh --delete-ci --delete-untagged else .github/scripts/delete_unused_images.sh fi