fix(smoke): hand the verifier the package's archive and digest as one readable release pair
CI / verify (pull_request) Successful in 58s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m51s
CI / image-build (pull_request) Successful in 22s
CI / deploy-smoke (same-origin) (pull_request) Successful in 42s
CI / deploy-smoke (split) (pull_request) Successful in 1m31s
CI / verify (pull_request) Successful in 58s
CI / infrastructure-smoke (pull_request) Successful in 36s
CI / migration-smoke (pull_request) Successful in 1m51s
CI / image-build (pull_request) Successful in 22s
CI / deploy-smoke (same-origin) (pull_request) Successful in 42s
CI / deploy-smoke (split) (pull_request) Successful in 1m31s
The split deploy smoke failed in CI with "The SHA-256 file could not be read." BuildKit's local exporter creates its output directory (`--output type=local,dest=$work/module-package`) with mode 0700, owned by whoever ran the build. That directory was mounted into the verifier, which runs as the image's unprivileged `node` user (uid 1000). In CI the build runs as root, so the directory was root:root 0700 and the verifier could read neither file; the digest file is simply read first. On a workstation the build runs as uid 1000, the same uid as `node`, so it was readable by coincidence. scripts/lib/module-package-release.sh turns one build's output into the release pair: it requires exactly one archive and its .sha256 and nothing else, checks that the digest line names that archive, then creates a fresh directory that must not exist yet (0755) and copies both files into it (0644). The smoke stages and mounts only that directory; BuildKit's output directory is never mounted. Archive-digest verification stays mandatory: the verifier still reads the .sha256 file. Nothing in the verifier changed. Tests: the helper is driven in bash against a 0700 build output, and refuses a missing, malformed, or mismatched digest, stray output, a second archive, and an existing release directory, creating nothing. A static test pins the smoke to the release pair and the verifier's single mount with --sha256-file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
35ef31df9f
commit
27d85e0162
@@ -0,0 +1,53 @@
|
||||
# shellcheck shell=bash
|
||||
# The deploy smoke test's module package release pair (ADR 0049).
|
||||
#
|
||||
# A package archive and its `.sha256` are one logical release: they travel together, and the
|
||||
# verifier reads both. This turns one build's output into exactly that pair, in a directory any user
|
||||
# can traverse, because the verifier runs as the image's unprivileged user.
|
||||
#
|
||||
# BuildKit's local exporter creates its output directory with mode 0700, owned by whoever ran the
|
||||
# build. On a workstation that is usually uid 1000, which is also the image's `node` user, so the
|
||||
# verifier happened to read it; in CI the build runs as root, and the verifier could read neither file.
|
||||
# Nothing here depends on who built the package.
|
||||
|
||||
# module_package_release <build output directory> <new release directory>
|
||||
#
|
||||
# Checks the build output holds exactly one archive and its digest and nothing else, and that the
|
||||
# digest line names that archive. Then creates the release directory, which must not exist yet (so an
|
||||
# earlier run's files can never be mixed in), mode 0755, and copies both files into it, mode 0644.
|
||||
# Prints the archive's file name. Fails, creating nothing, on any deviation.
|
||||
module_package_release() {
|
||||
local output="$1" release="$2" archives=() archive digest_line digest named entries
|
||||
|
||||
shopt -s nullglob
|
||||
archives=("$output"/*.tar.gz)
|
||||
shopt -u nullglob
|
||||
if ((${#archives[@]} != 1)); then
|
||||
echo "module_package_release: the build output must hold exactly one package archive." >&2
|
||||
return 1
|
||||
fi
|
||||
archive="$(basename -- "${archives[0]}")"
|
||||
if [[ ! -f "$output/$archive.sha256" ]]; then
|
||||
echo "module_package_release: the package's .sha256 is missing." >&2
|
||||
return 1
|
||||
fi
|
||||
entries="$(find "$output" -mindepth 1 -maxdepth 1 | wc -l)"
|
||||
if ((entries != 2)); then
|
||||
echo "module_package_release: the build output holds more than the archive and its digest." >&2
|
||||
return 1
|
||||
fi
|
||||
digest_line="$(<"$output/$archive.sha256")"
|
||||
digest="${digest_line%% *}"
|
||||
named="${digest_line#* }"
|
||||
if [[ ! "$digest" =~ ^[0-9a-f]{64}$ || "$named" != "$archive" ]]; then
|
||||
echo "module_package_release: the .sha256 line does not name this archive." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -m 755 -- "$release" || return 1
|
||||
if ! install -m 0644 -- "$output/$archive" "$output/$archive.sha256" "$release/"; then
|
||||
rm -rf -- "$release"
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$archive"
|
||||
}
|
||||
+11
-6
@@ -54,6 +54,8 @@ cd "$repository_root"
|
||||
source "$repository_root/scripts/lib/compose-diagnostics.sh"
|
||||
# shellcheck source=scripts/lib/ci-project.sh
|
||||
source "$repository_root/scripts/lib/ci-project.sh"
|
||||
# shellcheck source=scripts/lib/module-package-release.sh
|
||||
source "$repository_root/scripts/lib/module-package-release.sh"
|
||||
|
||||
for command in docker openssl awk; do
|
||||
command -v "$command" >/dev/null || {
|
||||
@@ -224,11 +226,14 @@ if [[ "$topology" == split ]]; then
|
||||
--label "${EKKLESIA_CI_LABEL}=${project}" --build-arg SOURCE_REVISION="$source_revision" \
|
||||
--output "type=local,dest=${work}/module-package" . >/dev/null ||
|
||||
fail "the module package did not build."
|
||||
package_archive="$(cd "$work/module-package" && ls -- *-any.tar.gz)"
|
||||
[[ "$package_archive" =~ ^example-[0-9]+\.[0-9]+\.[0-9]+-any\.tar\.gz$ &&
|
||||
-f "$work/module-package/${package_archive}.sha256" ]] ||
|
||||
# The archive and its digest are one release: copied together, from this run's own build output,
|
||||
# into a fresh directory the verifier's unprivileged user can read. BuildKit's output directory is
|
||||
# mode 0700 and owned by whoever built it, so it is never handed to the verifier itself.
|
||||
package_archive="$(module_package_release "$work/module-package" "$work/package")" ||
|
||||
fail "the packager did not produce exactly one archive and its digest."
|
||||
echo "ok packaged ${package_archive} ($(wc -c <"$work/module-package/$package_archive") bytes, sha256 $(cut -d' ' -f1 "$work/module-package/${package_archive}.sha256"))"
|
||||
[[ "$package_archive" =~ ^example-[0-9]+\.[0-9]+\.[0-9]+-any\.tar\.gz$ ]] ||
|
||||
fail "the packager produced an unexpected archive name."
|
||||
echo "ok packaged ${package_archive} ($(wc -c <"$work/package/$package_archive") bytes, sha256 $(cut -d' ' -f1 "$work/package/${package_archive}.sha256"))"
|
||||
fi
|
||||
|
||||
# The compose project directory: its relative bind mount (postgres/pg_hba.conf) resolves here.
|
||||
@@ -240,7 +245,7 @@ stage deploy
|
||||
mkdir -m 755 "$work/modules"
|
||||
stage modules
|
||||
if [[ "$topology" == split ]]; then
|
||||
stage module-package
|
||||
stage package
|
||||
# TEST-ONLY staging the verifier extracts into, as the image's unprivileged user. Nothing here is
|
||||
# module storage: the install step below moves a verified tree into module storage as root.
|
||||
mkdir -m 777 "$work/staging"
|
||||
@@ -443,7 +448,7 @@ install_test_module() {
|
||||
verified="$(
|
||||
docker run --rm --network none --read-only --tmpfs /tmp --cap-drop ALL \
|
||||
--security-opt no-new-privileges \
|
||||
-v "$daemon_work/module-package:/package:ro" -v "$daemon_work/staging:/staging" \
|
||||
-v "$daemon_work/package:/package:ro" -v "$daemon_work/staging:/staging" \
|
||||
--entrypoint node "$image" core/dist/cli/verify-module-package.js "/package/${package_archive}" \
|
||||
--sha256-file "/package/${package_archive}.sha256" --extract-to /staging/example 2>&1
|
||||
)" || fail "the module package did not verify: ${verified}"
|
||||
|
||||
Reference in New Issue
Block a user