From 8f57df367db7bdea6e3a8b0ade00a1a8a237503a Mon Sep 17 00:00:00 2001 From: Jack Spagnoli Date: Tue, 24 Feb 2026 12:25:39 +0000 Subject: [PATCH 1/6] removes tool-versions --- .tool-versions | 5 ----- .tool-versions.asdf | 2 -- 2 files changed, 7 deletions(-) delete mode 100644 .tool-versions delete mode 100644 .tool-versions.asdf diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index d3c826a..0000000 --- a/.tool-versions +++ /dev/null @@ -1,5 +0,0 @@ -nodejs 24.13.0 -actionlint 1.7.10 -shellcheck 0.11.0 -python 3.14.3 -poetry 2.3.2 diff --git a/.tool-versions.asdf b/.tool-versions.asdf deleted file mode 100644 index 4921076..0000000 --- a/.tool-versions.asdf +++ /dev/null @@ -1,2 +0,0 @@ -# define the .asdf-version to use here -0.18.0 From b0d10d3aef2b259688c3ca6ededec01bbfd56252 Mon Sep 17 00:00:00 2001 From: Jack Spagnoli Date: Tue, 24 Feb 2026 12:27:05 +0000 Subject: [PATCH 2/6] replaces verify-attestation with get-repo-config --- .github/workflows/get-repo-config.yml | 145 ++++++++++++++++++ .github/workflows/pull_request.yml | 25 +-- .../workflows/quality-checks-devcontainer.yml | 20 +-- .github/workflows/release.yml | 25 +-- .../workflows/tag-release-devcontainer.yml | 10 +- .github/workflows/verify-attestation.yml | 111 -------------- 6 files changed, 159 insertions(+), 177 deletions(-) create mode 100644 .github/workflows/get-repo-config.yml delete mode 100644 .github/workflows/verify-attestation.yml diff --git a/.github/workflows/get-repo-config.yml b/.github/workflows/get-repo-config.yml new file mode 100644 index 0000000..8439187 --- /dev/null +++ b/.github/workflows/get-repo-config.yml @@ -0,0 +1,145 @@ +name: Get Repo Config and Image +on: + workflow_call: + inputs: + registry: + required: false + type: string + default: ghcr.io + namespace: + required: false + type: string + default: nhsdigital/eps-devcontainers + owner: + required: false + type: string + default: NHSDigital + verify_published_from_main_image: + required: false + type: boolean + default: true + predicate_type: + required: false + type: string + default: https://slsa.dev/provenance/v1 + outputs: + tag_format: + description: The tag format to be used for releases, as defined in .github/config/settings.yml + value: ${{ jobs.get_config_values.outputs.tag_format }} + devcontainer_image: + description: The devcontainer image name as defined in .devcontainer/devcontainer.json + value: ${{ jobs.get_config_values.outputs.devcontainer_image }} + devcontainer_version: + description: The devcontainer image version as defined in .devcontainer/devcontainer.json + value: ${{ jobs.get_config_values.outputs.devcontainer_version }} + pinned_image: + description: Fully-qualified digest-pinned image reference + value: ${{ jobs.verify_attestation.outputs.pinned_image }} + resolved_digest: + description: Resolved digest for the supplied image reference + value: ${{ jobs.verify_attestation.outputs.resolved_digest }} + +jobs: + get_config_values: + runs-on: ubuntu-22.04 + outputs: + tag_format: ${{ steps.load-config.outputs.TAG_FORMAT }} + devcontainer_version: ${{ steps.load-config.outputs.DEVCONTAINER_VERSION }} + devcontainer_image: ${{ steps.load-config.outputs.DEVCONTAINER_IMAGE }} + runtime_docker_image: ${{ steps.load-config.outputs.RUNTIME_DOCKER_IMAGE }} + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ env.BRANCH_NAME }} + fetch-depth: 0 + + - name: Load config value + id: load-config + run: | + TAG_FORMAT=$(yq '.TAG_FORMAT' .github/config/settings.yml) + DEVCONTAINER_IMAGE=$(jq -r '.build.args.IMAGE_NAME' .devcontainer/devcontainer.json) + DEVCONTAINER_VERSION=$(jq -r '.build.args.IMAGE_VERSION' .devcontainer/devcontainer.json) + RUNTIME_DOCKER_IMAGE="${DEVCONTAINER_IMAGE}:githubactions-${DEVCONTAINER_VERSION}" + { + echo "TAG_FORMAT=$TAG_FORMAT" + echo "DEVCONTAINER_IMAGE=$DEVCONTAINER_IMAGE" + echo "DEVCONTAINER_VERSION=$DEVCONTAINER_VERSION" + echo "RUNTIME_DOCKER_IMAGE=$RUNTIME_DOCKER_IMAGE" + } >> "$GITHUB_OUTPUT" + + verify_attestation: + runs-on: ubuntu-22.04 + needs: get_config_values + permissions: + contents: read + packages: read + attestations: read + outputs: + pinned_image: ${{ steps.resolve.outputs.pinned_image }} + resolved_digest: ${{ steps.resolve.outputs.resolved_digest }} + steps: + - name: Login to github container registry + if: startsWith(inputs.registry, 'ghcr.io') + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve digest + id: resolve + shell: bash + env: + RUNTIME_DOCKER_IMAGE: ${{ needs.get_config_values.outputs.runtime_docker_image }} + REGISTRY: ${{ inputs.registry }} + NAMESPACE: ${{ inputs.namespace }} + run: | + set -euo pipefail + + if [[ "$RUNTIME_DOCKER_IMAGE" == *"/"* ]]; then + IMAGE_REF="$RUNTIME_DOCKER_IMAGE" + else + IMAGE_REF="${REGISTRY}/${NAMESPACE}/${RUNTIME_DOCKER_IMAGE}" + fi + + if [[ "$IMAGE_REF" == *@sha256:* ]]; then + IMAGE_BASE="${IMAGE_REF%@*}" + RESOLVED_DIGEST="${IMAGE_REF#*@}" + else + RESOLVED_DIGEST="$(docker buildx imagetools inspect "$IMAGE_REF" | awk '/^Digest:/ {print $2; exit}')" + IMAGE_BASE="${IMAGE_REF%:*}" + fi + + if [[ -z "$RESOLVED_DIGEST" ]]; then + echo "Could not resolve digest for image: $IMAGE_REF" >&2 + exit 1 + fi + + PINNED_IMAGE="${IMAGE_BASE}@${RESOLVED_DIGEST}" + echo "resolved_digest=${RESOLVED_DIGEST}" >> "$GITHUB_OUTPUT" + echo "pinned_image=${PINNED_IMAGE}" >> "$GITHUB_OUTPUT" + echo "Resolved image reference: ${IMAGE_REF}" + echo "Resolved digest: ${RESOLVED_DIGEST}" + echo "Resolved image reference: ${PINNED_IMAGE}" + + - name: Verify attestation + shell: bash + env: + GH_TOKEN: ${{ github.token }} + OWNER: ${{ inputs.owner }} + VERIFY_PUBLISHED_FROM_MAIN_IMAGE: ${{ inputs.verify_published_from_main_image }} + PREDICATE_TYPE: ${{ inputs.predicate_type }} + PINNED_IMAGE: ${{ steps.resolve.outputs.pinned_image }} + run: | + set -euo pipefail + + args=("oci://${PINNED_IMAGE}" "--owner" "$OWNER" "--predicate-type" "$PREDICATE_TYPE") + + if [[ "$VERIFY_PUBLISHED_FROM_MAIN_IMAGE" == "true" ]]; then + args+=("--source-ref" "refs/heads/main") + fi + + + GH_FORCE_TTY=120 gh attestation verify "${args[@]}" 2>&1 + echo "Verified attestation for ${PINNED_IMAGE}" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cdb4ec7..885d442 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -17,31 +17,12 @@ jobs: pr_title_format_check: uses: ./.github/workflows/pr_title_check.yml get_config_values: - runs-on: ubuntu-22.04 - outputs: - tag_format: ${{ steps.load-config.outputs.TAG_FORMAT }} - devcontainer_version: ${{ steps.load-config.outputs.DEVCONTAINER_VERSION }} - devcontainer_image: ${{ steps.load-config.outputs.DEVCONTAINER_IMAGE }} - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Load config value - id: load-config - run: | - TAG_FORMAT=$(yq '.TAG_FORMAT' .github/config/settings.yml) - DEVCONTAINER_IMAGE=$(jq -r '.build.args.IMAGE_NAME' .devcontainer/devcontainer.json) - DEVCONTAINER_VERSION=$(jq -r '.build.args.IMAGE_VERSION' .devcontainer/devcontainer.json) - { - echo "TAG_FORMAT=$TAG_FORMAT" - echo "DEVCONTAINER_IMAGE=$DEVCONTAINER_IMAGE" - echo "DEVCONTAINER_VERSION=$DEVCONTAINER_VERSION" - } >> "$GITHUB_OUTPUT" + uses: ./.github/workflows/get-repo-config.yml quality_checks: uses: ./.github/workflows/quality-checks-devcontainer.yml needs: [get_config_values] with: - runtime_docker_image: "${{ needs.get_config_values.outputs.devcontainer_image }}:githubactions-${{ needs.get_config_values.outputs.devcontainer_version }}" + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} tag_release: @@ -53,7 +34,7 @@ jobs: attestations: read with: dry_run: true - runtime_docker_image: "${{ needs.get_config_values.outputs.devcontainer_image }}:githubactions-${{ needs.get_config_values.outputs.devcontainer_version }}" + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} branch_name: ${{ github.event.pull_request.head.ref }} tag_format: ${{ needs.get_config_values.outputs.tag_format }} verify_published_from_main_image: false diff --git a/.github/workflows/quality-checks-devcontainer.yml b/.github/workflows/quality-checks-devcontainer.yml index 6f8afad..4314c61 100644 --- a/.github/workflows/quality-checks-devcontainer.yml +++ b/.github/workflows/quality-checks-devcontainer.yml @@ -21,21 +21,15 @@ on: description: comma separated list of docker image references to scan when docker scanning is enabled. default: "" required: false - runtime_docker_image: + pinned_image: type: string required: true jobs: - verify_attestation: - uses: ./.github/workflows/verify-attestation.yml - with: - runtime_docker_image: "${{ inputs.runtime_docker_image }}" - verify_published_from_main_image: false quality_checks: runs-on: ubuntu-22.04 - needs: verify_attestation container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: @@ -213,9 +207,8 @@ jobs: get_docker_images_to_scan: runs-on: ubuntu-22.04 - needs: verify_attestation container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: @@ -283,9 +276,9 @@ jobs: docker_vulnerability_scan: runs-on: ubuntu-22.04 - needs: [get_docker_images_to_scan, verify_attestation] + needs: get_docker_images_to_scan container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: @@ -326,9 +319,8 @@ jobs: IaC-validation: runs-on: ubuntu-22.04 - needs: verify_attestation container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5aeaab9..66172a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,31 +9,12 @@ env: jobs: get_config_values: - runs-on: ubuntu-22.04 - outputs: - tag_format: ${{ steps.load-config.outputs.TAG_FORMAT }} - devcontainer_version: ${{ steps.load-config.outputs.DEVCONTAINER_VERSION }} - devcontainer_image: ${{ steps.load-config.outputs.DEVCONTAINER_IMAGE }} - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Load config value - id: load-config - run: | - TAG_FORMAT=$(yq '.TAG_FORMAT' .github/config/settings.yml) - DEVCONTAINER_IMAGE=$(jq -r '.build.args.IMAGE_NAME' .devcontainer/devcontainer.json) - DEVCONTAINER_VERSION=$(jq -r '.build.args.IMAGE_VERSION' .devcontainer/devcontainer.json) - { - echo "TAG_FORMAT=$TAG_FORMAT" - echo "DEVCONTAINER_IMAGE=$DEVCONTAINER_IMAGE" - echo "DEVCONTAINER_VERSION=$DEVCONTAINER_VERSION" - } >> "$GITHUB_OUTPUT" + uses: ./.github/workflows/get-repo-config.yml quality_checks: needs: [get_config_values] uses: ./.github/workflows/quality-checks-devcontainer.yml with: - runtime_docker_image: "${{ needs.get_config_values.outputs.devcontainer_image }}:githubactions-${{ needs.get_config_values.outputs.devcontainer_version }}" + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} tag_release: @@ -41,7 +22,7 @@ jobs: uses: ./.github/workflows/tag-release-devcontainer.yml with: dry_run: false - runtime_docker_image: "${{ needs.get_config_values.outputs.devcontainer_image }}:githubactions-${{ needs.get_config_values.outputs.devcontainer_version }}" + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} branch_name: main tag_format: ${{ needs.get_config_values.outputs.tag_format }} verify_published_from_main_image: true diff --git a/.github/workflows/tag-release-devcontainer.yml b/.github/workflows/tag-release-devcontainer.yml index ba93091..7647e7b 100644 --- a/.github/workflows/tag-release-devcontainer.yml +++ b/.github/workflows/tag-release-devcontainer.yml @@ -11,7 +11,7 @@ on: description: "The branch name to base the release on" required: true type: string - runtime_docker_image: + pinned_image: type: string required: true publish_packages: @@ -62,16 +62,10 @@ on: required: false description: "NPM token to publish packages" jobs: - verify_attestation: - uses: ./.github/workflows/verify-attestation.yml - with: - runtime_docker_image: "${{ inputs.runtime_docker_image }}" - verify_published_from_main_image: ${{ inputs.verify_published_from_main_image }} tag_release: runs-on: ubuntu-22.04 - needs: verify_attestation container: - image: ${{ needs.verify_attestation.outputs.pinned_image }} + image: ${{ inputs.pinned_image }} options: --user 1001:1001 --group-add 128 defaults: run: diff --git a/.github/workflows/verify-attestation.yml b/.github/workflows/verify-attestation.yml deleted file mode 100644 index a996e34..0000000 --- a/.github/workflows/verify-attestation.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Verify image digest and attestation -"on": - workflow_call: - inputs: - runtime_docker_image: - required: true - type: string - description: Image reference as name:tag (for example node_24_python_3_12:v1.2.3) or fully qualified image ref - registry: - required: false - type: string - default: ghcr.io - namespace: - required: false - type: string - default: nhsdigital/eps-devcontainers - owner: - required: false - type: string - default: NHSDigital - verify_published_from_main_image: - required: false - type: boolean - default: true - predicate_type: - required: false - type: string - default: https://slsa.dev/provenance/v1 - outputs: - pinned_image: - description: Fully-qualified digest-pinned image reference - value: ${{ jobs.verify_attestation.outputs.pinned_image }} - resolved_digest: - description: Resolved digest for the supplied image reference - value: ${{ jobs.verify_attestation.outputs.resolved_digest }} - -jobs: - verify_attestation: - runs-on: ubuntu-22.04 - permissions: - contents: read - packages: read - attestations: read - outputs: - pinned_image: ${{ steps.resolve.outputs.pinned_image }} - resolved_digest: ${{ steps.resolve.outputs.resolved_digest }} - steps: - - name: Login to github container registry - if: startsWith(inputs.registry, 'ghcr.io') - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Resolve digest - id: resolve - shell: bash - env: - RUNTIME_DOCKER_IMAGE: ${{ inputs.runtime_docker_image }} - REGISTRY: ${{ inputs.registry }} - NAMESPACE: ${{ inputs.namespace }} - run: | - set -euo pipefail - - if [[ "$RUNTIME_DOCKER_IMAGE" == *"/"* ]]; then - IMAGE_REF="$RUNTIME_DOCKER_IMAGE" - else - IMAGE_REF="${REGISTRY}/${NAMESPACE}/${RUNTIME_DOCKER_IMAGE}" - fi - - if [[ "$IMAGE_REF" == *@sha256:* ]]; then - IMAGE_BASE="${IMAGE_REF%@*}" - RESOLVED_DIGEST="${IMAGE_REF#*@}" - else - RESOLVED_DIGEST="$(docker buildx imagetools inspect "$IMAGE_REF" | awk '/^Digest:/ {print $2; exit}')" - IMAGE_BASE="${IMAGE_REF%:*}" - fi - - if [[ -z "$RESOLVED_DIGEST" ]]; then - echo "Could not resolve digest for image: $IMAGE_REF" >&2 - exit 1 - fi - - PINNED_IMAGE="${IMAGE_BASE}@${RESOLVED_DIGEST}" - echo "resolved_digest=${RESOLVED_DIGEST}" >> "$GITHUB_OUTPUT" - echo "pinned_image=${PINNED_IMAGE}" >> "$GITHUB_OUTPUT" - echo "Resolved image reference: ${IMAGE_REF}" - echo "Resolved digest: ${RESOLVED_DIGEST}" - echo "Resolved image reference: ${PINNED_IMAGE}" - - - name: Verify attestation - shell: bash - env: - GH_TOKEN: ${{ github.token }} - OWNER: ${{ inputs.owner }} - VERIFY_PUBLISHED_FROM_MAIN_IMAGE: ${{ inputs.verify_published_from_main_image }} - PREDICATE_TYPE: ${{ inputs.predicate_type }} - PINNED_IMAGE: ${{ steps.resolve.outputs.pinned_image }} - run: | - set -euo pipefail - - args=("oci://${PINNED_IMAGE}" "--owner" "$OWNER" "--predicate-type" "$PREDICATE_TYPE") - - if [[ "$VERIFY_PUBLISHED_FROM_MAIN_IMAGE" == "true" ]]; then - args+=("--source-ref" "refs/heads/main") - fi - - - GH_FORCE_TTY=120 gh attestation verify "${args[@]}" 2>&1 - echo "Verified attestation for ${PINNED_IMAGE}" From 71fc9d0b1dd65490d9fe0d4a47324e4dca4d076d Mon Sep 17 00:00:00 2001 From: Jack Spagnoli Date: Tue, 24 Feb 2026 12:29:48 +0000 Subject: [PATCH 3/6] tag_release not needing quality_checks --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 885d442..cd8efe1 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,7 +26,7 @@ jobs: secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} tag_release: - needs: [quality_checks, get_config_values] + needs: get_config_values uses: ./.github/workflows/tag-release-devcontainer.yml permissions: contents: read From 3aa6241fec969fa123b05062c2fc60c9b6a85a59 Mon Sep 17 00:00:00 2001 From: Jack Spagnoli Date: Tue, 24 Feb 2026 12:30:24 +0000 Subject: [PATCH 4/6] renames workflow for consistency --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cd8efe1..354eab6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,4 +1,4 @@ -name: pr +name: Pull Request on: pull_request: From a2ef32663a6d2cefe2f30277e3ad3dea2641d4f5 Mon Sep 17 00:00:00 2001 From: Jack Spagnoli Date: Tue, 24 Feb 2026 13:02:33 +0000 Subject: [PATCH 5/6] restores tool versions --- .tool-versions | 5 +++++ .tool-versions.asdf | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .tool-versions create mode 100644 .tool-versions.asdf diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..d3c826a --- /dev/null +++ b/.tool-versions @@ -0,0 +1,5 @@ +nodejs 24.13.0 +actionlint 1.7.10 +shellcheck 0.11.0 +python 3.14.3 +poetry 2.3.2 diff --git a/.tool-versions.asdf b/.tool-versions.asdf new file mode 100644 index 0000000..4921076 --- /dev/null +++ b/.tool-versions.asdf @@ -0,0 +1,2 @@ +# define the .asdf-version to use here +0.18.0 From d5074259f1f960bca461a7cfe2672df92070e972 Mon Sep 17 00:00:00 2001 From: Jack Spagnoli Date: Tue, 24 Feb 2026 13:20:46 +0000 Subject: [PATCH 6/6] prompt rerun