#!/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 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"