From d24966be31fa95c6fe2711a86458c64b09a6e641 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 17 Sep 2026 13:44:57 +0600 Subject: [PATCH 1/9] build: allow immutable publish inputs --- Dockerfile | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0a0dabb..0a7eeb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM httpd:alpine +ARG HTTPD_ALPINE_REF=httpd:alpine +FROM ${HTTPD_ALPINE_REF} LABEL org.opencontainers.image.source="https://github.com/infocyph/docker-apache" LABEL org.opencontainers.image.description="Hardened LocalDevStack Apache backend with PHP-FPM, TLS and HTTP/2 support" @@ -6,6 +7,8 @@ LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.authors="infocyph,abmmhasan" ARG TZ=Asia/Dhaka +ARG SCRIPTOMATIC_REF=main +ARG TOOLSET_RELEASE=latest ENV APACHE_LOG_DIR=/var/log/apache2 \ SERVER_NAME=localhost \ @@ -39,17 +42,26 @@ COPY scripts/healthcheck.sh /usr/local/bin/healthcheck RUN set -eux; \ curl -fsSL --retry 3 --retry-all-errors --retry-delay 1 \ --connect-timeout 10 --max-time 120 \ - "https://raw.githubusercontent.com/infocyph/Scriptomatic/main/bash/banner.sh" \ + "https://raw.githubusercontent.com/infocyph/Scriptomatic/${SCRIPTOMATIC_REF}/bash/banner.sh" \ -o /usr/local/bin/show-banner; \ test -s /usr/local/bin/show-banner; \ bash -n /usr/local/bin/show-banner; \ + if [ "$TOOLSET_RELEASE" = latest ]; then \ + toolset_installer_url="https://github.com/infocyph/Toolset/releases/latest/download/install.sh"; \ + else \ + toolset_installer_url="https://github.com/infocyph/Toolset/releases/download/${TOOLSET_RELEASE}/install.sh"; \ + fi; \ curl -fsSL --retry 3 --retry-all-errors --retry-delay 1 \ --connect-timeout 10 --max-time 120 \ - "https://github.com/infocyph/Toolset/releases/latest/download/install.sh" \ + "$toolset_installer_url" \ -o /tmp/toolset-install.sh; \ test -s /tmp/toolset-install.sh; \ bash -n /tmp/toolset-install.sh; \ - bash /tmp/toolset-install.sh --prefix /usr/local/bin chromacat; \ + if [ "$TOOLSET_RELEASE" = latest ]; then \ + bash /tmp/toolset-install.sh --latest --prefix /usr/local/bin chromacat; \ + else \ + bash /tmp/toolset-install.sh --release "$TOOLSET_RELEASE" --prefix /usr/local/bin chromacat; \ + fi; \ chromacat --version; \ rm -f /tmp/toolset-install.sh; \ chmod +x \ From a92cba4058d6612256849e12e4551aa10eee132c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 17 Sep 2026 13:45:47 +0600 Subject: [PATCH 2/9] ci: pin and safely revalidate publish upstreams --- .github/workflows/docker.publish.yml | 214 ++++++++++++++++++++++----- 1 file changed, 180 insertions(+), 34 deletions(-) diff --git a/.github/workflows/docker.publish.yml b/.github/workflows/docker.publish.yml index 52083af..869584a 100644 --- a/.github/workflows/docker.publish.yml +++ b/.github/workflows/docker.publish.yml @@ -126,9 +126,13 @@ jobs: set -euo pipefail retry() { - local attempt=1 + local attempt=1 max_attempts=5 until "$@"; do - (( attempt >= 5 )) && return 1 + if (( attempt >= max_attempts )); then + echo "Command failed after ${max_attempts} attempts: $*" >&2 + return 1 + fi + echo "Retrying failed command in $((attempt * 2)) seconds: $*" >&2 sleep "$((attempt * 2))" attempt=$((attempt + 1)) done @@ -136,39 +140,75 @@ jobs: get_httpd_digest() { local output digest - output="$(docker buildx imagetools inspect httpd:alpine 2>&1)" || { echo "$output" >&2; return 1; } - digest="$(printf '%s\n' "$output" | awk '$1 == "Digest:" {print $2; exit}')" - [[ -n "$digest" ]] || return 1 + output="$(docker buildx imagetools inspect httpd:alpine 2>&1)" || { + echo "$output" >&2 + return 1 + } + digest="$(awk '$1 == "Digest:" {print $2; exit}' <<<"$output")" + [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { + echo 'Could not resolve a valid httpd:alpine digest.' >&2 + return 1 + } printf '%s\n' "$digest" } get_scriptomatic_sha() { - git ls-remote https://github.com/infocyph/Scriptomatic.git refs/heads/main | awk 'NF {print $1; exit}' + local output sha + output="$(git ls-remote https://github.com/infocyph/Scriptomatic.git refs/heads/main 2>&1)" || { + echo "$output" >&2 + return 1 + } + sha="$(awk 'NF {print $1; exit}' <<<"$output")" + [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || { + echo 'Could not resolve a valid Scriptomatic main revision.' >&2 + return 1 + } + printf '%s\n' "$sha" } get_toolset_release() { - gh api repos/infocyph/Toolset/releases/latest --jq '.tag_name // empty' + local tag + tag="$(gh api repos/infocyph/Toolset/releases/latest --jq '.tag_name // empty')" || return 1 + [[ "$tag" =~ ^v?[0-9]+\.[0-9]+(\.[0-9]+)?(-rc\.[0-9]+)?$ ]] || { + echo "Could not resolve a valid latest Toolset release: $tag" >&2 + return 1 + } + printf '%s\n' "$tag" } get_installer_sha() { - local file + local release="$1" file checksum_output sha file="$(mktemp)" - curl --fail --silent --show-error --location \ + if ! curl --fail --silent --show-error --location \ --retry 5 --retry-all-errors --retry-delay 2 \ --connect-timeout 15 --max-time 120 \ - https://github.com/infocyph/Toolset/releases/latest/download/install.sh \ - -o "$file" - test -s "$file" - sha256sum "$file" | awk '{print $1}' + "https://github.com/infocyph/Toolset/releases/download/${release}/install.sh" \ + -o "$file"; then + rm -f "$file" + return 1 + fi + if [[ ! -s "$file" ]]; then + echo "Toolset installer is empty for release $release." >&2 + rm -f "$file" + return 1 + fi + checksum_output="$(sha256sum "$file")" || { + rm -f "$file" + return 1 + } rm -f "$file" + sha="${checksum_output%% *}" + [[ "$sha" =~ ^[0-9a-f]{64}$ ]] || { + echo "Could not calculate Toolset installer checksum for $release." >&2 + return 1 + } + printf '%s\n' "$sha" } HTTPD_ALPINE_DIGEST="$(retry get_httpd_digest)" SCRIPTOMATIC_MAIN_SHA="$(retry get_scriptomatic_sha)" TOOLSET_RELEASE="$(retry get_toolset_release)" - TOOLSET_INSTALLER_SHA256="$(retry get_installer_sha)" - - [[ -n "$HTTPD_ALPINE_DIGEST" && -n "$SCRIPTOMATIC_MAIN_SHA" && -n "$TOOLSET_RELEASE" && -n "$TOOLSET_INSTALLER_SHA256" ]] + TOOLSET_INSTALLER_SHA256="$(retry get_installer_sha "$TOOLSET_RELEASE")" { echo "HTTPD_ALPINE_DIGEST=$HTTPD_ALPINE_DIGEST" @@ -177,6 +217,15 @@ jobs: echo "TOOLSET_INSTALLER_SHA256=$TOOLSET_INSTALLER_SHA256" } >> "$GITHUB_ENV" + { + echo '## Upstream snapshot' + echo + echo "- httpd:alpine: \`$HTTPD_ALPINE_DIGEST\`" + echo "- Scriptomatic main: \`$SCRIPTOMATIC_MAIN_SHA\`" + echo "- Toolset release: \`$TOOLSET_RELEASE\`" + echo "- Toolset installer SHA-256: \`$TOOLSET_INSTALLER_SHA256\`" + } >> "$GITHUB_STEP_SUMMARY" + - name: Build fresh amd64 release candidate uses: docker/build-push-action@v7 with: @@ -187,6 +236,10 @@ jobs: no-cache: true push: false tags: infocyph/apache:publish-candidate + build-args: | + HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }} + SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }} + TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }} cache-to: type=gha,scope=apache-publish-amd64,mode=max - name: Record candidate runtime resolution @@ -229,6 +282,10 @@ jobs: no-cache: true push: false tags: infocyph/apache:publish-candidate-arm64 + build-args: | + HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }} + SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }} + TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }} cache-to: type=gha,scope=apache-publish-arm64,mode=max - name: Final arm64 release-candidate gate @@ -249,22 +306,107 @@ jobs: run: | set -euo pipefail - current_httpd="$(docker buildx imagetools inspect httpd:alpine | awk '$1 == "Digest:" {print $2; exit}')" - current_scriptomatic="$(git ls-remote https://github.com/infocyph/Scriptomatic.git refs/heads/main | awk 'NF {print $1; exit}')" - current_toolset="$(gh api repos/infocyph/Toolset/releases/latest --jq '.tag_name // empty')" - file="$(mktemp)" - trap 'rm -f "$file"' EXIT - curl --fail --silent --show-error --location \ - --retry 5 --retry-all-errors --retry-delay 2 \ - --connect-timeout 15 --max-time 120 \ - https://github.com/infocyph/Toolset/releases/latest/download/install.sh \ - -o "$file" - current_installer="$(sha256sum "$file" | awk '{print $1}')" - - test "$current_httpd" = "$HTTPD_ALPINE_DIGEST" - test "$current_scriptomatic" = "$SCRIPTOMATIC_MAIN_SHA" - test "$current_toolset" = "$TOOLSET_RELEASE" - test "$current_installer" = "$TOOLSET_INSTALLER_SHA256" + retry() { + local attempt=1 max_attempts=5 + until "$@"; do + if (( attempt >= max_attempts )); then + echo "Command failed after ${max_attempts} attempts: $*" >&2 + return 1 + fi + echo "Retrying failed command in $((attempt * 2)) seconds: $*" >&2 + sleep "$((attempt * 2))" + attempt=$((attempt + 1)) + done + } + + get_httpd_digest() { + local output digest + output="$(docker buildx imagetools inspect httpd:alpine 2>&1)" || { + echo "$output" >&2 + return 1 + } + digest="$(awk '$1 == "Digest:" {print $2; exit}' <<<"$output")" + [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { + echo 'Could not resolve a valid httpd:alpine digest.' >&2 + return 1 + } + printf '%s\n' "$digest" + } + + get_scriptomatic_sha() { + local output sha + output="$(git ls-remote https://github.com/infocyph/Scriptomatic.git refs/heads/main 2>&1)" || { + echo "$output" >&2 + return 1 + } + sha="$(awk 'NF {print $1; exit}' <<<"$output")" + [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || { + echo 'Could not resolve a valid Scriptomatic main revision.' >&2 + return 1 + } + printf '%s\n' "$sha" + } + + get_toolset_release() { + local tag + tag="$(gh api repos/infocyph/Toolset/releases/latest --jq '.tag_name // empty')" || return 1 + [[ "$tag" =~ ^v?[0-9]+\.[0-9]+(\.[0-9]+)?(-rc\.[0-9]+)?$ ]] || { + echo "Could not resolve a valid latest Toolset release: $tag" >&2 + return 1 + } + printf '%s\n' "$tag" + } + + get_installer_sha() { + local release="$1" file checksum_output sha + file="$(mktemp)" + if ! curl --fail --silent --show-error --location \ + --retry 5 --retry-all-errors --retry-delay 2 \ + --connect-timeout 15 --max-time 120 \ + "https://github.com/infocyph/Toolset/releases/download/${release}/install.sh" \ + -o "$file"; then + rm -f "$file" + return 1 + fi + if [[ ! -s "$file" ]]; then + echo "Toolset installer is empty for release $release." >&2 + rm -f "$file" + return 1 + fi + checksum_output="$(sha256sum "$file")" || { + rm -f "$file" + return 1 + } + rm -f "$file" + sha="${checksum_output%% *}" + [[ "$sha" =~ ^[0-9a-f]{64}$ ]] || { + echo "Could not calculate Toolset installer checksum for $release." >&2 + return 1 + } + printf '%s\n' "$sha" + } + + compare_value() { + local name="$1" expected="$2" actual="$3" + if [[ "$expected" != "$actual" ]]; then + echo "$name changed during the publish build:" >&2 + echo " snapshot: $expected" >&2 + echo " current: $actual" >&2 + return 1 + fi + } + + current_httpd="$(retry get_httpd_digest)" + current_scriptomatic="$(retry get_scriptomatic_sha)" + current_toolset="$(retry get_toolset_release)" + current_installer="$(retry get_installer_sha "$current_toolset")" + + compare_value 'httpd:alpine digest' "$HTTPD_ALPINE_DIGEST" "$current_httpd" + compare_value 'Scriptomatic main revision' "$SCRIPTOMATIC_MAIN_SHA" "$current_scriptomatic" + compare_value 'Toolset release' "$TOOLSET_RELEASE" "$current_toolset" + compare_value 'Toolset installer checksum' "$TOOLSET_INSTALLER_SHA256" "$current_installer" + + echo 'Rolling upstream revalidation passed.' - name: Log in to Docker Hub uses: docker/login-action@v4 @@ -331,6 +473,10 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }} + SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }} + TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }} cache-from: | type=gha,scope=apache-publish-amd64 type=gha,scope=apache-publish-arm64 @@ -362,8 +508,8 @@ jobs: local ref="$1" output digest for _ in $(seq 1 12); do output="$(docker buildx imagetools inspect "$ref" 2>&1 || true)" - digest="$(printf '%s\n' "$output" | awk '/^Digest:/ {print $2; exit}')" - if [[ -n "$digest" ]]; then + digest="$(awk '/^Digest:/ {print $2; exit}' <<<"$output")" + if [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then printf '%s\n' "$digest" return 0 fi From 8200e9a593b3cce2567c11a0cb5cd3dc27010fae Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 17 Sep 2026 13:46:00 +0600 Subject: [PATCH 3/9] test: cover rolling defaults and publish pins --- tests/static.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/static.sh b/tests/static.sh index e614061..f62c8cd 100755 --- a/tests/static.sh +++ b/tests/static.sh @@ -19,7 +19,10 @@ done shellcheck scripts/*.sh tests/*.sh -grep -Fq 'FROM httpd:alpine' Dockerfile +grep -Fq 'ARG HTTPD_ALPINE_REF=httpd:alpine' Dockerfile +grep -Fq 'FROM ${HTTPD_ALPINE_REF}' Dockerfile +grep -Fq 'ARG SCRIPTOMATIC_REF=main' Dockerfile +grep -Fq 'ARG TOOLSET_RELEASE=latest' Dockerfile grep -Fq 'apk upgrade --no-cache' Dockerfile grep -Fq 'apache2-utils' Dockerfile if grep -Eq '^[[:space:]]+apache-mod-fcgid([[:space:]\\;]|$)' Dockerfile; then @@ -28,8 +31,11 @@ fi if grep -Eq '^[[:space:]]+apache2([[:space:]\\;]|$)' Dockerfile; then fail 'Alpine apache2 server package must not be installed' fi -grep -Fq 'Scriptomatic/main/bash/banner.sh' Dockerfile +grep -Fq 'Scriptomatic/${SCRIPTOMATIC_REF}/bash/banner.sh' Dockerfile grep -Fq 'Toolset/releases/latest/download/install.sh' Dockerfile +grep -Fq 'Toolset/releases/download/${TOOLSET_RELEASE}/install.sh' Dockerfile +grep -Fq -- '--release "$TOOLSET_RELEASE"' Dockerfile +grep -Fq -- '--latest --prefix /usr/local/bin chromacat' Dockerfile helper_downloads="$(grep -c -- '--connect-timeout 10 --max-time 120' Dockerfile)" if [[ "$helper_downloads" -ne 2 ]]; then fail 'Both helper downloads must use bounded connect and total timeouts' From 3ab71443025f8d201cf75fafad9be234b12fb4a8 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 17 Sep 2026 13:46:11 +0600 Subject: [PATCH 4/9] test: enforce reproducible publish inputs --- tests/release-contract.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/release-contract.sh b/tests/release-contract.sh index 1d4f5ca..1c1b014 100755 --- a/tests/release-contract.sh +++ b/tests/release-contract.sh @@ -16,6 +16,13 @@ for contract in \ "releases/tags/\${MANUAL_RELEASE_TAG}" \ 'PUBLISH_RELEASE_TAG' \ 'Enforce immutable release tags' \ + 'retry get_httpd_digest' \ + 'retry get_scriptomatic_sha' \ + 'retry get_toolset_release' \ + 'compare_value' \ + "HTTPD_ALPINE_REF=httpd:alpine@\${{ env.HTTPD_ALPINE_DIGEST }}" \ + "SCRIPTOMATIC_REF=\${{ env.SCRIPTOMATIC_MAIN_SHA }}" \ + "TOOLSET_RELEASE=\${{ env.TOOLSET_RELEASE }}" \ 'linux/amd64,linux/arm64' \ 'provenance: mode=max' \ 'sbom: true'; do @@ -25,6 +32,25 @@ done grep -Fq "cron: '0 0 * * 0'" "$workflow" grep -Fq 'types: [published]' "$workflow" +pinned_builds="$(grep -c 'HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }}' "$workflow")" +if [[ "$pinned_builds" -ne 3 ]]; then + echo "Expected all three publish builds to pin the httpd digest; found $pinned_builds." >&2 + exit 1 +fi + +if grep -Fq 'imagetools inspect httpd:alpine | awk' "$workflow"; then + echo 'Unsafe pipefail-sensitive httpd digest pipeline detected.' >&2 + exit 1 +fi +if grep -Fq 'Scriptomatic.git refs/heads/main | awk' "$workflow"; then + echo 'Unsafe pipefail-sensitive Scriptomatic revision pipeline detected.' >&2 + exit 1 +fi +if grep -Fq 'Toolset/releases/latest/download/install.sh' "$workflow"; then + echo 'Publish workflow must checksum an exact Toolset release installer.' >&2 + exit 1 +fi + if grep -Fq 'actions/checkout@v4' "$workflow"; then echo 'Legacy checkout action detected.' >&2 exit 1 From 020e6f545b6e5adec24c3eb27019ddd634fdc484 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 17 Sep 2026 13:47:41 +0600 Subject: [PATCH 5/9] build: verify pinned Toolset installer --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 0a7eeb2..f112076 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ LABEL org.opencontainers.image.authors="infocyph,abmmhasan" ARG TZ=Asia/Dhaka ARG SCRIPTOMATIC_REF=main ARG TOOLSET_RELEASE=latest +ARG TOOLSET_INSTALLER_SHA256= ENV APACHE_LOG_DIR=/var/log/apache2 \ SERVER_NAME=localhost \ @@ -56,6 +57,9 @@ RUN set -eux; \ "$toolset_installer_url" \ -o /tmp/toolset-install.sh; \ test -s /tmp/toolset-install.sh; \ + if [ -n "$TOOLSET_INSTALLER_SHA256" ]; then \ + printf '%s %s\n' "$TOOLSET_INSTALLER_SHA256" /tmp/toolset-install.sh | sha256sum -c -; \ + fi; \ bash -n /tmp/toolset-install.sh; \ if [ "$TOOLSET_RELEASE" = latest ]; then \ bash /tmp/toolset-install.sh --latest --prefix /usr/local/bin chromacat; \ From 4c8fc868259591a11819d515a411ae4efedc404a Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 17 Sep 2026 13:48:18 +0600 Subject: [PATCH 6/9] ci: publish only from immutable snapshot inputs --- .github/workflows/docker.publish.yml | 111 +-------------------------- 1 file changed, 3 insertions(+), 108 deletions(-) diff --git a/.github/workflows/docker.publish.yml b/.github/workflows/docker.publish.yml index 869584a..3594ded 100644 --- a/.github/workflows/docker.publish.yml +++ b/.github/workflows/docker.publish.yml @@ -240,6 +240,7 @@ jobs: HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }} SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }} TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }} + TOOLSET_INSTALLER_SHA256=${{ env.TOOLSET_INSTALLER_SHA256 }} cache-to: type=gha,scope=apache-publish-amd64,mode=max - name: Record candidate runtime resolution @@ -286,6 +287,7 @@ jobs: HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }} SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }} TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }} + TOOLSET_INSTALLER_SHA256=${{ env.TOOLSET_INSTALLER_SHA256 }} cache-to: type=gha,scope=apache-publish-arm64,mode=max - name: Final arm64 release-candidate gate @@ -300,114 +302,6 @@ jobs: chromacat --version ' - - name: Revalidate rolling upstreams before publish - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - - retry() { - local attempt=1 max_attempts=5 - until "$@"; do - if (( attempt >= max_attempts )); then - echo "Command failed after ${max_attempts} attempts: $*" >&2 - return 1 - fi - echo "Retrying failed command in $((attempt * 2)) seconds: $*" >&2 - sleep "$((attempt * 2))" - attempt=$((attempt + 1)) - done - } - - get_httpd_digest() { - local output digest - output="$(docker buildx imagetools inspect httpd:alpine 2>&1)" || { - echo "$output" >&2 - return 1 - } - digest="$(awk '$1 == "Digest:" {print $2; exit}' <<<"$output")" - [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { - echo 'Could not resolve a valid httpd:alpine digest.' >&2 - return 1 - } - printf '%s\n' "$digest" - } - - get_scriptomatic_sha() { - local output sha - output="$(git ls-remote https://github.com/infocyph/Scriptomatic.git refs/heads/main 2>&1)" || { - echo "$output" >&2 - return 1 - } - sha="$(awk 'NF {print $1; exit}' <<<"$output")" - [[ "$sha" =~ ^[0-9a-f]{40}$ ]] || { - echo 'Could not resolve a valid Scriptomatic main revision.' >&2 - return 1 - } - printf '%s\n' "$sha" - } - - get_toolset_release() { - local tag - tag="$(gh api repos/infocyph/Toolset/releases/latest --jq '.tag_name // empty')" || return 1 - [[ "$tag" =~ ^v?[0-9]+\.[0-9]+(\.[0-9]+)?(-rc\.[0-9]+)?$ ]] || { - echo "Could not resolve a valid latest Toolset release: $tag" >&2 - return 1 - } - printf '%s\n' "$tag" - } - - get_installer_sha() { - local release="$1" file checksum_output sha - file="$(mktemp)" - if ! curl --fail --silent --show-error --location \ - --retry 5 --retry-all-errors --retry-delay 2 \ - --connect-timeout 15 --max-time 120 \ - "https://github.com/infocyph/Toolset/releases/download/${release}/install.sh" \ - -o "$file"; then - rm -f "$file" - return 1 - fi - if [[ ! -s "$file" ]]; then - echo "Toolset installer is empty for release $release." >&2 - rm -f "$file" - return 1 - fi - checksum_output="$(sha256sum "$file")" || { - rm -f "$file" - return 1 - } - rm -f "$file" - sha="${checksum_output%% *}" - [[ "$sha" =~ ^[0-9a-f]{64}$ ]] || { - echo "Could not calculate Toolset installer checksum for $release." >&2 - return 1 - } - printf '%s\n' "$sha" - } - - compare_value() { - local name="$1" expected="$2" actual="$3" - if [[ "$expected" != "$actual" ]]; then - echo "$name changed during the publish build:" >&2 - echo " snapshot: $expected" >&2 - echo " current: $actual" >&2 - return 1 - fi - } - - current_httpd="$(retry get_httpd_digest)" - current_scriptomatic="$(retry get_scriptomatic_sha)" - current_toolset="$(retry get_toolset_release)" - current_installer="$(retry get_installer_sha "$current_toolset")" - - compare_value 'httpd:alpine digest' "$HTTPD_ALPINE_DIGEST" "$current_httpd" - compare_value 'Scriptomatic main revision' "$SCRIPTOMATIC_MAIN_SHA" "$current_scriptomatic" - compare_value 'Toolset release' "$TOOLSET_RELEASE" "$current_toolset" - compare_value 'Toolset installer checksum' "$TOOLSET_INSTALLER_SHA256" "$current_installer" - - echo 'Rolling upstream revalidation passed.' - - name: Log in to Docker Hub uses: docker/login-action@v4 with: @@ -477,6 +371,7 @@ jobs: HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }} SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }} TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }} + TOOLSET_INSTALLER_SHA256=${{ env.TOOLSET_INSTALLER_SHA256 }} cache-from: | type=gha,scope=apache-publish-amd64 type=gha,scope=apache-publish-arm64 From 4440f0ab0121df41ec57a06f6e02f41b748f7c75 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 17 Sep 2026 14:03:10 +0600 Subject: [PATCH 7/9] test: align publish contracts with immutable snapshot flow --- tests/release-contract.sh | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/release-contract.sh b/tests/release-contract.sh index 1c1b014..267997b 100755 --- a/tests/release-contract.sh +++ b/tests/release-contract.sh @@ -16,13 +16,14 @@ for contract in \ "releases/tags/\${MANUAL_RELEASE_TAG}" \ 'PUBLISH_RELEASE_TAG' \ 'Enforce immutable release tags' \ + 'Snapshot rolling upstream inputs' \ 'retry get_httpd_digest' \ 'retry get_scriptomatic_sha' \ 'retry get_toolset_release' \ - 'compare_value' \ "HTTPD_ALPINE_REF=httpd:alpine@\${{ env.HTTPD_ALPINE_DIGEST }}" \ "SCRIPTOMATIC_REF=\${{ env.SCRIPTOMATIC_MAIN_SHA }}" \ "TOOLSET_RELEASE=\${{ env.TOOLSET_RELEASE }}" \ + "TOOLSET_INSTALLER_SHA256=\${{ env.TOOLSET_INSTALLER_SHA256 }}" \ 'linux/amd64,linux/arm64' \ 'provenance: mode=max' \ 'sbom: true'; do @@ -32,12 +33,22 @@ done grep -Fq "cron: '0 0 * * 0'" "$workflow" grep -Fq 'types: [published]' "$workflow" -pinned_builds="$(grep -c 'HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }}' "$workflow")" -if [[ "$pinned_builds" -ne 3 ]]; then - echo "Expected all three publish builds to pin the httpd digest; found $pinned_builds." >&2 +for pinned_contract in \ + 'HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }}' \ + 'SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }}' \ + 'TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }}' \ + 'TOOLSET_INSTALLER_SHA256=${{ env.TOOLSET_INSTALLER_SHA256 }}'; do + pinned_builds="$(grep -cF "$pinned_contract" "$workflow")" + if [[ "$pinned_builds" -ne 3 ]]; then + echo "Expected all three publish builds to use pinned input: $pinned_contract; found $pinned_builds." >&2 + exit 1 + fi +done + +if grep -Fq 'Revalidate rolling upstreams before publish' "$workflow"; then + echo 'Publish workflow must not re-resolve mutable upstreams after immutable snapshotting.' >&2 exit 1 fi - if grep -Fq 'imagetools inspect httpd:alpine | awk' "$workflow"; then echo 'Unsafe pipefail-sensitive httpd digest pipeline detected.' >&2 exit 1 From 0010435eb8a663756831588c0dc41e17ae2b0ccd Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 17 Sep 2026 14:04:12 +0600 Subject: [PATCH 8/9] test: satisfy shellcheck for literal build contracts --- tests/static.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/static.sh b/tests/static.sh index f62c8cd..f469c87 100755 --- a/tests/static.sh +++ b/tests/static.sh @@ -20,7 +20,7 @@ done shellcheck scripts/*.sh tests/*.sh grep -Fq 'ARG HTTPD_ALPINE_REF=httpd:alpine' Dockerfile -grep -Fq 'FROM ${HTTPD_ALPINE_REF}' Dockerfile +grep -Fq "FROM \${HTTPD_ALPINE_REF}" Dockerfile grep -Fq 'ARG SCRIPTOMATIC_REF=main' Dockerfile grep -Fq 'ARG TOOLSET_RELEASE=latest' Dockerfile grep -Fq 'apk upgrade --no-cache' Dockerfile @@ -31,10 +31,10 @@ fi if grep -Eq '^[[:space:]]+apache2([[:space:]\\;]|$)' Dockerfile; then fail 'Alpine apache2 server package must not be installed' fi -grep -Fq 'Scriptomatic/${SCRIPTOMATIC_REF}/bash/banner.sh' Dockerfile +grep -Fq "Scriptomatic/\${SCRIPTOMATIC_REF}/bash/banner.sh" Dockerfile grep -Fq 'Toolset/releases/latest/download/install.sh' Dockerfile -grep -Fq 'Toolset/releases/download/${TOOLSET_RELEASE}/install.sh' Dockerfile -grep -Fq -- '--release "$TOOLSET_RELEASE"' Dockerfile +grep -Fq "Toolset/releases/download/\${TOOLSET_RELEASE}/install.sh" Dockerfile +grep -Fq -- "--release \"\$TOOLSET_RELEASE\"" Dockerfile grep -Fq -- '--latest --prefix /usr/local/bin chromacat' Dockerfile helper_downloads="$(grep -c -- '--connect-timeout 10 --max-time 120' Dockerfile)" if [[ "$helper_downloads" -ne 2 ]]; then From a3ff04f5cc025843c065b2646883c9bceaed17b7 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 17 Sep 2026 14:04:24 +0600 Subject: [PATCH 9/9] test: quote literal workflow expressions --- tests/release-contract.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/release-contract.sh b/tests/release-contract.sh index 267997b..1e6bb73 100755 --- a/tests/release-contract.sh +++ b/tests/release-contract.sh @@ -34,10 +34,10 @@ grep -Fq "cron: '0 0 * * 0'" "$workflow" grep -Fq 'types: [published]' "$workflow" for pinned_contract in \ - 'HTTPD_ALPINE_REF=httpd:alpine@${{ env.HTTPD_ALPINE_DIGEST }}' \ - 'SCRIPTOMATIC_REF=${{ env.SCRIPTOMATIC_MAIN_SHA }}' \ - 'TOOLSET_RELEASE=${{ env.TOOLSET_RELEASE }}' \ - 'TOOLSET_INSTALLER_SHA256=${{ env.TOOLSET_INSTALLER_SHA256 }}'; do + "HTTPD_ALPINE_REF=httpd:alpine@\${{ env.HTTPD_ALPINE_DIGEST }}" \ + "SCRIPTOMATIC_REF=\${{ env.SCRIPTOMATIC_MAIN_SHA }}" \ + "TOOLSET_RELEASE=\${{ env.TOOLSET_RELEASE }}" \ + "TOOLSET_INSTALLER_SHA256=\${{ env.TOOLSET_INSTALLER_SHA256 }}"; do pinned_builds="$(grep -cF "$pinned_contract" "$workflow")" if [[ "$pinned_builds" -ne 3 ]]; then echo "Expected all three publish builds to use pinned input: $pinned_contract; found $pinned_builds." >&2