fix(ci): resolve published upgrade baseline by digest
quality / commitlint (push) Skipped
quality / changes (push) Successful in 7s
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m1s
quality / api (push) Successful in 1m35s
quality / linux-agent-smoke (push) Failing after 18s
quality / docker-check (push) Failing after 3m28s
CD / quality (push) Failing after 7m1s
CD / publish (push) Skipped

This commit is contained in:
Denozordec committed 2026-10-26 13:26:53 +07:00
1 parent a37ffdf061
commit 4db34f94f2
4 files changed
+94 -2

No files matched your search

+16 -1
View File
@@ -375,11 +375,26 @@ jobs:
- name: Capture exact built image identity
id: built-image
run: echo "id=$(docker image inspect --format '{{.Id}}' "$SMOKE_IMAGE")" >> "$GITHUB_OUTPUT"
- name: Built image runtime smoke
- name: Verify upgrade baseline resolver
run: sh scripts/ci/resolve-upgrade-baseline.test.sh
- name: Resolve immutable upgrade baseline
id: upgrade-baseline
env:
PREVIOUS_IMAGE: ${{ vars.EVOFW_PREVIOUS_IMAGE }}
REQUIRE_UPGRADE: ${{ !inputs.is_pull_request }}
FIRST_RELEASE: ${{ vars.EVOFW_FIRST_RELEASE == 'true' }}
REPOSITORY_OWNER: ${{ github.repository_owner }}
run: |
set -eu
owner_lc="$(printf '%s' "$REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')"
export IMAGE_REPOSITORY="git.shx.one/$owner_lc/evofw"
baseline="$(sh scripts/ci/resolve-upgrade-baseline.sh)"
printf 'image=%s\n' "$baseline" >> "$GITHUB_OUTPUT"
- name: Built image runtime smoke
env:
PREVIOUS_IMAGE: ${{ steps.upgrade-baseline.outputs.image }}
REQUIRE_UPGRADE: ${{ !inputs.is_pull_request }}
FIRST_RELEASE: ${{ vars.EVOFW_FIRST_RELEASE == 'true' }}
SMOKE_ARTIFACT_DIR: runtime-artifacts
BUILT_IMAGE: ${{ steps.built-image.outputs.id }}
run: sh scripts/ci/image-smoke.sh "$BUILT_IMAGE" "${PREVIOUS_IMAGE:-}"
+5 -1
View File
@@ -37,7 +37,11 @@ REQUIRE_UPGRADE=true sh scripts/ci/image-smoke.sh evofw-smoke:local \
git.shx.one/denozord/evofw@sha256:PREVIOUS_DIGEST
```
Smoke uses disposable volumes and verifies migration/native SQLite startup, `/health`, DB-backed `/ready`, API JSON, SPA deep links, agent script delivery, runtime file placement, clean SIGTERM, restart persistence, an online backup and restoration into another volume. Supply Gitea variable `EVOFW_PREVIOUS_IMAGE` with the last deployed version/digest to exercise upgrade migrations. Every CD quality invocation now builds and runs smoke, including docs-only releases, before semantic-release can publish. `EVOFW_PREVIOUS_IMAGE` must end in an immutable `@sha256:...` digest. Missing or mutable previous images fail release validation. Only an explicitly approved first deployment may set Gitea variable `EVOFW_FIRST_RELEASE=true`; unset it immediately after that first deployment. PR/development smoke can run without upgrade validation and reports NOT RUN. The image healthcheck uses `/ready` with 40s startup grace; Compose allows 30s shutdown and rotates JSON logs (3 x 10MB).
Smoke uses disposable volumes and verifies migration/native SQLite startup, `/health`, DB-backed `/ready`, API JSON, SPA deep links, agent script delivery, runtime file placement, clean SIGTERM, restart persistence, an online backup and restoration into another volume. Every CD quality invocation builds and runs smoke, including docs-only releases, before semantic-release can publish. The image healthcheck uses `/ready` with 40s startup grace; Compose allows 30s shutdown and rotates JSON logs (3 x 10MB).
Upgrade baseline selection is explicit and fail-closed. A configured Gitea variable `EVOFW_PREVIOUS_IMAGE` takes precedence and must be an immutable `repository@sha256:...` reference; use the actually deployed digest when that is the intended rehearsal baseline. Mutable overrides fail rather than silently selecting another image. When the variable is unset for a release run, `scripts/ci/resolve-upgrade-baseline.sh` pulls `git.shx.one/<lowercase-owner>/evofw:latest`, requires exactly one matching repository digest from Docker's `RepoDigests`, and passes that immutable reference to the unchanged smoke guard. This is the **previously published image**, which may differ from production. A pull failure, missing/unrelated/ambiguous digest or invalid reference fails CI; it never enables a first-release exception automatically. Registry access uses the existing anonymous/public pull path and does not expose registry secrets to PRs.
Only an explicitly approved first deployment may set `EVOFW_FIRST_RELEASE=true`; unset it immediately afterward. Without an explicit baseline, PR runs do not pull a published image and can perform development smoke with upgrade validation reported NOT RUN. A pinned override is honored for PRs and for explicitly configured first-release runs. Local shell syntax and the fake-Docker resolver regression passed, covering override priority, latest-to-digest resolution, pull failure, wrong/ambiguous repository digests, mutable override rejection, PR behavior and explicit first release. The real upgrade still requires CI execution; no new native pass is inferred from these checks.
## SQLite backup and recovery
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env sh
# Stdout is one immutable image reference (or empty for PR/explicit first release).
# A published baseline is not necessarily the operator's deployed production image.
set -eu
previous="${PREVIOUS_IMAGE:-}"
if [ -n "$previous" ]; then
printf '%s\n' "$previous" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._:/-]*@sha256:[0-9a-f]{64}$' || { echo 'EVOFW_PREVIOUS_IMAGE must be an immutable repository@sha256 digest reference' >&2; exit 1; }
printf '%s\n' "$previous"
exit 0
fi
if [ "${REQUIRE_UPGRADE:-false}" != true ] || [ "${FIRST_RELEASE:-false}" = true ]; then exit 0; fi
repository="${IMAGE_REPOSITORY:?IMAGE_REPOSITORY required for published upgrade baseline}"
printf '%s\n' "$repository" | grep -Eq '^[a-z0-9][a-z0-9._:/-]*$' || { echo 'Invalid published image repository' >&2; exit 1; }
echo 'Resolving the previously published latest image as upgrade baseline; set EVOFW_PREVIOUS_IMAGE to test an explicitly deployed digest instead' >&2
if ! docker pull "$repository:latest" >&2; then
echo 'Cannot pull published upgrade baseline. Supply EVOFW_PREVIOUS_IMAGE, or explicitly configure the approved first-release exception; no automatic exception is applied.' >&2
exit 1
fi
digests="$(docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$repository:latest")"
matches="$(printf '%s\n' "$digests" | awk -v prefix="$repository@" 'index($0,prefix)==1 {print}')"
count="$(printf '%s\n' "$matches" | awk 'NF {n++} END {print n+0}')"
[ "$count" = 1 ] || { echo 'Published image must have exactly one matching repository digest; refusing absent, unrelated or ambiguous RepoDigests' >&2; exit 1; }
digest="${matches#"$repository@"}"
printf '%s\n' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo 'Published image has an invalid repository digest' >&2; exit 1; }
printf '%s\n' "$repository@$digest"
@@ -0,0 +1,48 @@
#!/usr/bin/env sh
# No registry/network access: exercise the actual resolver with a fake Docker CLI.
set -eu
resolver="$(pwd)/scripts/ci/resolve-upgrade-baseline.sh"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
mkdir "$tmp/bin"
cat >"$tmp/bin/docker" <<'DOCKER'
#!/usr/bin/env sh
set -eu
printf '%s\n' "$*" >>"$FAKE_LOG"
case "$1" in
pull) exit "$FAKE_PULL_EXIT" ;;
image) printf '%s\n' "$FAKE_DIGESTS" ;;
*) exit 98 ;;
esac
DOCKER
chmod +x "$tmp/bin/docker"
export PATH="$tmp/bin:$PATH" FAKE_LOG="$tmp/docker.log" FAKE_PULL_EXIT=0
export IMAGE_REPOSITORY=git.shx.one/team/evofw REQUIRE_UPGRADE=true FIRST_RELEASE=false
digest="sha256:$(printf '%064d' 0)"
export FAKE_DIGESTS="$IMAGE_REPOSITORY@$digest" PREVIOUS_IMAGE="another.registry/deployed/app@$digest"
sh "$resolver" >"$tmp/result"
[ "$(cat "$tmp/result")" = "$PREVIOUS_IMAGE" ]
[ ! -s "$FAKE_LOG" ] || { echo 'Explicit pinned override must not resolve latest' >&2; exit 1; }
export PREVIOUS_IMAGE=''
sh "$resolver" >"$tmp/result"
[ "$(cat "$tmp/result")" = "$IMAGE_REPOSITORY@$digest" ]
grep -F "pull $IMAGE_REPOSITORY:latest" "$FAKE_LOG" >/dev/null
grep -F " $IMAGE_REPOSITORY:latest" "$FAKE_LOG" >/dev/null
export FAKE_PULL_EXIT=27
if sh "$resolver" >"$tmp/result" 2>/dev/null; then echo 'Missing published image must fail' >&2; exit 1; fi
[ ! -s "$tmp/result" ]
export FAKE_PULL_EXIT=0 FAKE_DIGESTS="wrong.registry/team/evofw@$digest"
if sh "$resolver" >"$tmp/result" 2>/dev/null; then echo 'Unrelated RepoDigest must fail' >&2; exit 1; fi
export FAKE_DIGESTS="$IMAGE_REPOSITORY@$digest
$IMAGE_REPOSITORY@sha256:$(printf '%064d' 1)"
if sh "$resolver" >"$tmp/result" 2>/dev/null; then echo 'Ambiguous RepoDigests must fail' >&2; exit 1; fi
export PREVIOUS_IMAGE="$IMAGE_REPOSITORY:latest"
if sh "$resolver" >"$tmp/result" 2>/dev/null; then echo 'Mutable override must fail rather than fall back' >&2; exit 1; fi
export PREVIOUS_IMAGE='' REQUIRE_UPGRADE=false
: >"$FAKE_LOG"
sh "$resolver" >"$tmp/result"
[ ! -s "$tmp/result" ] && [ ! -s "$FAKE_LOG" ]
export REQUIRE_UPGRADE=true FIRST_RELEASE=true
sh "$resolver" >"$tmp/result"
[ ! -s "$tmp/result" ] && [ ! -s "$FAKE_LOG" ]
echo 'Upgrade baseline resolver passed (override, latest digest, failure, wrong/ambiguous repo, PR and explicit first release)'