#!/bin/bash
|
# Generate internal and public release manifests from an existing Aligner release
|
# directory. This does not upload, notarize, staple, or mark artifacts as public.
|
|
set -euo pipefail
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
SOURCE_ROOT="$OUTPUT_ROOT/C1.source"
|
INFO_PLIST="$SOURCE_ROOT/Resources/Aligner-Info.plist"
|
|
fail() {
|
echo "generate-release-manifest.sh failed: $*" >&2
|
exit 1
|
}
|
|
usage() {
|
cat <<'USAGE' >&2
|
Usage:
|
03-O/C3.tools/generate-release-manifest.sh <release-dir>
|
|
Optional environment:
|
ALIGNER_RELEASE_DOWNLOAD_URL
|
ALIGNER_RELEASE_NOTES_URL
|
ALIGNER_RELEASE_NOTARIZED=1
|
ALIGNER_NOTARIZATION_REQUEST_ID
|
ALIGNER_SIGNING_IDENTITY
|
ALIGNER_TEAM_ID
|
USAGE
|
}
|
|
RELEASE_DIR="${1:-}"
|
[ -n "$RELEASE_DIR" ] || { usage; exit 2; }
|
[ -d "$RELEASE_DIR" ] || fail "release dir does not exist: $RELEASE_DIR"
|
|
VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$INFO_PLIST" 2>/dev/null)" \
|
|| fail "unable to read CFBundleShortVersionString"
|
BUILD="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$INFO_PLIST" 2>/dev/null)" \
|
|| fail "unable to read CFBundleVersion"
|
MINIMUM_MACOS="$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$INFO_PLIST" 2>/dev/null)" \
|
|| fail "unable to read LSMinimumSystemVersion"
|
DMG_NAME="Aligner-${VERSION}-build${BUILD}.dmg"
|
DMG_PATH="$RELEASE_DIR/$DMG_NAME"
|
SUMS_PATH="$RELEASE_DIR/SHA256SUMS.txt"
|
INTERNAL_MANIFEST="$RELEASE_DIR/release-manifest.json"
|
PUBLIC_MANIFEST="$RELEASE_DIR/public-latest.json"
|
|
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|
|| fail "version must be MAJOR.MINOR.PATCH, got: $VERSION"
|
[[ "$BUILD" =~ ^[0-9]{8}\.[0-9]{4}$ ]] \
|
|| fail "build must be YYYYMMDD.HHMM, got: $BUILD"
|
[ -f "$DMG_PATH" ] || fail "missing expected DMG: $DMG_PATH"
|
[ -f "$SUMS_PATH" ] || fail "missing SHA256SUMS.txt: $SUMS_PATH"
|
|
DMG_SHA256="$(awk -v name="$DMG_NAME" '$2 == name { print $1 }' "$SUMS_PATH")"
|
[ -n "$DMG_SHA256" ] || fail "SHA256SUMS.txt does not contain $DMG_NAME"
|
ACTUAL_SHA="$(shasum -a 256 "$DMG_PATH" | awk '{ print $1 }')"
|
[ "$DMG_SHA256" = "$ACTUAL_SHA" ] || fail "SHA256 mismatch for $DMG_NAME"
|
DMG_SIZE="$(stat -f%z "$DMG_PATH")"
|
|
GIT_BRANCH="$(git -C "$OUTPUT_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
GIT_COMMIT="$(git -C "$OUTPUT_ROOT" rev-parse HEAD 2>/dev/null || true)"
|
EXPECTED_GIT_TAG="v${VERSION}-build${BUILD}"
|
ACTUAL_GIT_TAG="$(git -C "$OUTPUT_ROOT" describe --tags --exact-match 2>/dev/null || true)"
|
if [ "$ACTUAL_GIT_TAG" = "$EXPECTED_GIT_TAG" ]; then
|
GIT_TAG="$ACTUAL_GIT_TAG"
|
else
|
GIT_TAG=""
|
fi
|
if git -C "$OUTPUT_ROOT" diff --quiet --ignore-submodules -- 2>/dev/null \
|
&& git -C "$OUTPUT_ROOT" diff --cached --quiet --ignore-submodules -- 2>/dev/null \
|
&& [ -z "$(git -C "$OUTPUT_ROOT" ls-files --others --exclude-standard)" ]; then
|
SOURCE_STATUS="clean"
|
else
|
SOURCE_STATUS="dirty"
|
fi
|
|
CREATED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
CREATED_BY="$(id -un)"
|
NOTARIZED=false
|
if [ "${ALIGNER_RELEASE_NOTARIZED:-0}" = "1" ]; then
|
NOTARIZED=true
|
fi
|
|
/usr/bin/python3 - \
|
"$INTERNAL_MANIFEST" \
|
"$PUBLIC_MANIFEST" \
|
"$VERSION" \
|
"$BUILD" \
|
"$MINIMUM_MACOS" \
|
"$DMG_PATH" \
|
"$DMG_SHA256" \
|
"$DMG_SIZE" \
|
"$GIT_BRANCH" \
|
"$GIT_COMMIT" \
|
"$GIT_TAG" \
|
"$EXPECTED_GIT_TAG" \
|
"$SOURCE_STATUS" \
|
"$CREATED_AT" \
|
"$CREATED_BY" \
|
"$NOTARIZED" <<'PY'
|
import json
|
import os
|
import pathlib
|
import sys
|
|
(
|
internal_path,
|
public_path,
|
version,
|
build,
|
minimum_macos,
|
dmg_path,
|
sha256,
|
size,
|
git_branch,
|
git_commit,
|
git_tag,
|
expected_git_tag,
|
source_status,
|
created_at,
|
created_by,
|
notarized_text,
|
) = sys.argv[1:]
|
|
notarized = notarized_text.lower() == "true"
|
download_url = os.environ.get("ALIGNER_RELEASE_DOWNLOAD_URL", "")
|
release_notes_url = os.environ.get("ALIGNER_RELEASE_NOTES_URL", "")
|
|
internal = {
|
"product": "Aligner",
|
"version": version,
|
"build": build,
|
"gitBranch": git_branch,
|
"gitCommit": git_commit,
|
"gitTag": git_tag,
|
"expectedGitTag": expected_git_tag,
|
"sourceStatus": source_status,
|
"buildCommand": "03-O/C3.tools/package-app.sh",
|
"signingIdentity": os.environ.get("ALIGNER_SIGNING_IDENTITY", ""),
|
"teamID": os.environ.get("ALIGNER_TEAM_ID", ""),
|
"notarizationRequestID": os.environ.get("ALIGNER_NOTARIZATION_REQUEST_ID", ""),
|
"notarizationStatus": "passed" if notarized else "not-submitted",
|
"stapleStatus": "not-run",
|
"codesignVerifyStatus": "not-run",
|
"spctlStatus": "not-run",
|
"dmgPath": dmg_path,
|
"dmgSHA256": sha256,
|
"dmgSize": int(size),
|
"qaCommands": [],
|
"qaReports": [],
|
"createdAt": created_at,
|
"createdBy": created_by,
|
}
|
|
public = {
|
"product": "Aligner",
|
"version": version,
|
"build": build,
|
"minimumMacOS": minimum_macos or "14.0",
|
"architecture": "arm64",
|
"downloadURL": download_url,
|
"sha256": sha256,
|
"size": int(size),
|
"notarized": notarized,
|
"releaseNotesURL": release_notes_url,
|
"publishedAt": created_at if download_url else "",
|
}
|
|
pathlib.Path(internal_path).write_text(
|
json.dumps(internal, indent=2, ensure_ascii=False, sort_keys=True) + "\n",
|
encoding="utf-8",
|
)
|
pathlib.Path(public_path).write_text(
|
json.dumps(public, indent=2, ensure_ascii=False, sort_keys=True) + "\n",
|
encoding="utf-8",
|
)
|
PY
|
|
echo "Internal manifest: $INTERNAL_MANIFEST"
|
echo "Public manifest: $PUBLIC_MANIFEST"
|