This commit is contained in:
@@ -143,8 +143,10 @@ else
|
||||
fi
|
||||
|
||||
install -m 0755 "$ROOT/deploy/install.sh" "$OUTPUT_DIR/install.sh"
|
||||
install -m 0755 "$ROOT/deploy/purge.sh" "$OUTPUT_DIR/purge.sh"
|
||||
PUBLIC_KEY_SHA256=$(sha256_file "$OUTPUT_DIR/release-public.pem")
|
||||
INSTALLER_SHA256=$(sha256_file "$OUTPUT_DIR/install.sh")
|
||||
PURGER_SHA256=$(sha256_file "$OUTPUT_DIR/purge.sh")
|
||||
if [ -n "$TRUSTED_PUBLIC_KEY_SHA256" ]; then
|
||||
NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256=$(printf '%s' "$TRUSTED_PUBLIC_KEY_SHA256" | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|
||||
@@ -158,6 +160,7 @@ fi
|
||||
cat > "$OUTPUT_DIR/bootstrap-checksums.txt" <<EOF
|
||||
KAIDI_RELEASE_PUBLIC_KEY_SHA256=$PUBLIC_KEY_SHA256
|
||||
KAIDI_INSTALLER_SHA256=$INSTALLER_SHA256
|
||||
KAIDI_PURGER_SHA256=$PURGER_SHA256
|
||||
EOF
|
||||
BOOTSTRAP_SHA256=$(sha256_file "$OUTPUT_DIR/bootstrap-checksums.txt")
|
||||
|
||||
@@ -165,7 +168,8 @@ RELEASE_NOTES_VALUE=${KAIDI_RELEASE_NOTES:-"Kaidi Finance Preview $VERSION"}
|
||||
VERSION="$VERSION" ARTIFACT="$ARTIFACT" SHA256="$SHA256" RELEASE_NOTES="$RELEASE_NOTES_VALUE" \
|
||||
BACKEND_SBOM_SHA256="$BACKEND_SBOM_SHA256" FRONTEND_SBOM_SHA256="$FRONTEND_SBOM_SHA256" \
|
||||
BACKEND_BUILD_VERSION="$BACKEND_BUILD_VERSION" FRONTEND_BUILD_VERSION="$FRONTEND_BUILD_VERSION" \
|
||||
INSTALLER_SHA256="$INSTALLER_SHA256" PUBLIC_KEY_SHA256="$PUBLIC_KEY_SHA256" \
|
||||
INSTALLER_SHA256="$INSTALLER_SHA256" PURGER_SHA256="$PURGER_SHA256" \
|
||||
PUBLIC_KEY_SHA256="$PUBLIC_KEY_SHA256" \
|
||||
BOOTSTRAP_SHA256="$BOOTSTRAP_SHA256" SOURCE_REVISION="$SOURCE_REVISION" SOURCE_REF="$SOURCE_REF" \
|
||||
SOURCE_DIRTY="$SOURCE_DIRTY" \
|
||||
node <<'NODE' > "$OUTPUT_DIR/release-manifest.json"
|
||||
@@ -192,6 +196,8 @@ const manifest = {
|
||||
bootstrap: {
|
||||
installer: 'install.sh',
|
||||
installerSha256: process.env.INSTALLER_SHA256,
|
||||
purger: 'purge.sh',
|
||||
purgerSha256: process.env.PURGER_SHA256,
|
||||
publicKey: 'release-public.pem',
|
||||
publicKeySha256: process.env.PUBLIC_KEY_SHA256,
|
||||
checksums: 'bootstrap-checksums.txt',
|
||||
@@ -211,14 +217,15 @@ fi
|
||||
cd "$OUTPUT_DIR"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$ARTIFACT" backend-sbom.cdx.json frontend-sbom.cdx.json \
|
||||
release-manifest.json release-manifest.sig release-public.pem install.sh \
|
||||
release-manifest.json release-manifest.sig release-public.pem install.sh purge.sh \
|
||||
bootstrap-checksums.txt > SHA256SUMS
|
||||
else
|
||||
shasum -a 256 "$ARTIFACT" backend-sbom.cdx.json frontend-sbom.cdx.json \
|
||||
release-manifest.json release-manifest.sig release-public.pem install.sh \
|
||||
release-manifest.json release-manifest.sig release-public.pem install.sh purge.sh \
|
||||
bootstrap-checksums.txt > SHA256SUMS
|
||||
fi
|
||||
)
|
||||
printf 'Release assets created in %s\n' "$OUTPUT_DIR"
|
||||
printf 'Trusted release public-key SHA-256: %s\n' "$PUBLIC_KEY_SHA256"
|
||||
printf 'Installer SHA-256: %s\n' "$INSTALLER_SHA256"
|
||||
printf 'Purger SHA-256: %s\n' "$PURGER_SHA256"
|
||||
|
||||
@@ -87,6 +87,7 @@ ASSETS=(
|
||||
release-manifest.sig
|
||||
release-public.pem
|
||||
install.sh
|
||||
purge.sh
|
||||
bootstrap-checksums.txt
|
||||
SHA256SUMS
|
||||
)
|
||||
@@ -124,7 +125,7 @@ jq -n \
|
||||
> "$WORK/publish.json"
|
||||
publish_status=$(request PATCH "$API/releases/$RELEASE_ID" "$WORK/published.json" "$WORK/publish.json")
|
||||
[ "$publish_status" = 200 ] || fail "release publication returned HTTP $publish_status"
|
||||
jq -e '.draft == false and .prerelease == false and (.assets | length) == 9' \
|
||||
jq -e '.draft == false and .prerelease == false and (.assets | length) == 10' \
|
||||
"$WORK/published.json" >/dev/null || fail 'published release state is invalid'
|
||||
|
||||
printf 'Published Gitea release %s with nine verified assets\n' "$TAG"
|
||||
printf 'Published Gitea release %s with ten verified assets\n' "$TAG"
|
||||
|
||||
@@ -14,7 +14,7 @@ RELEASE="$WORK/release"
|
||||
mkdir -p "$RELEASE/ops" "$RELEASE/public" "$WORK/files" "$WORK/tmp"
|
||||
printf 'fixture jar\n' > "$RELEASE/app.jar"
|
||||
printf '<!doctype html>\n' > "$RELEASE/public/index.html"
|
||||
printf '1.0.0-preview.23\n' > "$RELEASE/VERSION"
|
||||
printf '1.0.0-preview.24\n' > "$RELEASE/VERSION"
|
||||
cp "$ROOT/deploy/baota-start.sh" "$RELEASE/ops/baota-start.sh"
|
||||
chmod 0755 "$RELEASE/ops/baota-start.sh"
|
||||
|
||||
@@ -46,7 +46,7 @@ FILE_STORAGE_TEMP="$WORK/tmp" \
|
||||
grep -Fqx "cwd=$RELEASE" "$WORK/java.log" || fail 'launcher did not use the release working directory'
|
||||
grep -Fqx "args=-XX:MaxRAMPercentage=70 -Dfile.encoding=UTF-8 -jar $RELEASE/app.jar" "$WORK/java.log" \
|
||||
|| fail 'launcher did not execute the packaged Spring Boot jar'
|
||||
grep -Fqx 'version=1.0.0-preview.23' "$WORK/java.log" || fail 'launcher did not read VERSION'
|
||||
grep -Fqx 'version=1.0.0-preview.24' "$WORK/java.log" || fail 'launcher did not read VERSION'
|
||||
grep -Fqx 'address=127.0.0.1' "$WORK/java.log" || fail 'launcher did not default to loopback'
|
||||
grep -Fqx 'port=18080' "$WORK/java.log" || fail 'launcher did not default to port 18080'
|
||||
grep -Fqx 'update=false' "$WORK/java.log" || fail 'launcher did not disable the systemd updater'
|
||||
|
||||
@@ -24,6 +24,7 @@ for name in \
|
||||
release-manifest.sig \
|
||||
release-public.pem \
|
||||
install.sh \
|
||||
purge.sh \
|
||||
bootstrap-checksums.txt \
|
||||
SHA256SUMS; do
|
||||
printf 'fixture asset %s\n' "$name" > "$RELEASE_DIR/$name"
|
||||
@@ -102,11 +103,11 @@ GITEA_TOKEN="$TOKEN" \
|
||||
KAIDI_RELEASE_DIR="$RELEASE_DIR" \
|
||||
"$ROOT/scripts/publish-gitea-release.sh" > "$WORK/publish.log"
|
||||
|
||||
grep -Fqx "Published Gitea release $TAG with nine verified assets" "$WORK/publish.log" \
|
||||
grep -Fqx "Published Gitea release $TAG with ten verified assets" "$WORK/publish.log" \
|
||||
|| fail 'publish script did not report success'
|
||||
[ "$(wc -l < "$WORK/assets.jsonl" | tr -d '[:space:]')" -eq 9 ] \
|
||||
|| fail 'publish script did not upload exactly nine assets'
|
||||
[ "$(wc -l < "$WORK/assets.jsonl" | tr -d '[:space:]')" -eq 10 ] \
|
||||
|| fail 'publish script did not upload exactly ten assets'
|
||||
! grep -Fq "$TOKEN" "$WORK/curl.log" \
|
||||
|| fail 'Gitea write token leaked into curl process arguments'
|
||||
|
||||
printf 'Gitea draft, nine-asset verification, and publication fixture passed\n'
|
||||
printf 'Gitea draft, ten-asset verification, and publication fixture passed\n'
|
||||
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
fail() {
|
||||
printf 'Purge fixture failed: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
sed '$d' "$ROOT/deploy/purge.sh" > "$WORK/purge-functions.sh"
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
source "$WORK/purge-functions.sh"
|
||||
|
||||
install() {
|
||||
local -a arguments=()
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-o|-g) shift 2 ;;
|
||||
*) arguments+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
command install "${arguments[@]}"
|
||||
}
|
||||
|
||||
APP_ROOT="$WORK/opt/kaidi"
|
||||
BAOTA_ROOT="$WORK/www/kaidi"
|
||||
CONFIG_ROOT="$WORK/etc/kaidi"
|
||||
STATE_ROOT="$WORK/var/lib/kaidi"
|
||||
UPDATE_STATE_ROOT="$WORK/var/lib/kaidi-update"
|
||||
LOG_ROOT="$WORK/var/log/kaidi"
|
||||
FIRST_LOGIN_FILE="$WORK/root/kaidi-first-login.txt"
|
||||
# shellcheck disable=SC2034 # Referenced by the sourced purge helper.
|
||||
BACKUP_ROOT="$WORK/recovery"
|
||||
BACKUP_ARCHIVE=
|
||||
|
||||
mkdir -p \
|
||||
"$APP_ROOT" \
|
||||
"$BAOTA_ROOT" \
|
||||
"$CONFIG_ROOT" \
|
||||
"$STATE_ROOT/files" \
|
||||
"$UPDATE_STATE_ROOT/backups" \
|
||||
"$UPDATE_STATE_ROOT/failed" \
|
||||
"$UPDATE_STATE_ROOT/transactions" \
|
||||
"$LOG_ROOT" \
|
||||
"$(dirname "$FIRST_LOGIN_FILE")" \
|
||||
"$WORK/external-mysql" \
|
||||
"$WORK/reverse-proxy"
|
||||
printf 'DB_PASSWORD="secret"\n' > "$CONFIG_ROOT/kaidi.env"
|
||||
printf 'document\n' > "$STATE_ROOT/files/document.txt"
|
||||
printf 'dump\n' > "$UPDATE_STATE_ROOT/backups/mysql.sql"
|
||||
printf 'setup code\n' > "$FIRST_LOGIN_FILE"
|
||||
printf 'database sentinel\n' > "$WORK/external-mysql/keep"
|
||||
printf 'proxy sentinel\n' > "$WORK/reverse-proxy/keep"
|
||||
|
||||
create_recovery_backup
|
||||
[ -s "$BACKUP_ARCHIVE" ] || fail 'root-only recovery archive was not created'
|
||||
[ "$(stat -c '%a' "$BACKUP_ARCHIVE" 2>/dev/null || stat -f '%Lp' "$BACKUP_ARCHIVE")" = 600 ] \
|
||||
|| fail 'recovery archive mode is not 0600'
|
||||
archive_list=$(tar -tzf "$BACKUP_ARCHIVE")
|
||||
grep -Fq "${CONFIG_ROOT#/}/kaidi.env" <<< "$archive_list" \
|
||||
|| fail 'configuration was omitted from the recovery archive'
|
||||
grep -Fq "${STATE_ROOT#/}/files/document.txt" <<< "$archive_list" \
|
||||
|| fail 'file storage was omitted from the recovery archive'
|
||||
grep -Fq "${UPDATE_STATE_ROOT#/}/backups/mysql.sql" <<< "$archive_list" \
|
||||
|| fail 'database backup was omitted from the recovery archive'
|
||||
|
||||
remove_managed_paths
|
||||
for path in "$APP_ROOT" "$BAOTA_ROOT" "$CONFIG_ROOT" "$STATE_ROOT" "$UPDATE_STATE_ROOT" "$LOG_ROOT"; do
|
||||
[ ! -e "$path" ] || fail "managed path was not removed: $path"
|
||||
done
|
||||
[ ! -e "$FIRST_LOGIN_FILE" ] || fail 'first-login file was not removed'
|
||||
[ -f "$WORK/external-mysql/keep" ] || fail 'external database state was removed'
|
||||
[ -f "$WORK/reverse-proxy/keep" ] || fail 'reverse-proxy state was removed'
|
||||
[ -s "$BACKUP_ARCHIVE" ] || fail 'recovery archive was removed with the installation'
|
||||
|
||||
[ "$(tail -n 1 "$ROOT/deploy/purge.sh")" = 'main "$@"' ] \
|
||||
|| fail 'purge actions can execute before a complete curl stream is parsed'
|
||||
# shellcheck disable=SC2016 # Match the literal guard in the purge source.
|
||||
grep -Fq 'KAIDI_PURGE_CONFIRM=$REQUIRED_CONFIRMATION' "$ROOT/deploy/purge.sh" \
|
||||
|| fail 'destructive removal no longer requires explicit confirmation'
|
||||
grep -Fq 'External MySQL data and reverse-proxy configuration will not be modified' "$ROOT/deploy/purge.sh" \
|
||||
|| fail 'purge boundary is no longer explicit'
|
||||
|
||||
printf 'Local purge and recovery fixture passed\n'
|
||||
@@ -40,6 +40,7 @@ printf '%s\n' \
|
||||
bootstrap-checksums.txt \
|
||||
frontend-sbom.cdx.json \
|
||||
install.sh \
|
||||
purge.sh \
|
||||
"$ARTIFACT" \
|
||||
release-manifest.json \
|
||||
release-manifest.sig \
|
||||
@@ -49,7 +50,7 @@ for path in ./* ./.[!.]* ./..?*; do
|
||||
basename "$path"
|
||||
done | LC_ALL=C sort > "$WORK/actual-assets.txt"
|
||||
diff -u "$WORK/expected-assets.txt" "$WORK/actual-assets.txt" \
|
||||
|| { printf 'Release directory must contain exactly the nine published assets\n' >&2; exit 1; }
|
||||
|| { printf 'Release directory must contain exactly the ten published assets\n' >&2; exit 1; }
|
||||
EXPECTED_ARTIFACT_SHA256=$(jq -er \
|
||||
'.sha256 | strings | ascii_downcase | select(test("^[0-9a-f]{64}$"))' \
|
||||
release-manifest.json)
|
||||
@@ -109,16 +110,21 @@ done
|
||||
|
||||
PUBLIC_KEY_SHA256=$(sha256_file release-public.pem)
|
||||
INSTALLER_SHA256=$(sha256_file install.sh)
|
||||
PURGER_SHA256=$(sha256_file purge.sh)
|
||||
BOOTSTRAP_SHA256=$(sha256_file bootstrap-checksums.txt)
|
||||
SIGNED_INSTALLER=$(jq -er '.bootstrap.installer' release-manifest.json)
|
||||
SIGNED_PURGER=$(jq -er '.bootstrap.purger' release-manifest.json)
|
||||
SIGNED_PUBLIC_KEY=$(jq -er '.bootstrap.publicKey' release-manifest.json)
|
||||
SIGNED_BOOTSTRAP=$(jq -er '.bootstrap.checksums' release-manifest.json)
|
||||
[ "$SIGNED_INSTALLER" = install.sh ] && [ "$SIGNED_PUBLIC_KEY" = release-public.pem ] \
|
||||
[ "$SIGNED_INSTALLER" = install.sh ] && [ "$SIGNED_PURGER" = purge.sh ] \
|
||||
&& [ "$SIGNED_PUBLIC_KEY" = release-public.pem ] \
|
||||
&& [ "$SIGNED_BOOTSTRAP" = bootstrap-checksums.txt ] \
|
||||
|| { printf 'Release manifest bootstrap asset names are invalid\n' >&2; exit 1; }
|
||||
jq -e --arg installer "$INSTALLER_SHA256" --arg publicKey "$PUBLIC_KEY_SHA256" \
|
||||
jq -e --arg installer "$INSTALLER_SHA256" --arg purger "$PURGER_SHA256" \
|
||||
--arg publicKey "$PUBLIC_KEY_SHA256" \
|
||||
--arg bootstrap "$BOOTSTRAP_SHA256" \
|
||||
'.bootstrap.installerSha256 == $installer
|
||||
and .bootstrap.purgerSha256 == $purger
|
||||
and .bootstrap.publicKeySha256 == $publicKey
|
||||
and .bootstrap.checksumsSha256 == $bootstrap' release-manifest.json >/dev/null \
|
||||
|| { printf 'Bootstrap assets do not match the signed manifest\n' >&2; exit 1; }
|
||||
@@ -131,6 +137,8 @@ NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256=$(printf '%s' "$TRUSTED_PUBLIC_KEY_SHA256"
|
||||
|| { printf 'Bootstrap public-key fingerprint does not match\n' >&2; exit 1; }
|
||||
[ "$(sed -n 's/^KAIDI_INSTALLER_SHA256=//p' bootstrap-checksums.txt)" = "$INSTALLER_SHA256" ] \
|
||||
|| { printf 'Bootstrap installer digest does not match\n' >&2; exit 1; }
|
||||
[ "$(sed -n 's/^KAIDI_PURGER_SHA256=//p' bootstrap-checksums.txt)" = "$PURGER_SHA256" ] \
|
||||
|| { printf 'Bootstrap purger digest does not match\n' >&2; exit 1; }
|
||||
|
||||
ARCHIVE_LIST=$(tar -tzf "$ARTIFACT")
|
||||
if grep -Eq '(^/|(^|/)\.\.(/|$))' <<< "$ARCHIVE_LIST"; then
|
||||
@@ -171,5 +179,8 @@ tar -tvzf "$ARTIFACT" ./ops/baota-start.sh | grep -Eq '^-.{2}x.{2}x.{2}x' \
|
||||
bash -n "$ROOT/deploy/baota-start.sh"
|
||||
cmp install.sh "$ROOT/deploy/install.sh" \
|
||||
|| { printf 'Release installer does not match deploy/install.sh\n' >&2; exit 1; }
|
||||
bash -n "$ROOT/deploy/purge.sh"
|
||||
cmp purge.sh "$ROOT/deploy/purge.sh" \
|
||||
|| { printf 'Release purger does not match deploy/purge.sh\n' >&2; exit 1; }
|
||||
|
||||
printf 'Release %s verified; installer SHA-256: %s\n' "$VERSION" "$INSTALLER_SHA256"
|
||||
|
||||
Reference in New Issue
Block a user