#!/usr/bin/env bash set -euo pipefail root=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT new_repo() { local name=$1 local repo=$tmp/$name mkdir -p "$repo/apps/api/migrations" "$repo/apps/web/src" "$repo/packages/contracts/src" "$repo/docs" git -C "$repo" init -q git -C "$repo" config user.name 'Manual Release Test' git -C "$repo" config user.email 'manual-release-test@example.invalid' printf 'package api\n' >"$repo/apps/api/main.go" printf 'SELECT 1;\n' >"$repo/apps/api/migrations/0001_init.sql" printf 'web\n' >"$repo/apps/web/src/app.ts" printf 'contracts\n' >"$repo/packages/contracts/src/index.ts" # shellcheck disable=SC2016 printf 'ARG API_RUNTIME_IMAGE=scratch\nARG WEB_RUNTIME_IMAGE=scratch\nFROM ${API_RUNTIME_IMAGE} AS api\nCOPY apps/api /app\nFROM ${WEB_RUNTIME_IMAGE} AS web\nCOPY apps/web /web\n' >"$repo/Dockerfile" printf 'docs\n' >"$repo/docs/readme.md" git -C "$repo" add -A git -C "$repo" commit -qm baseline printf '%s\n' "$repo" } classify() { local repo=$1 local base=$2 (cd "$repo" && node "$root/scripts/release-components.mjs" "$base") } repo=$(new_repo api) base=$(git -C "$repo" rev-parse HEAD) printf 'package api\n// changed\n' >"$repo/apps/api/main.go" git -C "$repo" add -A && git -C "$repo" commit -qm api result=$(classify "$repo" "$base") node -e 'const value=JSON.parse(process.argv[1]); if(value.components!=="api"||value.migrationsChanged)process.exit(1)' "$result" repo=$(new_repo web) base=$(git -C "$repo" rev-parse HEAD) printf 'web changed\n' >"$repo/apps/web/src/app.ts" git -C "$repo" add -A && git -C "$repo" commit -qm web result=$(classify "$repo" "$base") node -e 'const value=JSON.parse(process.argv[1]); if(value.components!=="web")process.exit(1)' "$result" repo=$(new_repo all) base=$(git -C "$repo" rev-parse HEAD) printf 'node_modules\n' >"$repo/.dockerignore" git -C "$repo" add -A && git -C "$repo" commit -qm all result=$(classify "$repo" "$base") node -e 'const value=JSON.parse(process.argv[1]); if(value.components!=="all")process.exit(1)' "$result" repo=$(new_repo api-docker-stage) base=$(git -C "$repo" rev-parse HEAD) # shellcheck disable=SC2016 printf 'ARG API_RUNTIME_IMAGE=scratch\nARG WEB_RUNTIME_IMAGE=scratch\nFROM ${API_RUNTIME_IMAGE} AS api\nCOPY apps/api /app\nRUN echo api\nFROM ${WEB_RUNTIME_IMAGE} AS web\nCOPY apps/web /web\n' >"$repo/Dockerfile" git -C "$repo" add -A && git -C "$repo" commit -qm api-docker-stage result=$(classify "$repo" "$base") node -e 'const value=JSON.parse(process.argv[1]); if(value.components!=="api")process.exit(1)' "$result" repo=$(new_repo web-docker-stage) base=$(git -C "$repo" rev-parse HEAD) printf '\nRUN echo web\n' >>"$repo/Dockerfile" git -C "$repo" add -A && git -C "$repo" commit -qm web-docker-stage result=$(classify "$repo" "$base") node -e 'const value=JSON.parse(process.argv[1]); if(value.components!=="web")process.exit(1)' "$result" repo=$(new_repo migration) base=$(git -C "$repo" rev-parse HEAD) printf 'ALTER TABLE demo ADD COLUMN note text;\n' >"$repo/apps/api/migrations/0002_note.sql" git -C "$repo" add -A && git -C "$repo" commit -qm migration result=$(classify "$repo" "$base") node -e 'const value=JSON.parse(process.argv[1]); if(value.components!=="api"||!value.migrationsChanged)process.exit(1)' "$result" repo=$(new_repo docs) base=$(git -C "$repo" rev-parse HEAD) printf 'docs changed\n' >"$repo/docs/readme.md" git -C "$repo" add -A && git -C "$repo" commit -qm docs result=$(classify "$repo" "$base") node -e 'const value=JSON.parse(process.argv[1]); if(value.components!=="none")process.exit(1)' "$result" bare=$tmp/origin.git git init -q --bare "$bare" repo=$(new_repo preflight) git -C "$repo" branch -M main git -C "$repo" remote add origin "$bare" git -C "$repo" push -q -u origin main (cd "$repo" && node "$root/scripts/release-preflight.mjs" >/dev/null) printf 'dirty\n' >>"$repo/docs/readme.md" if (cd "$repo" && node "$root/scripts/release-preflight.mjs" >/dev/null 2>&1); then echo 'dirty worktree passed release preflight' >&2 exit 1 fi git -C "$repo" restore docs/readme.md printf 'untracked\n' >"$repo/untracked" if (cd "$repo" && node "$root/scripts/release-preflight.mjs" >/dev/null 2>&1); then echo 'untracked file passed release preflight' >&2 exit 1 fi rm "$repo/untracked" git -C "$repo" switch -q -c feature printf 'feature\n' >>"$repo/docs/readme.md" git -C "$repo" add -A && git -C "$repo" commit -qm feature if (cd "$repo" && node "$root/scripts/release-preflight.mjs" >/dev/null 2>&1); then echo 'non-main commit passed release preflight' >&2 exit 1 fi manifest=$tmp/release.json recorded=$tmp/recorded.json source_sha=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa base_sha=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb api_image=registry.example.com/easyai/ai-gateway@sha256:$(printf '1%.0s' {1..64}) web_image=registry.example.com/easyai/ai-gateway-web@sha256:$(printf '2%.0s' {1..64}) RELEASE_SOURCE_SHA=$source_sha \ RELEASE_BASE_SHA=$base_sha \ RELEASE_COMPONENTS=api \ RELEASE_MIGRATIONS_CHANGED=false \ RELEASE_API_IMAGE=$api_image \ RELEASE_WEB_IMAGE=$web_image \ RELEASE_BUILD_COMPLETED_AT=2026-07-22T00:00:00Z \ RELEASE_SMOKE_COMPLETED_AT=2026-07-22T00:00:00Z \ node "$root/scripts/release-manifest.mjs" create "$manifest" >/dev/null node "$root/scripts/release-manifest.mjs" validate "$manifest" >/dev/null [[ $(node "$root/scripts/release-manifest.mjs" get "$manifest" images.api) == "$api_image" ]] bootstrap_manifest=$tmp/bootstrap.json RELEASE_SOURCE_SHA=$source_sha \ RELEASE_BASE_SHA= \ RELEASE_COMPONENTS=api \ RELEASE_MIGRATIONS_CHANGED=true \ RELEASE_API_IMAGE=$api_image \ RELEASE_WEB_IMAGE=$web_image \ RELEASE_BUILD_COMPLETED_AT=2026-07-22T00:00:00Z \ RELEASE_SMOKE_COMPLETED_AT=2026-07-22T00:00:00Z \ node "$root/scripts/release-manifest.mjs" create "$bootstrap_manifest" >/dev/null [[ -z $(node "$root/scripts/release-manifest.mjs" get "$bootstrap_manifest" baseReleaseSha) ]] || { echo 'bootstrap manifest returned a non-empty production base' >&2 exit 1 } RELEASE_DEPLOYED_AT=2026-07-22T00:01:00Z \ RELEASE_DEPLOY_DURATION_SECONDS=17 \ RELEASE_DEPLOY_ACTION=deploy \ node "$root/scripts/release-manifest.mjs" record-deployment "$manifest" "$recorded" >/dev/null node "$root/scripts/release-manifest.mjs" validate "$recorded" >/dev/null sensitive=$tmp/sensitive.json node -e 'const fs=require("fs"); const value=JSON.parse(fs.readFileSync(process.argv[1])); value.accessToken="forbidden"; fs.writeFileSync(process.argv[2], JSON.stringify(value))' "$manifest" "$sensitive" if node "$root/scripts/release-manifest.mjs" validate "$sensitive" >/dev/null 2>&1; then echo 'sensitive manifest field passed validation' >&2 exit 1 fi tampered=$tmp/tampered.json node -e 'const fs=require("fs"); const value=JSON.parse(fs.readFileSync(process.argv[1])); value.images.api=value.images.api.replace(/^registry[.]/, "mirror."); fs.writeFileSync(process.argv[2], JSON.stringify(value))' "$manifest" "$tampered" if node "$root/scripts/release-manifest.mjs" validate "$tampered" >/dev/null 2>&1; then echo 'tampered manifest content passed validation' >&2 exit 1 fi incomplete_digest=$tmp/incomplete-digest.json node -e 'const fs=require("fs"); const value=JSON.parse(fs.readFileSync(process.argv[1])); value.images.web="registry.example.com/easyai/ai-gateway-web:tag"; fs.writeFileSync(process.argv[2], JSON.stringify(value))' "$manifest" "$incomplete_digest" if node "$root/scripts/release-manifest.mjs" validate "$incomplete_digest" >/dev/null 2>&1; then echo 'tag-based image passed manifest validation' >&2 exit 1 fi if grep -Eqi 'password|secret|token|credential|private.?key' "$manifest"; then echo 'generated manifest contains a sensitive field name' >&2 exit 1 fi publisher_repo=$tmp/publisher mkdir -p "$publisher_repo/scripts" "$publisher_repo/apps/api" "$publisher_repo/fake-bin" cp "$root/scripts/publish-release-images.sh" \ "$root/scripts/release-preflight.mjs" \ "$root/scripts/release-manifest.mjs" \ "$root/scripts/release-components.mjs" \ "$root/scripts/ci-validate-migrations.mjs" \ "$publisher_repo/scripts/" printf 'FROM scratch\n' >"$publisher_repo/Dockerfile" printf 'module example.invalid/release-test\n\ngo 1.26\n' >"$publisher_repo/apps/api/go.mod" cat >"$publisher_repo/fake-bin/docker" <<'EOF' #!/usr/bin/env bash case "$*" in info|'buildx version'|'compose version') exit 0 ;; 'buildx imagetools inspect '*) if [[ ${FAKE_DOCKER_MODE:-} == registry ]]; then echo 'unauthorized: authentication required' >&2 else echo 'manifest unknown: not found' >&2 fi exit 1 ;; 'run --rm --platform linux/amd64 '*) exit 1 ;; esac echo "unexpected fake docker call: $*" >&2 exit 1 EOF cat >"$publisher_repo/fake-bin/ssh" <<'EOF' #!/usr/bin/env bash exit 1 EOF chmod +x "$publisher_repo/scripts/"* "$publisher_repo/fake-bin/"* git -C "$publisher_repo" init -q git -C "$publisher_repo" config user.name 'Manual Release Test' git -C "$publisher_repo" config user.email 'manual-release-test@example.invalid' git -C "$publisher_repo" add -A git -C "$publisher_repo" commit -qm baseline git -C "$publisher_repo" branch -M main publisher_bare=$tmp/publisher-origin.git git init -q --bare "$publisher_bare" git -C "$publisher_repo" remote add origin "$publisher_bare" git -C "$publisher_repo" push -q -u origin main if (cd "$publisher_repo" && PATH="$publisher_repo/fake-bin:$PATH" \ ./scripts/publish-release-images.sh --components auto >/dev/null 2>&1); then echo 'first release guessed components in auto mode' >&2 exit 1 fi if (cd "$publisher_repo" && PATH="$publisher_repo/fake-bin:$PATH" FAKE_DOCKER_MODE=registry \ ./scripts/publish-release-images.sh --components all >/dev/null 2>&1); then echo 'publisher passed without registry access' >&2 exit 1 fi if (cd "$publisher_repo" && PATH="$publisher_repo/fake-bin:$PATH" FAKE_DOCKER_MODE=platform \ ./scripts/publish-release-images.sh --components all >/dev/null 2>&1); then echo 'publisher passed without linux/amd64 execution support' >&2 exit 1 fi current_manifest=$tmp/current.json RELEASE_SOURCE_SHA=cccccccccccccccccccccccccccccccccccccccc \ RELEASE_BASE_SHA=dddddddddddddddddddddddddddddddddddddddd \ RELEASE_COMPONENTS=web \ RELEASE_MIGRATIONS_CHANGED=false \ RELEASE_API_IMAGE=$api_image \ RELEASE_WEB_IMAGE=$web_image \ RELEASE_BUILD_COMPLETED_AT=2026-07-22T00:02:00Z \ RELEASE_SMOKE_COMPLETED_AT=2026-07-22T00:02:00Z \ node "$root/scripts/release-manifest.mjs" create "$current_manifest" >/dev/null stale_bin=$tmp/stale-bin mkdir -p "$stale_bin" cat >"$stale_bin/ssh" <<'EOF' #!/usr/bin/env bash if [[ $* == *' status' ]]; then cat "$STALE_STATUS_FILE" fi exit 0 EOF cat >"$stale_bin/scp" <<'EOF' #!/usr/bin/env bash : >"$STALE_SCP_MARKER" exit 0 EOF chmod +x "$stale_bin/"* stale_marker=$tmp/stale-scp-called if PATH="$stale_bin:$PATH" STALE_STATUS_FILE=$current_manifest STALE_SCP_MARKER=$stale_marker \ "$root/scripts/deploy-production-release.sh" "$manifest" >/dev/null 2>&1; then echo 'stale production base passed deploy client validation' >&2 exit 1 fi [[ ! -e $stale_marker ]] || { echo 'stale deploy uploaded a manifest' >&2 exit 1 } if find "$root/.gitea/workflows" -type f -print -quit 2>/dev/null | grep -q .; then echo 'automatic Gitea workflow still exists' >&2 exit 1 fi grep -Fq 'stale release manifest' "$root/deploy/manual/easyai-ai-gateway-release" grep -Fq 'production_changed=false' "$root/scripts/publish-release-images.sh" echo 'manual_release_tests=PASS'