222 lines
10 KiB
Bash
Executable File
222 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
VERSION=${1:-${KAIDI_RELEASE_VERSION:-}}
|
|
OUTPUT_DIR=${KAIDI_RELEASE_OUTPUT_DIR:-$ROOT/dist/release}
|
|
SIGNING_KEY=${KAIDI_RELEASE_SIGNING_KEY:-}
|
|
TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256:-}
|
|
WORK=$(mktemp -d)
|
|
FRONTEND_VERSION_OVERRIDDEN=false
|
|
|
|
cleanup() {
|
|
if [ "$FRONTEND_VERSION_OVERRIDDEN" = true ]; then
|
|
cp "$WORK/package.json" "$ROOT/frontend/package.json"
|
|
cp "$WORK/package-lock.json" "$ROOT/frontend/package-lock.json"
|
|
fi
|
|
rm -rf "$WORK"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
sha256_file() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
sha256sum "$1" | awk '{print $1}'
|
|
else
|
|
shasum -a 256 "$1" | awk '{print $1}'
|
|
fi
|
|
}
|
|
|
|
"$ROOT/scripts/check-semver.sh" "$VERSION" \
|
|
|| { printf 'Usage: %s VERSION\n' "$0" >&2; exit 2; }
|
|
|
|
SOURCE_REVISION=${KAIDI_SOURCE_REVISION:-}
|
|
if [ -z "$SOURCE_REVISION" ]; then
|
|
SOURCE_REVISION=$(git -C "$ROOT" rev-parse --verify HEAD 2>/dev/null || true)
|
|
fi
|
|
[[ "$SOURCE_REVISION" =~ ^[0-9A-Fa-f]{40}$ ]] \
|
|
|| { echo 'KAIDI_SOURCE_REVISION must be a 40-character Git commit SHA' >&2; exit 1; }
|
|
SOURCE_REVISION=$(printf '%s' "$SOURCE_REVISION" | tr '[:upper:]' '[:lower:]')
|
|
SOURCE_REF=${KAIDI_SOURCE_REF:-}
|
|
if [ -z "$SOURCE_REF" ]; then
|
|
SOURCE_REF=$(git -C "$ROOT" tag --points-at "$SOURCE_REVISION" 2>/dev/null \
|
|
| awk -v expected="v$VERSION" '$0 == expected { print; exit }')
|
|
fi
|
|
[ -n "$SOURCE_REF" ] || SOURCE_REF=local
|
|
[ "${#SOURCE_REF}" -le 128 ] && [[ "$SOURCE_REF" != *$'\n'* ]] && [[ "$SOURCE_REF" != *$'\r'* ]] \
|
|
|| { echo 'KAIDI_SOURCE_REF must be a single line of at most 128 characters' >&2; exit 1; }
|
|
SOURCE_DIRTY=${KAIDI_SOURCE_DIRTY:-}
|
|
if [ -z "$SOURCE_DIRTY" ]; then
|
|
if [ -n "$(git -C "$ROOT" status --porcelain --untracked-files=all 2>/dev/null)" ]; then
|
|
SOURCE_DIRTY=true
|
|
else
|
|
SOURCE_DIRTY=false
|
|
fi
|
|
fi
|
|
case "$SOURCE_DIRTY" in
|
|
true|false) ;;
|
|
*) echo 'KAIDI_SOURCE_DIRTY must be true or false' >&2; exit 1 ;;
|
|
esac
|
|
|
|
if [ "${KAIDI_SKIP_BUILD:-false}" != "true" ]; then
|
|
cp "$ROOT/frontend/package.json" "$WORK/package.json"
|
|
cp "$ROOT/frontend/package-lock.json" "$WORK/package-lock.json"
|
|
FRONTEND_VERSION_OVERRIDDEN=true
|
|
(cd "$ROOT/frontend" \
|
|
&& npm version "$VERSION" --no-git-tag-version --allow-same-version --ignore-scripts >/dev/null)
|
|
(cd "$ROOT/backend" && ./mvnw -q -Drevision="$VERSION" -DskipTests package)
|
|
(cd "$ROOT/frontend" && HUSKY=0 npm ci && npm run build)
|
|
fi
|
|
|
|
JAR="$ROOT/backend/target/finance-system-$VERSION.jar"
|
|
[ -s "$JAR" ] || { echo 'Backend release jar is missing' >&2; exit 1; }
|
|
[ -s "$ROOT/backend/target/backend-sbom.json" ] \
|
|
|| { echo 'Backend CycloneDX SBOM is missing; run the Maven package phase first' >&2; exit 1; }
|
|
[ -s "$ROOT/frontend/dist/index.html" ] || { echo 'Frontend release build is missing' >&2; exit 1; }
|
|
JAR_METADATA_DIR="$WORK/jar-metadata"
|
|
mkdir -p "$JAR_METADATA_DIR"
|
|
(cd "$JAR_METADATA_DIR" && jar -xf "$JAR" \
|
|
META-INF/MANIFEST.MF META-INF/maven/com.kaidi/finance-system/pom.properties)
|
|
JAR_POM_VERSION=$(sed -n 's/^version=//p' \
|
|
"$JAR_METADATA_DIR/META-INF/maven/com.kaidi/finance-system/pom.properties")
|
|
JAR_IMPLEMENTATION_VERSION=$(sed -n 's/^Implementation-Version: //p' \
|
|
"$JAR_METADATA_DIR/META-INF/MANIFEST.MF" | tr -d '\r')
|
|
[ "$JAR_POM_VERSION" = "$VERSION" ] && [ "$JAR_IMPLEMENTATION_VERSION" = "$VERSION" ] \
|
|
|| { printf 'JAR metadata versions do not match release %s\n' "$VERSION" >&2; exit 1; }
|
|
|
|
rm -rf "$OUTPUT_DIR"
|
|
mkdir -p "$OUTPUT_DIR"
|
|
STAGE="$WORK/stage"
|
|
mkdir -p "$STAGE/public" "$STAGE/ops"
|
|
cp "$JAR" "$STAGE/app.jar"
|
|
cp -R "$ROOT/frontend/dist/." "$STAGE/public/"
|
|
printf '%s\n' "$VERSION" > "$STAGE/VERSION"
|
|
cp "$ROOT/deploy/update.sh" "$STAGE/ops/update.sh"
|
|
cp "$ROOT/deploy/nginx/kaidi-finance.conf" "$STAGE/ops/kaidi-finance.conf"
|
|
cp "$ROOT/deploy/systemd/kaidi-finance.service" "$STAGE/ops/kaidi-finance.service"
|
|
cp "$ROOT/deploy/systemd/kaidi-update.service" "$STAGE/ops/kaidi-update.service"
|
|
cp "$ROOT/deploy/systemd/kaidi-update.path" "$STAGE/ops/kaidi-update.path"
|
|
chmod 0755 "$STAGE/ops/update.sh"
|
|
|
|
ARTIFACT="kaidi-finance-$VERSION.tar.gz"
|
|
COPYFILE_DISABLE=1 tar -czf "$OUTPUT_DIR/$ARTIFACT" -C "$STAGE" .
|
|
SHA256=$(sha256_file "$OUTPUT_DIR/$ARTIFACT")
|
|
cp "$ROOT/backend/target/backend-sbom.json" "$OUTPUT_DIR/backend-sbom.cdx.json"
|
|
(cd "$ROOT/frontend" && npm sbom --omit=dev --package-lock-only \
|
|
--sbom-format cyclonedx --sbom-type application) > "$OUTPUT_DIR/frontend-sbom.cdx.json"
|
|
BACKEND_BUILD_VERSION=$(jq -er '.metadata.component.version | strings | select(length > 0)' \
|
|
"$OUTPUT_DIR/backend-sbom.cdx.json")
|
|
FRONTEND_BUILD_VERSION=$(jq -er '.metadata.component.version | strings | select(length > 0)' \
|
|
"$OUTPUT_DIR/frontend-sbom.cdx.json")
|
|
[ "$BACKEND_BUILD_VERSION" = "$VERSION" ] \
|
|
|| { printf 'Backend build version %s does not match release %s\n' "$BACKEND_BUILD_VERSION" "$VERSION" >&2; exit 1; }
|
|
[ "$FRONTEND_BUILD_VERSION" = "$VERSION" ] \
|
|
|| { printf 'Frontend build version %s does not match release %s\n' "$FRONTEND_BUILD_VERSION" "$VERSION" >&2; exit 1; }
|
|
for sbom in "$OUTPUT_DIR/backend-sbom.cdx.json" "$OUTPUT_DIR/frontend-sbom.cdx.json"; do
|
|
jq -e --arg version "$VERSION" \
|
|
'. as $document | .metadata.component.version == $version
|
|
and (.metadata.component["bom-ref"] | strings | length > 0)
|
|
and (.metadata.component.purl | strings | length > 0)
|
|
and any(.dependencies[]; .ref == $document.metadata.component["bom-ref"])' \
|
|
"$sbom" >/dev/null \
|
|
|| { printf 'SBOM build-version references are inconsistent in %s\n' "$sbom" >&2; exit 1; }
|
|
done
|
|
BACKEND_SBOM_SHA256=$(sha256_file "$OUTPUT_DIR/backend-sbom.cdx.json")
|
|
FRONTEND_SBOM_SHA256=$(sha256_file "$OUTPUT_DIR/frontend-sbom.cdx.json")
|
|
|
|
if [ -n "$SIGNING_KEY" ]; then
|
|
[ -s "$SIGNING_KEY" ] || { echo 'KAIDI_RELEASE_SIGNING_KEY does not exist' >&2; exit 1; }
|
|
openssl rsa -in "$SIGNING_KEY" -check -noout >/dev/null 2>&1 \
|
|
|| { echo 'KAIDI_RELEASE_SIGNING_KEY must be an RSA private key' >&2; exit 1; }
|
|
SIGNING_KEY_BITS=$(openssl rsa -in "$SIGNING_KEY" -text -noout 2>/dev/null \
|
|
| sed -n 's/^Private-Key: (\([0-9][0-9]*\) bit.*$/\1/p' | head -n 1)
|
|
[ -n "$SIGNING_KEY_BITS" ] && [ "$SIGNING_KEY_BITS" -ge 3072 ] \
|
|
|| { echo 'KAIDI_RELEASE_SIGNING_KEY must use at least 3072 bits' >&2; exit 1; }
|
|
openssl pkey -in "$SIGNING_KEY" -pubout -out "$OUTPUT_DIR/release-public.pem"
|
|
elif [ "${KAIDI_ALLOW_UNSIGNED_RELEASE:-false}" = "true" ]; then
|
|
: > "$OUTPUT_DIR/release-public.pem"
|
|
else
|
|
echo 'KAIDI_RELEASE_SIGNING_KEY is required for an online-update release' >&2
|
|
exit 1
|
|
fi
|
|
|
|
install -m 0755 "$ROOT/deploy/install.sh" "$OUTPUT_DIR/install.sh"
|
|
PUBLIC_KEY_SHA256=$(sha256_file "$OUTPUT_DIR/release-public.pem")
|
|
INSTALLER_SHA256=$(sha256_file "$OUTPUT_DIR/install.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}$ ]] \
|
|
|| { echo 'KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256 must be 64 hexadecimal characters' >&2; exit 1; }
|
|
[ "$PUBLIC_KEY_SHA256" = "$NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256" ] \
|
|
|| { echo 'Release signing key does not match the trusted public-key SHA-256' >&2; exit 1; }
|
|
elif [ "${GITHUB_ACTIONS:-false}" = true ] || [ "${GITEA_ACTIONS:-false}" = true ]; then
|
|
echo 'KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256 is required in release CI' >&2
|
|
exit 1
|
|
fi
|
|
cat > "$OUTPUT_DIR/bootstrap-checksums.txt" <<EOF
|
|
KAIDI_RELEASE_PUBLIC_KEY_SHA256=$PUBLIC_KEY_SHA256
|
|
KAIDI_INSTALLER_SHA256=$INSTALLER_SHA256
|
|
EOF
|
|
BOOTSTRAP_SHA256=$(sha256_file "$OUTPUT_DIR/bootstrap-checksums.txt")
|
|
|
|
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" \
|
|
BOOTSTRAP_SHA256="$BOOTSTRAP_SHA256" SOURCE_REVISION="$SOURCE_REVISION" SOURCE_REF="$SOURCE_REF" \
|
|
SOURCE_DIRTY="$SOURCE_DIRTY" \
|
|
node <<'NODE' > "$OUTPUT_DIR/release-manifest.json"
|
|
const manifest = {
|
|
version: process.env.VERSION,
|
|
artifact: process.env.ARTIFACT,
|
|
sha256: process.env.SHA256,
|
|
publishedAt: new Date().toISOString(),
|
|
minimumJava: 17,
|
|
source: {
|
|
revision: process.env.SOURCE_REVISION,
|
|
ref: process.env.SOURCE_REF,
|
|
dirty: process.env.SOURCE_DIRTY === 'true',
|
|
},
|
|
buildVersions: {
|
|
backend: process.env.BACKEND_BUILD_VERSION,
|
|
frontend: process.env.FRONTEND_BUILD_VERSION,
|
|
},
|
|
releaseNotes: process.env.RELEASE_NOTES,
|
|
sboms: [
|
|
{ ecosystem: 'maven', artifact: 'backend-sbom.cdx.json', sha256: process.env.BACKEND_SBOM_SHA256 },
|
|
{ ecosystem: 'npm', artifact: 'frontend-sbom.cdx.json', sha256: process.env.FRONTEND_SBOM_SHA256 },
|
|
],
|
|
bootstrap: {
|
|
installer: 'install.sh',
|
|
installerSha256: process.env.INSTALLER_SHA256,
|
|
publicKey: 'release-public.pem',
|
|
publicKeySha256: process.env.PUBLIC_KEY_SHA256,
|
|
checksums: 'bootstrap-checksums.txt',
|
|
checksumsSha256: process.env.BOOTSTRAP_SHA256,
|
|
},
|
|
};
|
|
process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
NODE
|
|
|
|
if [ -n "$SIGNING_KEY" ]; then
|
|
openssl dgst -sha256 -sign "$SIGNING_KEY" \
|
|
-out "$OUTPUT_DIR/release-manifest.sig" "$OUTPUT_DIR/release-manifest.json"
|
|
else
|
|
: > "$OUTPUT_DIR/release-manifest.sig"
|
|
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 \
|
|
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 \
|
|
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"
|