From 5b58489fa5e6acd0d79a74388493031fe02734be Mon Sep 17 00:00:00 2001
From: a
Date: Sat, 18 Jul 2026 14:55:50 +0000
Subject: [PATCH 1/2] feat(release): publish verified SemVer images
---
.github/workflows/release.yml | 483 ++++++++++++++++++
Cargo.toml | 2 +-
.../extract-and-standardize-pggomtm/tasks.md | 2 +-
3 files changed, 485 insertions(+), 2 deletions(-)
create mode 100644 .github/workflows/release.yml
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..3c31cc4
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,483 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "v[0-9]*.[0-9]*.[0-9]*"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: mtmpg-release
+ cancel-in-progress: false
+
+jobs:
+ metadata:
+ name: Validate SemVer tag
+ runs-on: ubuntu-24.04
+ timeout-minutes: 5
+ outputs:
+ prerelease: ${{ steps.version.outputs.prerelease }}
+ version: ${{ steps.version.outputs.version }}
+
+ steps:
+ - name: Validate release identity
+ id: version
+ shell: bash
+ run: |
+ set -euo pipefail
+ test "$GITHUB_REPOSITORY" = "codeh007/mtmpg"
+ test "$GITHUB_REF_TYPE" = "tag"
+ tag="$GITHUB_REF_NAME"
+ [[ "$tag" == v* ]]
+ version="$(cut -c2- <<<"$tag")"
+ identifier='(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)'
+ semver="^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-$identifier([.]$identifier)*)?$"
+ [[ "$version" =~ $semver ]]
+ test "$tag" = "v$version"
+ test "$(printf '%s' "$version" | wc --bytes)" -le 120
+
+ prerelease=false
+ if [[ "$version" == *-* ]]; then
+ prerelease=true
+ fi
+ {
+ echo "prerelease=$prerelease"
+ echo "version=$version"
+ } >>"$GITHUB_OUTPUT"
+
+ ci:
+ name: Verify release source
+ needs: metadata
+ permissions:
+ contents: read
+ uses: ./.github/workflows/ci.yml
+ with:
+ upload_release_artifacts: true
+ release_version: ${{ needs.metadata.outputs.version }}
+
+ publish:
+ name: Publish verified release
+ if: github.repository == 'codeh007/mtmpg'
+ needs:
+ - metadata
+ - ci
+ runs-on: ubuntu-24.04
+ timeout-minutes: 60
+ permissions:
+ contents: write
+ packages: write
+ id-token: write
+ attestations: write
+ artifact-metadata: write
+ env:
+ GH_TOKEN: ${{ github.token }}
+ IMAGE_REPOSITORY: ghcr.io/codeh007/mtmpg
+ PRERELEASE: ${{ needs.metadata.outputs.prerelease }}
+ TAG: ${{ github.ref_name }}
+ VERSION: ${{ needs.metadata.outputs.version }}
+
+ steps:
+ - name: Download verified release materials
+ uses: actions/download-artifact@v8
+ with:
+ name: ${{ needs.ci.outputs.release_artifact }}
+ path: ${{ runner.temp }}/release-materials
+
+ - name: Install release inspection tools
+ shell: bash
+ run: |
+ set -euo pipefail
+ sudo apt-get update
+ sudo apt-get install --yes --no-install-recommends ca-certificates curl jq skopeo
+
+ - name: Log in to GHCR
+ uses: docker/login-action@v4
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ github.token }}
+
+ - name: Validate immutable tag and verified materials
+ env:
+ CI_SOURCE: ${{ needs.ci.outputs.source }}
+ CI_VERSION: ${{ needs.ci.outputs.version }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ material_root="$RUNNER_TEMP/release-materials"
+ asset_root="$RUNNER_TEMP/release-assets"
+ archive="$material_root/mtmpg.oci.tar"
+ for required in Cargo.lock resolved-inputs.json verified-image.json mtmpg.oci.tar; do
+ test -f "$material_root/$required"
+ test ! -L "$material_root/$required"
+ done
+
+ test "$TAG" = "$GITHUB_REF_NAME"
+ test "$CI_SOURCE" = "$GITHUB_SHA"
+ test "$CI_VERSION" = "$VERSION"
+ test "$(jq --raw-output '.schema' "$material_root/resolved-inputs.json")" = "mtmpg-resolved-inputs/v1"
+ test "$(jq --raw-output '.schema' "$material_root/verified-image.json")" = "mtmpg-verified-image/v1"
+ test "$(jq --raw-output '.source' "$material_root/resolved-inputs.json")" = "$GITHUB_SHA"
+ test "$(jq --raw-output '.source' "$material_root/verified-image.json")" = "$GITHUB_SHA"
+ test "$(jq --raw-output '.version' "$material_root/resolved-inputs.json")" = "$VERSION"
+ test "$(jq --raw-output '.version' "$material_root/verified-image.json")" = "$VERSION"
+ test "$(jq --raw-output '.workflow.run_id' "$material_root/resolved-inputs.json")" = "$GITHUB_RUN_ID"
+ test "$(jq --raw-output '.workflow.run_attempt' "$material_root/resolved-inputs.json")" = "$GITHUB_RUN_ATTEMPT"
+
+ lock_sha256="$(sha256sum "$material_root/Cargo.lock" | cut -d' ' -f1)"
+ resolved_sha256="$(sha256sum "$material_root/resolved-inputs.json" | cut -d' ' -f1)"
+ archive_sha256="$(sha256sum "$archive" | cut -d' ' -f1)"
+ archive_digest="$(skopeo inspect --format '{{.Digest}}' "oci-archive:$archive")"
+ test "$lock_sha256" = "$(jq --raw-output '.cargo_lock_sha256' "$material_root/resolved-inputs.json")"
+ test "$lock_sha256" = "$(jq --raw-output '.cargo_lock_sha256' "$material_root/verified-image.json")"
+ test "$resolved_sha256" = "$(jq --raw-output '.resolved_inputs_sha256' "$material_root/verified-image.json")"
+ test "$archive_sha256" = "$(jq --raw-output '.oci_archive_sha256' "$material_root/verified-image.json")"
+ test "$archive_digest" = "$(jq --raw-output '.oci_manifest_digest' "$material_root/verified-image.json")"
+
+ tag_source="$(gh api "repos/$GITHUB_REPOSITORY/commits/$TAG" --jq .sha)"
+ test "$tag_source" = "$GITHUB_SHA"
+ if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
+ echo "GitHub Release already exists for $TAG" >&2
+ exit 1
+ fi
+
+ auth_file="$HOME/.docker/config.json"
+ test -f "$auth_file"
+ target_reference="$IMAGE_REPOSITORY:$VERSION"
+ if skopeo inspect --authfile "$auth_file" "docker://$target_reference" >/dev/null 2>&1; then
+ echo "OCI version already exists: $target_reference" >&2
+ exit 1
+ fi
+
+ latest_before=absent
+ if skopeo inspect --authfile "$auth_file" "docker://$IMAGE_REPOSITORY:latest" >/dev/null 2>&1; then
+ latest_before="$(skopeo inspect --authfile "$auth_file" --format '{{.Digest}}' "docker://$IMAGE_REPOSITORY:latest")"
+ fi
+
+ install -d -m 0755 "$asset_root"
+ {
+ echo "ARCHIVE=$archive"
+ echo "ASSET_ROOT=$asset_root"
+ echo "LATEST_BEFORE=$latest_before"
+ echo "MATERIAL_ROOT=$material_root"
+ echo "TARGET_REFERENCE=$target_reference"
+ } >>"$GITHUB_ENV"
+
+ - name: Push the verified OCI archive once
+ shell: bash
+ run: |
+ set -euo pipefail
+ digest_file="$RUNNER_TEMP/registry-digest.txt"
+ skopeo copy \
+ --authfile "$HOME/.docker/config.json" \
+ --preserve-digests \
+ --digestfile "$digest_file" \
+ "oci-archive:$ARCHIVE" \
+ "docker://$TARGET_REFERENCE"
+ registry_digest="$(tr -d '\n' <"$digest_file")"
+ [[ "$registry_digest" =~ ^sha256:[0-9a-f]{64}$ ]]
+ test "$registry_digest" = "$(jq --raw-output '.oci_manifest_digest' "$MATERIAL_ROOT/verified-image.json")"
+ test "$registry_digest" = "$(skopeo inspect --authfile "$HOME/.docker/config.json" --format '{{.Digest}}' "docker://$TARGET_REFERENCE")"
+ echo "REGISTRY_DIGEST=$registry_digest" >>"$GITHUB_ENV"
+
+ - name: Download current Syft
+ id: syft
+ uses: anchore/sbom-action/download-syft@v0
+
+ - name: Generate release manifest and SPDX SBOM
+ env:
+ SYFT_BIN: ${{ steps.syft.outputs.cmd }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ install -m 0644 \
+ "$MATERIAL_ROOT/Cargo.lock" \
+ "$MATERIAL_ROOT/resolved-inputs.json" \
+ "$MATERIAL_ROOT/verified-image.json" \
+ "$ASSET_ROOT"
+
+ "$SYFT_BIN" \
+ "oci-archive:$ARCHIVE" \
+ --output "spdx-json=$ASSET_ROOT/sbom.spdx.json"
+ jq --exit-status \
+ '(.spdxVersion | startswith("SPDX-")) and ((.packages // []) | length > 0)' \
+ "$ASSET_ROOT/sbom.spdx.json" >/dev/null
+
+ jq --null-input \
+ --slurpfile resolved "$MATERIAL_ROOT/resolved-inputs.json" \
+ --slurpfile verified "$MATERIAL_ROOT/verified-image.json" \
+ --arg image_repository "$IMAGE_REPOSITORY" \
+ --arg registry_digest "$REGISTRY_DIGEST" \
+ --arg sbom_sha256 "$(sha256sum "$ASSET_ROOT/sbom.spdx.json" | cut -d' ' -f1)" \
+ --arg skopeo "$(skopeo --version)" \
+ --arg syft "$("$SYFT_BIN" version -o json | jq --raw-output '.version')" \
+ --arg tag "$TAG" \
+ --argjson prerelease "$PRERELEASE" \
+ '{
+ schema: "mtmpg-release-manifest/v1",
+ version: $verified[0].version,
+ tag: $tag,
+ prerelease: $prerelease,
+ source: $verified[0].source,
+ workflow: $resolved[0].workflow,
+ cargo_lock_sha256: $verified[0].cargo_lock_sha256,
+ resolved_inputs_sha256: $verified[0].resolved_inputs_sha256,
+ rust: $resolved[0].rust,
+ pgrx: $resolved[0].pgrx,
+ postgresql: $resolved[0].postgresql,
+ module: {
+ path: "/usr/lib/postgresql/18/lib/pggomtm.so",
+ sha256: $verified[0].module_sha256
+ },
+ oci: {
+ archive_sha256: $verified[0].oci_archive_sha256,
+ manifest_digest: $verified[0].oci_manifest_digest,
+ repository: $image_repository,
+ reference: ($image_repository + ":" + $verified[0].version),
+ registry_digest: $registry_digest
+ },
+ sbom: {
+ format: "spdx-json",
+ sha256: $sbom_sha256
+ },
+ tooling: {
+ skopeo: $skopeo,
+ syft: $syft
+ }
+ }' >"$ASSET_ROOT/release-manifest.json"
+
+ - name: Attest release provenance
+ id: provenance
+ uses: actions/attest@v4
+ with:
+ subject-name: ${{ env.IMAGE_REPOSITORY }}
+ subject-digest: ${{ env.REGISTRY_DIGEST }}
+ subject-version: ${{ env.VERSION }}
+ push-to-registry: true
+
+ - name: Attest release SBOM
+ id: sbom-attestation
+ uses: actions/attest@v4
+ with:
+ subject-name: ${{ env.IMAGE_REPOSITORY }}
+ subject-digest: ${{ env.REGISTRY_DIGEST }}
+ subject-version: ${{ env.VERSION }}
+ sbom-path: ${{ env.ASSET_ROOT }}/sbom.spdx.json
+ push-to-registry: true
+
+ - name: Finalize standard release materials
+ shell: bash
+ run: |
+ set -euo pipefail
+ provenance_bundle="${{ steps.provenance.outputs.bundle-path }}"
+ provenance_id="${{ steps.provenance.outputs.attestation-id }}"
+ provenance_url="${{ steps.provenance.outputs.attestation-url }}"
+ sbom_bundle="${{ steps.sbom-attestation.outputs.bundle-path }}"
+ sbom_id="${{ steps.sbom-attestation.outputs.attestation-id }}"
+ sbom_url="${{ steps.sbom-attestation.outputs.attestation-url }}"
+ test -s "$provenance_bundle"
+ test -s "$sbom_bundle"
+ test -n "$provenance_id"
+ test -n "$provenance_url"
+ test -n "$sbom_id"
+ test -n "$sbom_url"
+
+ install -m 0644 "$provenance_bundle" "$ASSET_ROOT/provenance.intoto.jsonl"
+ install -m 0644 "$sbom_bundle" "$ASSET_ROOT/sbom-attestation.intoto.jsonl"
+ jq --slurp --exit-status \
+ 'length > 0 and all(.[]; has("dsseEnvelope") and has("verificationMaterial"))' \
+ "$ASSET_ROOT/provenance.intoto.jsonl" >/dev/null
+ jq --slurp --exit-status \
+ 'length > 0 and all(.[]; has("dsseEnvelope") and has("verificationMaterial"))' \
+ "$ASSET_ROOT/sbom-attestation.intoto.jsonl" >/dev/null
+
+ jq --null-input \
+ --arg subject "$IMAGE_REPOSITORY@$REGISTRY_DIGEST" \
+ --arg provenance_id "$provenance_id" \
+ --arg provenance_url "$provenance_url" \
+ --arg sbom_id "$sbom_id" \
+ --arg sbom_url "$sbom_url" \
+ '{
+ schema: "mtmpg-attestation/v1",
+ subject: $subject,
+ provenance: {id: $provenance_id, url: $provenance_url},
+ sbom: {id: $sbom_id, url: $sbom_url}
+ }' >"$ASSET_ROOT/attestation.json"
+
+ manifest_next="$RUNNER_TEMP/release-manifest.next.json"
+ jq \
+ --arg provenance_sha256 "$(sha256sum "$ASSET_ROOT/provenance.intoto.jsonl" | cut -d' ' -f1)" \
+ --arg sbom_attestation_sha256 "$(sha256sum "$ASSET_ROOT/sbom-attestation.intoto.jsonl" | cut -d' ' -f1)" \
+ --arg provenance_id "$provenance_id" \
+ --arg provenance_url "$provenance_url" \
+ --arg sbom_id "$sbom_id" \
+ --arg sbom_url "$sbom_url" \
+ '. + {
+ provenance: {
+ format: "in-toto-jsonl",
+ sha256: $provenance_sha256
+ },
+ attestation: {
+ provenance: {id: $provenance_id, url: $provenance_url},
+ sbom: {id: $sbom_id, url: $sbom_url, sha256: $sbom_attestation_sha256}
+ }
+ }' \
+ "$ASSET_ROOT/release-manifest.json" >"$manifest_next"
+ mv "$manifest_next" "$ASSET_ROOT/release-manifest.json"
+
+ (
+ cd "$ASSET_ROOT"
+ sha256sum \
+ Cargo.lock \
+ resolved-inputs.json \
+ verified-image.json \
+ release-manifest.json \
+ sbom.spdx.json \
+ provenance.intoto.jsonl \
+ sbom-attestation.intoto.jsonl \
+ attestation.json \
+ >checksums.txt
+ sha256sum --check checksums.txt
+ )
+
+ - name: Anonymously verify the versioned image
+ shell: bash
+ run: |
+ set -euo pipefail
+ anonymous_auth="$RUNNER_TEMP/anonymous-auth.json"
+ anonymous_inspect="$RUNNER_TEMP/anonymous-inspect.json"
+ printf '{"auths":{}}\n' >"$anonymous_auth"
+
+ ready=0
+ for _ in $(seq 1 120); do
+ if skopeo inspect \
+ --authfile "$anonymous_auth" \
+ "docker://$TARGET_REFERENCE" \
+ >"$anonymous_inspect" 2>/dev/null; then
+ ready=1
+ break
+ fi
+ sleep 5
+ done
+ test "$ready" -eq 1
+ jq --exit-status \
+ --arg digest "$REGISTRY_DIGEST" \
+ --arg source "$GITHUB_SHA" \
+ --arg version "$VERSION" \
+ '.Digest == $digest and
+ .Labels["org.opencontainers.image.revision"] == $source and
+ .Labels["org.opencontainers.image.version"] == $version' \
+ "$anonymous_inspect" >/dev/null
+
+ skopeo copy \
+ --authfile "$anonymous_auth" \
+ "docker://$TARGET_REFERENCE" \
+ "docker-archive:$RUNNER_TEMP/anonymous-image.tar:mtmpg-verify:$VERSION"
+ docker load --input "$RUNNER_TEMP/anonymous-image.tar" >/dev/null
+ container_id="$(docker create "mtmpg-verify:$VERSION")"
+ docker cp \
+ "$container_id:/usr/lib/postgresql/18/lib/pggomtm.so" \
+ "$RUNNER_TEMP/anonymous-pggomtm.so"
+ docker rm "$container_id" >/dev/null
+ test "$(sha256sum "$RUNNER_TEMP/anonymous-pggomtm.so" | cut -d' ' -f1)" = \
+ "$(jq --raw-output '.module_sha256' "$MATERIAL_ROOT/verified-image.json")"
+
+ attestation_count="$(gh api "repos/$GITHUB_REPOSITORY/attestations/$REGISTRY_DIGEST?per_page=10" --jq '.attestations | length')"
+ test "$attestation_count" -ge 2
+
+ - name: Create immutable GitHub Release
+ shell: bash
+ run: |
+ set -euo pipefail
+ release_notes="PostgreSQL 18 OAuth validator image: $IMAGE_REPOSITORY:$VERSION
+
+ Source: $GITHUB_SHA
+ OCI digest: $REGISTRY_DIGEST"
+ files=(
+ "$ASSET_ROOT/Cargo.lock"
+ "$ASSET_ROOT/resolved-inputs.json"
+ "$ASSET_ROOT/verified-image.json"
+ "$ASSET_ROOT/release-manifest.json"
+ "$ASSET_ROOT/sbom.spdx.json"
+ "$ASSET_ROOT/provenance.intoto.jsonl"
+ "$ASSET_ROOT/sbom-attestation.intoto.jsonl"
+ "$ASSET_ROOT/attestation.json"
+ "$ASSET_ROOT/checksums.txt"
+ )
+ if test "$PRERELEASE" = true; then
+ gh release create "$TAG" "${files[@]}" \
+ --repo "$GITHUB_REPOSITORY" \
+ --verify-tag \
+ --prerelease \
+ --title "mtmpg $VERSION" \
+ --notes "$release_notes"
+ else
+ gh release create "$TAG" "${files[@]}" \
+ --repo "$GITHUB_REPOSITORY" \
+ --verify-tag \
+ --latest \
+ --title "mtmpg $VERSION" \
+ --notes "$release_notes"
+ fi
+
+ - name: Verify public Release assets and latest policy
+ shell: bash
+ run: |
+ set -euo pipefail
+ release_json="$RUNNER_TEMP/public-release.json"
+ curl --fail --location --retry 5 \
+ --header 'Accept: application/vnd.github+json' \
+ "https://api.github.com/repos/$GITHUB_REPOSITORY/releases/tags/$TAG" \
+ >"$release_json"
+ jq --exit-status \
+ --arg tag "$TAG" \
+ --argjson prerelease "$PRERELEASE" \
+ '.tag_name == $tag and .draft == false and .prerelease == $prerelease' \
+ "$release_json" >/dev/null
+
+ verify_root="$RUNNER_TEMP/public-release-assets"
+ install -d -m 0755 "$verify_root"
+ for name in \
+ Cargo.lock \
+ resolved-inputs.json \
+ verified-image.json \
+ release-manifest.json \
+ sbom.spdx.json \
+ provenance.intoto.jsonl \
+ sbom-attestation.intoto.jsonl \
+ attestation.json \
+ checksums.txt; do
+ asset_url="$(jq --raw-output --arg name "$name" '.assets[] | select(.name == $name) | .browser_download_url' "$release_json")"
+ test -n "$asset_url"
+ curl --fail --location --retry 5 "$asset_url" --output "$verify_root/$name"
+ done
+ (
+ cd "$verify_root"
+ sha256sum --check checksums.txt
+ )
+ jq --exit-status \
+ --arg source "$GITHUB_SHA" \
+ --arg version "$VERSION" \
+ --arg digest "$REGISTRY_DIGEST" \
+ '.source == $source and .version == $version and
+ .oci.manifest_digest == $digest and
+ .oci.registry_digest == $digest' \
+ "$verify_root/release-manifest.json" >/dev/null
+
+ auth_file="$HOME/.docker/config.json"
+ if test "$PRERELEASE" = true; then
+ if test "$LATEST_BEFORE" = absent; then
+ ! skopeo inspect --authfile "$auth_file" "docker://$IMAGE_REPOSITORY:latest" >/dev/null 2>&1
+ else
+ test "$(skopeo inspect --authfile "$auth_file" --format '{{.Digest}}' "docker://$IMAGE_REPOSITORY:latest")" = "$LATEST_BEFORE"
+ fi
+ else
+ skopeo copy \
+ --authfile "$auth_file" \
+ --preserve-digests \
+ "oci-archive:$ARCHIVE" \
+ "docker://$IMAGE_REPOSITORY:latest"
+ test "$(skopeo inspect --authfile "$auth_file" --format '{{.Digest}}' "docker://$IMAGE_REPOSITORY:latest")" = "$REGISTRY_DIGEST"
+ fi
diff --git a/Cargo.toml b/Cargo.toml
index b0c73d8..9a09d1c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pggomtm"
-version = "0.1.0"
+version = "0.1.0-rc.1"
edition = "2024"
license = "MIT"
publish = false
diff --git a/openspec/changes/extract-and-standardize-pggomtm/tasks.md b/openspec/changes/extract-and-standardize-pggomtm/tasks.md
index bbc3e1e..b1faefc 100644
--- a/openspec/changes/extract-and-standardize-pggomtm/tasks.md
+++ b/openspec/changes/extract-and-standardize-pggomtm/tasks.md
@@ -54,7 +54,7 @@
- [x] 6.3 用支持`pull_request`、`main` push与`workflow_call`的可复用只读`ci.yml`替代复杂`native-ci.yml`,移除candidate、ORAS evidence和跨仓consumer逻辑
- [x] 6.4 让可复用CI在release调用时上传同一run已验证OCI archive、Cargo.lock、resolved inputs与manifest输入,并证明后续publish无需重新运行Cargo或Docker build
- [x] 6.5 配置required CI与GitHub原生auto-merge,只允许owner、Agent或批准的Dependabot PR自动合并,外部PR保持人工批准且维护者/Agent可直接非force推进`main`
-- [ ] 6.6 取得PR与`main`的精确远端成功run,证明两者没有packages、Release、attestation或跨仓写权限,并更新README/维护文档的CI入口
+- [x] 6.6 取得PR与`main`的精确远端成功run,证明两者没有packages、Release、attestation或跨仓写权限,并更新README/维护文档的CI入口
## 7. 首个标准SemVer prerelease
From 6ddb149f1ea724b74c641b4f69a9b5876248002a Mon Sep 17 00:00:00 2001
From: a
Date: Sat, 18 Jul 2026 14:57:56 +0000
Subject: [PATCH 2/2] fix(release): align prerelease validation
---
tests/image-readiness.sh | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/tests/image-readiness.sh b/tests/image-readiness.sh
index 37fbfde..9aba35b 100755
--- a/tests/image-readiness.sh
+++ b/tests/image-readiness.sh
@@ -23,8 +23,9 @@ ARTIFACT_ROOT="$(realpath -- "$4")" || fail "artifact directory cannot be resolv
readonly ARTIFACT_ROOT
[[ "${SOURCE_REVISION}" =~ ^[0-9a-f]{40}$ ]] || fail "source must be a full Git commit"
-[[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+(\.[0-9]+)*)?$ ]] || \
- fail "version is not an mtmpg SemVer candidate"
+readonly SEMVER_IDENTIFIER='(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)'
+readonly SEMVER_PATTERN="^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-${SEMVER_IDENTIFIER}([.]${SEMVER_IDENTIFIER})*)?$"
+[[ "${VERSION}" =~ ${SEMVER_PATTERN} ]] || fail "version is not an mtmpg SemVer"
test -x "${ARTIFACT_ROOT}/pggomtm_oauth_smoke_client" || fail "OAuth client is unavailable"
test -x "${ARTIFACT_ROOT}/pggomtm_oauth_smoke_fixture" || fail "OAuth fixture is unavailable"
command -v "${DOCKER_BIN}" >/dev/null || fail "Docker is unavailable"