diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e9a1ba2..6572217 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,6 +10,9 @@ on: permissions: contents: read +env: + CI_RECIPES_VERSION: 6e790adf553ecff9f5ba5a3d0beeb9a9256a29ee + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -26,6 +29,19 @@ jobs: with: fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: 1.27.x + cache: false + + - name: Install CI recipes + env: + GOBIN: ${{ runner.temp }}/ci-recipes-bin + run: | + go install "github.com/soulteary/ci-recipes/cmd/ci-recipes@${CI_RECIPES_VERSION}" + echo "${GOBIN}" >> "${GITHUB_PATH}" + - name: Install ShellCheck env: SHELLCHECK_VERSION: 0.11.0 @@ -58,7 +74,6 @@ jobs: .dockerignore \ Dockerfile \ docker-entrypoint-sqlite.sh \ - scripts/validate-release.sh \ sqlite-database-integration-loader.php \ sqlite-diagnostics.php \ sqlite-local-core-update.php \ @@ -83,7 +98,7 @@ jobs: echo "Expected IMAGE_VERSION in Dockerfile." >&2 exit 1 fi - ./scripts/validate-release.sh "${release_version}" + ci-recipes docker-sqlite-wordpress validate-release "${release_version}" echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}" - name: Lint shell scripts @@ -123,6 +138,13 @@ jobs: tar --no-same-owner -xzf "${archive}" -C /tmp actionlint /tmp/actionlint + - name: Verify pinned WordPress base image + if: steps.changes.outputs.image_changed == 'true' + run: >- + ci-recipes docker-sqlite-wordpress validate-release + "${{ steps.release.outputs.release_version }}" + --verify-upstream + image-smoke: needs: validate if: needs.validate.outputs.image_changed == 'true' @@ -155,9 +177,6 @@ jobs: with: persist-credentials: false - - name: Verify pinned WordPress base image - run: ./scripts/validate-release.sh "${{ needs.validate.outputs.release_version }}" --verify-upstream - - name: Set up QEMU if: ${{ matrix.qemu }} uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ab4d919..d81ae54 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -17,6 +17,7 @@ concurrency: cancel-in-progress: false env: + CI_RECIPES_VERSION: 6e790adf553ecff9f5ba5a3d0beeb9a9256a29ee REGISTRY_IMAGE: soulteary/sqlite-wordpress GHCR_IMAGE: ghcr.io/soulteary/sqlite-wordpress @@ -39,6 +40,19 @@ jobs: persist-credentials: false ref: ${{ github.event.release.tag_name || github.ref }} + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: 1.27.x + cache: false + + - name: Install CI recipes + env: + GOBIN: ${{ runner.temp }}/ci-recipes-bin + run: | + go install "github.com/soulteary/ci-recipes/cmd/ci-recipes@${CI_RECIPES_VERSION}" + echo "${GOBIN}" >> "${GITHUB_PATH}" + - name: Validate release tag and project versions id: release env: @@ -83,7 +97,7 @@ jobs: exit 1 fi - ./scripts/validate-release.sh "${RELEASE_REF_NAME}" + ci-recipes docker-sqlite-wordpress validate-release "${RELEASE_REF_NAME}" echo "version=${RELEASE_REF_NAME}" >> "${GITHUB_OUTPUT}" echo "source_sha=${source_sha}" >> "${GITHUB_OUTPUT}" @@ -106,7 +120,10 @@ jobs: - name: Verify pinned WordPress base image env: RELEASE_VERSION: ${{ steps.release.outputs.version }} - run: ./scripts/validate-release.sh "${RELEASE_VERSION}" --verify-upstream + run: >- + ci-recipes docker-sqlite-wordpress validate-release + "${RELEASE_VERSION}" + --verify-upstream - name: Select fresh or resumable publication id: registry @@ -297,6 +314,19 @@ jobs: with: persist-credentials: false + - name: Set up Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: 1.27.x + cache: false + + - name: Install CI recipes + env: + GOBIN: ${{ runner.temp }}/ci-recipes-bin + run: | + go install "github.com/soulteary/ci-recipes/cmd/ci-recipes@${CI_RECIPES_VERSION}" + echo "${GOBIN}" >> "${GITHUB_PATH}" + - name: Download digests if: env.RESUME_EXISTING != 'true' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -422,12 +452,12 @@ jobs: jq -Rsc 'split("\n") | map(select(length > 0))' <<< "${expected_platforms}" )" sbom="$(docker buildx imagetools inspect "${image}:${RELEASE_VERSION}" --format '{{ json .SBOM }}')" - if ! bash ./scripts/validate-buildx-evidence.sh SPDX "${expected_evidence_platforms}" <<< "${sbom}"; then + if ! ci-recipes docker-sqlite-wordpress validate-buildx-evidence SPDX "${expected_evidence_platforms}" <<< "${sbom}"; then echo "::error::Incomplete per-platform SPDX SBOM for ${image}:${RELEASE_VERSION}." exit 1 fi provenance="$(docker buildx imagetools inspect "${image}:${RELEASE_VERSION}" --format '{{ json .Provenance }}')" - if ! bash ./scripts/validate-buildx-evidence.sh SLSA "${expected_evidence_platforms}" <<< "${provenance}"; then + if ! ci-recipes docker-sqlite-wordpress validate-buildx-evidence SLSA "${expected_evidence_platforms}" <<< "${provenance}"; then echo "::error::Incomplete per-platform SLSA provenance for ${image}:${RELEASE_VERSION}." exit 1 fi @@ -457,7 +487,7 @@ jobs: done for image in "${REGISTRY_IMAGE}" "${GHCR_IMAGE}"; do - bash ./scripts/verify-cosign-signature.sh \ + ci-recipes docker-sqlite-wordpress verify-cosign-signature \ "${image}@${MANIFEST_DIGEST}" \ "${certificate_identity}" \ "${certificate_issuer}" diff --git a/CHANGELOG.md b/CHANGELOG.md index e271302..75f34cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Changed +- Replaced repository-local CI validation Bash scripts with the SHA-pinned + `ci-recipes` Go CLI and moved the architecture-independent upstream image + check out of the three-platform smoke-test matrix. - Marked `2026.09.02-r2` as published after both registries, supply-chain evidence, signatures, and mutable aliases passed verification; switched the Quick Start and Compose example from a local `main` build to the immutable diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45d0a8e..aef3d73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,12 +98,12 @@ Run the complete fast test set before opening a pull request: ```bash bash tests/test-entrypoint-reconcile.sh bash tests/test-documentation.sh -bash tests/test-validate-release.sh php tests/test-sqlite-local-core-update.php php tests/test-sqlite-select-id-key-fix.php php tests/test-tool-update-site-url.php php tests/test-tool-reset-user-password.php -./scripts/validate-release.sh 2026.09.02-r2 +go install github.com/soulteary/ci-recipes/cmd/ci-recipes@6e790adf553ecff9f5ba5a3d0beeb9a9256a29ee +ci-recipes docker-sqlite-wordpress validate-release 2026.09.02-r2 ``` To reproduce the remaining lint and configuration checks: diff --git a/RELEASING.md b/RELEASING.md index 72f50a9..0d65761 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -36,12 +36,12 @@ tag protection remains the authoritative control. ```bash bash tests/test-entrypoint-reconcile.sh - bash tests/test-validate-release.sh php tests/test-sqlite-local-core-update.php php tests/test-sqlite-select-id-key-fix.php php tests/test-tool-update-site-url.php php tests/test-tool-reset-user-password.php - ./scripts/validate-release.sh 2026.09.02-r2 + go install github.com/soulteary/ci-recipes/cmd/ci-recipes@6e790adf553ecff9f5ba5a3d0beeb9a9256a29ee + ci-recipes docker-sqlite-wordpress validate-release 2026.09.02-r2 ``` 4. Let pull-request CI test amd64, native arm64, and the 32-bit ARM pure-PHP @@ -170,7 +170,7 @@ release=2026.09.02-r2 availability gate: ```bash - ./scripts/verify-published-release.sh "${release}" + ci-recipes docker-sqlite-wordpress verify-published-release "${release}" ``` The GitHub Release already exists at this stage because its `published` @@ -181,8 +181,8 @@ release=2026.09.02-r2 4. If the release pull request carried the `release-availability: pending` README marker and local-build Compose configuration, open a documentation follow-up that removes the marker and switches Compose to the exact published - tag. `tests/test-release-availability.sh` rejects that transition until the - Git tag exists and Docker Hub and GHCR expose the same manifest digest. + tag only after the registry availability gate confirms that the Git tag exists + and Docker Hub and GHCR expose the same manifest digest. Never force-push, delete, or retarget a published release tag. If anything in the image or its evidence must change, publish a new CalVer revision. Rerun a diff --git a/scripts/validate-buildx-evidence.sh b/scripts/validate-buildx-evidence.sh deleted file mode 100755 index 1aa3654..0000000 --- a/scripts/validate-buildx-evidence.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail - -field="${1:-}" -expected_platforms="${2:-}" - -if [[ "${field}" != "SPDX" && "${field}" != "SLSA" ]] \ - || ! jq -e 'type == "array" and length > 0 and all(.[]; type == "string" and length > 0)' \ - <<< "${expected_platforms}" >/dev/null 2>&1; then - echo "usage: $0 SPDX|SLSA EXPECTED_PLATFORMS_JSON" >&2 - exit 2 -fi - -if ! jq -e \ - --arg field "${field}" \ - --argjson expected "${expected_platforms}" \ - 'type == "object" - and (keys | sort) == ($expected | sort) - and all(.[]; .[$field] | type == "object" and length > 0)' \ - >/dev/null; then - echo "evidence must contain one non-empty ${field} object for every expected platform" >&2 - exit 1 -fi diff --git a/scripts/validate-release.sh b/scripts/validate-release.sh deleted file mode 100755 index 79d8fb8..0000000 --- a/scripts/validate-release.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -release_version="${1:-}" -if [[ ! "${release_version}" =~ ^[0-9]{4}\.(0[1-9]|1[0-2])\.(0[1-9]|[12][0-9]|3[01])-r[1-9][0-9]*$ ]]; then - echo "release version must use YYYY.MM.DD-rN CalVer (for example, 2026.09.01-r1)" >&2 - exit 1 -fi - -release_date="${release_version%-r*}" -# The PHP program is intentionally single-quoted so Bash cannot expand it. -# shellcheck disable=SC2016 -if ! php -r ' -$date = DateTimeImmutable::createFromFormat( "!Y.m.d", $argv[1], new DateTimeZone( "UTC" ) ); -$errors = DateTimeImmutable::getLastErrors(); -exit( $date && false === $errors && $date->format( "Y.m.d" ) === $argv[1] ? 0 : 1 ); -' "${release_date}"; then - echo "release version contains an invalid calendar date: ${release_date}" >&2 - exit 1 -fi - -verify_upstream=false -case "${2:-}" in - "") ;; - --verify-upstream) verify_upstream=true ;; - *) - echo "usage: $0 RELEASE_VERSION [--verify-upstream]" >&2 - exit 1 - ;; -esac -if [[ "$#" -gt 2 ]]; then - echo "usage: $0 RELEASE_VERSION [--verify-upstream]" >&2 - exit 1 -fi - -wordpress_image="$(sed -nE 's/^ARG WORDPRESS_IMAGE=([^[:space:]]+)$/\1/p' Dockerfile)" -if [[ -z "${wordpress_image}" ]]; then - echo "WORDPRESS_IMAGE must be defined" >&2 - exit 1 -fi - -mapfile -t base_images < <(sed -nE 's/^FROM \$\{WORDPRESS_IMAGE\}.*$/WORDPRESS_IMAGE/p' Dockerfile) -if [[ "${#base_images[@]}" -ne 2 ]]; then - echo "expected exactly two stages based on WORDPRESS_IMAGE" >&2 - exit 1 -fi - -if [[ ! "${wordpress_image}" =~ ^wordpress:[^@[:space:]]+@sha256:[0-9a-f]{64}$ ]]; then - echo "WORDPRESS_IMAGE must pin an official image tag and sha256 digest" >&2 - exit 1 -fi - -base_image="${wordpress_image%@sha256:*}" -wordpress_version="$(sed -nE 's/^ARG WORDPRESS_VERSION=([^[:space:]]+)$/\1/p' Dockerfile)" -if [[ ! "${wordpress_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "WORDPRESS_VERSION must be an exact stable version" >&2 - exit 1 -fi -if [[ ! "${base_image}" =~ ^wordpress:${wordpress_version}-php[0-9]+\.[0-9]+-apache$ ]]; then - echo "base image ${base_image} does not match WordPress ${wordpress_version}" >&2 - exit 1 -fi - -image_version="$(sed -nE 's/^ARG IMAGE_VERSION=([^[:space:]]+)$/\1/p' Dockerfile)" -if [[ "${image_version}" != "${release_version}" ]]; then - echo "Dockerfile IMAGE_VERSION ${image_version:-} does not match release ${release_version}" >&2 - exit 1 -fi - -pinned_digest="${wordpress_image##*@}" -if ${verify_upstream}; then - for command_name in docker jq; do - if ! command -v "${command_name}" >/dev/null 2>&1; then - echo "${command_name} is required for upstream image verification" >&2 - exit 1 - fi - done - - manifest_json="" - for attempt in 1 2 3; do - if manifest_json="$( - docker buildx imagetools inspect "${base_image}" \ - --format '{{json .Manifest}}' - )"; then - break - fi - if [[ "${attempt}" -eq 3 ]]; then - echo "unable to resolve official image tag ${base_image}" >&2 - exit 1 - fi - sleep "$((attempt * 2))" - done - - if ! resolved_digest="$( - jq -er '.digest | select(type == "string" and test("^sha256:[0-9a-f]{64}$"))' \ - <<< "${manifest_json}" - )"; then - echo "unable to read the registry digest for ${base_image}" >&2 - exit 1 - fi - if [[ "${resolved_digest}" != "${pinned_digest}" ]]; then - echo "${base_image} resolves to ${resolved_digest}, not pinned digest ${pinned_digest}" >&2 - exit 1 - fi -fi - -php_version="${base_image#wordpress:"${wordpress_version}"-php}" -php_version="${php_version%-apache}" -plugin_version="$(sed -nE 's/^ARG SQLITE_DATABASE_INTEGRATION_VERSION=([^[:space:]]+)$/\1/p' Dockerfile)" -if [[ ! "${plugin_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "SQLite Database Integration must use a stable semantic version" >&2 - exit 1 -fi - -plugin_commit="$(sed -nE 's/^ARG SQLITE_DATABASE_INTEGRATION_COMMIT=([^[:space:]]+)$/\1/p' Dockerfile)" -if [[ ! "${plugin_commit}" =~ ^[0-9a-f]{40}$ ]]; then - echo "SQLite Database Integration must pin a full commit SHA" >&2 - exit 1 -fi - -rust_toolchain="$(sed -nE 's/^ARG RUST_TOOLCHAIN_VERSION=([^[:space:]]+)$/\1/p' Dockerfile)" -if [[ ! "${rust_toolchain}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Rust toolchain must use an exact stable version" >&2 - exit 1 -fi - -grep -Fq "WordPress \`${wordpress_version}\` on PHP ${php_version}/Apache" README.md || { - echo "README runtime version does not match ${base_image}" >&2 - exit 1 -} -grep -Fq "\`v${plugin_version}\`" README.md || { - echo "README SQLite Database Integration version does not match ${plugin_version}" >&2 - exit 1 -} -grep -Fq "soulteary/sqlite-wordpress:${release_version}" README.md || { - echo "README does not contain the ${release_version} image tag" >&2 - exit 1 -} -if grep -Fq '' README.md; then - grep -Fq 'image: sqlite-wordpress:main' docker-compose.yml || { - echo "pending release Compose configuration must use sqlite-wordpress:main" >&2 - exit 1 - } - grep -Eq '^[[:space:]]+build:$' docker-compose.yml || { - echo "pending release Compose configuration must build the current repository" >&2 - exit 1 - } -else - grep -Fq "image: soulteary/sqlite-wordpress:${release_version}" docker-compose.yml || { - echo "docker-compose.yml does not use the ${release_version} image tag" >&2 - exit 1 - } -fi -# These Dockerfile label values intentionally contain literal build variables. -# shellcheck disable=SC2016 -grep -Fq 'org.opencontainers.image.version="${IMAGE_VERSION}"' Dockerfile || { - echo "OCI image version must use IMAGE_VERSION" >&2 - exit 1 -} -# shellcheck disable=SC2016 -grep -Fq 'org.opencontainers.image.revision="${IMAGE_REVISION}"' Dockerfile || { - echo "OCI image revision must use IMAGE_REVISION" >&2 - exit 1 -} -grep -Fq "org.opencontainers.image.base.name=\"docker.io/library/${base_image}\"" Dockerfile || { - echo "OCI base image name does not match ${base_image}" >&2 - exit 1 -} -grep -Fq "org.opencontainers.image.base.digest=\"${pinned_digest}\"" Dockerfile || { - echo "OCI base image digest does not match ${pinned_digest}" >&2 - exit 1 -} -grep -Fq 'org.opencontainers.image.licenses="Apache-2.0 AND GPL-2.0-or-later"' Dockerfile || { - echo "OCI image licenses do not match the documented project and bundled application licenses" >&2 - exit 1 -} -grep -Fq 'Apache-2.0 AND GPL-2.0-or-later' LICENSES.md || { - echo "LICENSES.md does not document the OCI image license expression" >&2 - exit 1 -} -grep -Fq 'YYYY.MM.DD-rN' VERSIONING.md || { - echo "VERSIONING.md does not document the CalVer release format" >&2 - exit 1 -} -grep -Fq "## [${release_version}]" CHANGELOG.md || { - echo "CHANGELOG.md does not contain a ${release_version} release section" >&2 - exit 1 -} - -grep -Fq 'COPY sqlite-local-core-update.php' Dockerfile || { - echo "Dockerfile does not package the local WordPress core update integration" >&2 - exit 1 -} - -printf 'release_version=%s\nwordpress_version=%s\nwordpress_image=%s\nplugin_version=%s\nplugin_commit=%s\nrust_toolchain=%s\n' \ - "${release_version}" "${wordpress_version}" "${wordpress_image}" "${plugin_version}" "${plugin_commit}" "${rust_toolchain}" -if ${verify_upstream}; then - printf 'wordpress_upstream_digest=%s\n' "${resolved_digest}" -fi diff --git a/scripts/verify-cosign-signature.sh b/scripts/verify-cosign-signature.sh deleted file mode 100755 index 9045599..0000000 --- a/scripts/verify-cosign-signature.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail - -image_ref="${1:-}" -certificate_identity="${2:-}" -certificate_issuer="${3:-}" -attempts="${COSIGN_VERIFY_ATTEMPTS:-12}" -delay_seconds="${COSIGN_VERIFY_DELAY_SECONDS:-10}" - -if [[ -z "${image_ref}" || -z "${certificate_identity}" || -z "${certificate_issuer}" ]] \ - || [[ ! "${attempts}" =~ ^[1-9][0-9]*$ ]] \ - || [[ ! "${delay_seconds}" =~ ^[0-9]+$ ]]; then - echo "usage: $0 IMAGE_REF CERTIFICATE_IDENTITY CERTIFICATE_ISSUER" >&2 - exit 2 -fi - -verify_log="$(mktemp)" -trap 'rm -f "${verify_log}"' EXIT - -for ((attempt = 1; attempt <= attempts; attempt++)); do - : > "${verify_log}" - verify_status=0 - if cosign verify \ - --certificate-identity "${certificate_identity}" \ - --certificate-oidc-issuer "${certificate_issuer}" \ - "${image_ref}" > /dev/null 2>"${verify_log}"; then - exit 0 - else - verify_status=$? - fi - - if ! grep -Fqi 'no signatures found' "${verify_log}"; then - cat "${verify_log}" >&2 - exit "${verify_status}" - fi - if [[ "${attempt}" -eq "${attempts}" ]]; then - cat "${verify_log}" >&2 - echo "signature referrer for ${image_ref} was not visible after ${attempts} attempts" >&2 - exit "${verify_status}" - fi - - echo "signature referrer for ${image_ref} is not visible yet; retrying in ${delay_seconds}s (${attempt}/${attempts})" >&2 - sleep "${delay_seconds}" -done diff --git a/scripts/verify-published-release.sh b/scripts/verify-published-release.sh deleted file mode 100755 index 0267ef5..0000000 --- a/scripts/verify-published-release.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail - -release_version="${1:-}" -if [[ ! "${release_version}" =~ ^[0-9]{4}\.[0-9]{2}\.[0-9]{2}-r[1-9][0-9]*$ ]]; then - echo "usage: $0 YYYY.MM.DD-rN" >&2 - exit 1 -fi - -for command_name in curl jq; do - if ! command -v "${command_name}" >/dev/null 2>&1; then - echo "${command_name} is required to verify a published release" >&2 - exit 1 - fi -done - -curl_args=( - --proto '=https' - --tlsv1.2 - --retry 3 - --retry-all-errors - --connect-timeout 10 - --max-time 60 - --fail - --silent - --show-error -) - -dockerhub_json="$( - curl "${curl_args[@]}" \ - "https://hub.docker.com/v2/repositories/soulteary/sqlite-wordpress/tags/${release_version}" -)" -dockerhub_digest="$( - jq -er '.digest | select(type == "string" and test("^sha256:[0-9a-f]{64}$"))' \ - <<< "${dockerhub_json}" -)" - -ghcr_token="$( - curl "${curl_args[@]}" \ - "https://ghcr.io/token?service=ghcr.io&scope=repository:soulteary/sqlite-wordpress:pull" \ - | jq -er '.token | select(type == "string" and length > 0)' -)" -ghcr_headers="$( - curl "${curl_args[@]}" \ - --dump-header - \ - --output /dev/null \ - --header "Authorization: Bearer ${ghcr_token}" \ - --header 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json' \ - "https://ghcr.io/v2/soulteary/sqlite-wordpress/manifests/${release_version}" -)" -ghcr_digest="$( - awk 'tolower($1) == "docker-content-digest:" { gsub("\r", "", $2); print $2 }' \ - <<< "${ghcr_headers}" \ - | tail -n 1 -)" - -if [[ ! "${ghcr_digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "GHCR did not return a valid manifest digest for ${release_version}" >&2 - exit 1 -fi -if [[ "${dockerhub_digest}" != "${ghcr_digest}" ]]; then - echo "registry digest mismatch for ${release_version}" >&2 - echo "Docker Hub: ${dockerhub_digest}" >&2 - echo "GHCR: ${ghcr_digest}" >&2 - exit 1 -fi - -printf 'published_release=%s\nmanifest_digest=%s\n' \ - "${release_version}" "${dockerhub_digest}" diff --git a/tests/test-cosign-signature-retry.sh b/tests/test-cosign-signature-retry.sh deleted file mode 100755 index 6070c6b..0000000 --- a/tests/test-cosign-signature-retry.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "${repo_root}" - -fake_bin="$(mktemp -d)" -trap 'rm -rf "${fake_bin}"' EXIT - -cat > "${fake_bin}/cosign" <<'EOF' -#!/usr/bin/env bash -set -Eeuo pipefail - -count="$(<"${FAKE_COSIGN_COUNT_FILE}")" -count=$((count + 1)) -printf '%s\n' "${count}" > "${FAKE_COSIGN_COUNT_FILE}" -printf '%s\n' "$*" >> "${FAKE_COSIGN_CALLS_FILE}" - -case "${FAKE_COSIGN_MODE}" in - eventual) - if [[ "${count}" -lt "${FAKE_COSIGN_SUCCESS_AT}" ]]; then - echo 'Error: no signatures found' >&2 - exit 10 - fi - ;; - fatal) - echo 'Error: certificate identity mismatch' >&2 - exit 1 - ;; - missing) - echo 'Error: no signatures found' >&2 - exit 10 - ;; - *) - echo "unexpected fake mode: ${FAKE_COSIGN_MODE}" >&2 - exit 2 - ;; -esac -EOF -chmod +x "${fake_bin}/cosign" - -counter="${fake_bin}/count" -calls="${fake_bin}/calls" -identity='https://github.com/soulteary/docker-sqlite-wordpress/.github/workflows/release.yaml@refs/tags/2026.08.31-r3' -issuer='https://token.actions.githubusercontent.com' -image_ref='soulteary/sqlite-wordpress@sha256:9716786a5213d89f0d77bbad5bd04723aad8791018d5a8811c5974df73eb40c1' - -printf '0\n' > "${counter}" -: > "${calls}" -PATH="${fake_bin}:${PATH}" \ - FAKE_COSIGN_COUNT_FILE="${counter}" \ - FAKE_COSIGN_CALLS_FILE="${calls}" \ - FAKE_COSIGN_MODE=eventual \ - FAKE_COSIGN_SUCCESS_AT=3 \ - COSIGN_VERIFY_ATTEMPTS=3 \ - COSIGN_VERIFY_DELAY_SECONDS=0 \ - bash ./scripts/verify-cosign-signature.sh "${image_ref}" "${identity}" "${issuer}" -[[ "$(<"${counter}")" == 3 ]] -grep -Fq -- "verify --certificate-identity ${identity} --certificate-oidc-issuer ${issuer} ${image_ref}" "${calls}" - -printf '0\n' > "${counter}" -: > "${calls}" -if PATH="${fake_bin}:${PATH}" \ - FAKE_COSIGN_COUNT_FILE="${counter}" \ - FAKE_COSIGN_CALLS_FILE="${calls}" \ - FAKE_COSIGN_MODE=fatal \ - COSIGN_VERIFY_ATTEMPTS=5 \ - COSIGN_VERIFY_DELAY_SECONDS=0 \ - bash ./scripts/verify-cosign-signature.sh "${image_ref}" "${identity}" "${issuer}" \ - >/dev/null 2>&1; then - echo "non-transient verification failure unexpectedly retried to success" >&2 - exit 1 -fi -[[ "$(<"${counter}")" == 1 ]] - -printf '0\n' > "${counter}" -: > "${calls}" -if PATH="${fake_bin}:${PATH}" \ - FAKE_COSIGN_COUNT_FILE="${counter}" \ - FAKE_COSIGN_CALLS_FILE="${calls}" \ - FAKE_COSIGN_MODE=missing \ - COSIGN_VERIFY_ATTEMPTS=2 \ - COSIGN_VERIFY_DELAY_SECONDS=0 \ - bash ./scripts/verify-cosign-signature.sh "${image_ref}" "${identity}" "${issuer}" \ - >/dev/null 2>&1; then - echo "permanently missing signature unexpectedly passed" >&2 - exit 1 -fi -[[ "$(<"${counter}")" == 2 ]] - -grep -Fq 'cosign-release: v3.1.3' .github/workflows/release.yaml -grep -Fq 'bash ./scripts/verify-cosign-signature.sh' .github/workflows/release.yaml - -echo "cosign signature visibility retry tests passed" diff --git a/tests/test-documentation.sh b/tests/test-documentation.sh old mode 100755 new mode 100644 index 4fc80e6..16661a4 --- a/tests/test-documentation.sh +++ b/tests/test-documentation.sh @@ -165,7 +165,7 @@ if [[ ! "${release_version}" =~ ^[0-9]{4}\.(0[1-9]|1[0-2])\.(0[1-9]|[12][0-9]|3[ fi for release_doc in CONTRIBUTING.md RELEASING.md; do - if ! grep -Fq "./scripts/validate-release.sh ${release_version}" "${release_doc}"; then + if ! grep -Fq "ci-recipes docker-sqlite-wordpress validate-release ${release_version}" "${release_doc}"; then echo "${release_doc} does not use image release ${release_version}" >&2 failures=$((failures + 1)) fi diff --git a/tests/test-release-availability.sh b/tests/test-release-availability.sh deleted file mode 100755 index 9366e87..0000000 --- a/tests/test-release-availability.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "${repo_root}" - -release_version="$(sed -nE 's/^ARG IMAGE_VERSION=([^[:space:]]+)$/\1/p' Dockerfile)" -if [[ -z "${release_version}" ]]; then - echo "could not determine IMAGE_VERSION from Dockerfile" >&2 - exit 1 -fi - -pending_marker='' -if grep -Fq "${pending_marker}" README.md; then - grep -Fq "\`${release_version}\` is prepared but is not published yet" README.md - grep -Fq 'docker build -t sqlite-wordpress:main .' README.md - grep -Fq 'image: sqlite-wordpress:main' docker-compose.yml - grep -Eq '^[[:space:]]+build:$' docker-compose.yml - echo "release ${release_version} is explicitly documented as pending" - exit 0 -fi - -if ! git show-ref --verify --quiet "refs/tags/${release_version}"; then - echo "README advertises ${release_version}, but the Git tag does not exist" >&2 - exit 1 -fi - -grep -Fq "image: soulteary/sqlite-wordpress:${release_version}" docker-compose.yml -./scripts/verify-published-release.sh "${release_version}" - -echo "published release availability checks passed" diff --git a/tests/test-release-evidence.sh b/tests/test-release-evidence.sh deleted file mode 100755 index b4e670c..0000000 --- a/tests/test-release-evidence.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "${repo_root}" - -expected='["linux/amd64","linux/arm/v5","linux/arm/v6","linux/arm/v7","linux/arm64"]' - -valid_sbom='{ - "linux/amd64":{"SPDX":{"spdxVersion":"SPDX-2.3"}}, - "linux/arm/v5":{"SPDX":{"spdxVersion":"SPDX-2.3"}}, - "linux/arm/v6":{"SPDX":{"spdxVersion":"SPDX-2.3"}}, - "linux/arm/v7":{"SPDX":{"spdxVersion":"SPDX-2.3"}}, - "linux/arm64":{"SPDX":{"spdxVersion":"SPDX-2.3"}} -}' -valid_provenance='{ - "linux/amd64":{"SLSA":{"buildType":"https://mobyproject.org/buildkit@v1"}}, - "linux/arm/v5":{"SLSA":{"buildType":"https://mobyproject.org/buildkit@v1"}}, - "linux/arm/v6":{"SLSA":{"buildType":"https://mobyproject.org/buildkit@v1"}}, - "linux/arm/v7":{"SLSA":{"buildType":"https://mobyproject.org/buildkit@v1"}}, - "linux/arm64":{"SLSA":{"buildType":"https://mobyproject.org/buildkit@v1"}} -}' - -bash ./scripts/validate-buildx-evidence.sh SPDX "${expected}" <<< "${valid_sbom}" -bash ./scripts/validate-buildx-evidence.sh SLSA "${expected}" <<< "${valid_provenance}" - -missing_platform="$(jq 'del(.["linux/arm/v5"])' <<< "${valid_sbom}")" -if bash ./scripts/validate-buildx-evidence.sh SPDX "${expected}" \ - <<< "${missing_platform}" >/dev/null 2>&1; then - echo "evidence with a missing platform unexpectedly passed" >&2 - exit 1 -fi - -empty_evidence="$(jq '.["linux/arm64"].SLSA = {}' <<< "${valid_provenance}")" -if bash ./scripts/validate-buildx-evidence.sh SLSA "${expected}" \ - <<< "${empty_evidence}" >/dev/null 2>&1; then - echo "empty per-platform evidence unexpectedly passed" >&2 - exit 1 -fi - -grep -Fq -- "--format '{{ json .Provenance }}'" .github/workflows/release.yaml -if grep -Fq '.Provenance.SLSA' .github/workflows/release.yaml; then - echo "multi-platform provenance must be read before selecting each SLSA value" >&2 - exit 1 -fi - -echo "release evidence validation tests passed" diff --git a/tests/test-validate-release.sh b/tests/test-validate-release.sh deleted file mode 100644 index 37a7447..0000000 --- a/tests/test-validate-release.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "${repo_root}" - -fake_bin="$(mktemp -d)" -trap 'rm -rf "${fake_bin}"' EXIT - -cat > "${fake_bin}/docker" <<'SCRIPT' -#!/usr/bin/env bash -set -euo pipefail - -if [[ "$#" -ne 6 ]] \ - || [[ "$1" != "buildx" ]] \ - || [[ "$2" != "imagetools" ]] \ - || [[ "$3" != "inspect" ]] \ - || [[ "$4" != "${FAKE_EXPECTED_REF}" ]] \ - || [[ "$5" != "--format" ]]; then - echo "unexpected docker invocation: $*" >&2 - exit 1 -fi -printf '{"digest":"%s"}\n' "${FAKE_MANIFEST_DIGEST}" -SCRIPT -chmod +x "${fake_bin}/docker" - -wordpress_image="$(sed -nE 's/^ARG WORDPRESS_IMAGE=([^[:space:]]+)$/\1/p' Dockerfile)" -base_image="${wordpress_image%@sha256:*}" -pinned_digest="${wordpress_image##*@}" - -PATH="${fake_bin}:${PATH}" \ - FAKE_EXPECTED_REF="${base_image}" \ - FAKE_MANIFEST_DIGEST="${pinned_digest}" \ - ./scripts/validate-release.sh 2026.09.02-r2 --verify-upstream >/dev/null - -mismatch_digest="sha256:0000000000000000000000000000000000000000000000000000000000000000" -if PATH="${fake_bin}:${PATH}" \ - FAKE_EXPECTED_REF="${base_image}" \ - FAKE_MANIFEST_DIGEST="${mismatch_digest}" \ - ./scripts/validate-release.sh 2026.09.02-r2 --verify-upstream \ - >/dev/null 2>"${fake_bin}/mismatch.log"; then - echo "digest mismatch unexpectedly passed" >&2 - exit 1 -fi -grep -Fq "not pinned digest ${pinned_digest}" "${fake_bin}/mismatch.log" - -for invalid_version in 7.1.0 2026.9.01-r1 2026.02.30-r1 2026.09.01-r0; do - if ./scripts/validate-release.sh "${invalid_version}" >/dev/null 2>"${fake_bin}/invalid.log"; then - echo "invalid release version unexpectedly passed: ${invalid_version}" >&2 - exit 1 - fi -done - -echo "release validation digest tests passed"