#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" usage() { cat <<'EOF' Usage: Scripts/seal-release-secrets.sh [--source-dir ] [--only ] Encrypt Burrow release inputs into the agenix secrets consumed by CI. Expected files in --source-dir (default: intake/release): appstore-key-id.txt appstore-key-issuer-id.txt appstore-key.p8 distribution-cert.p12 distribution-cert-password.txt sparkle-eddsa.key Pre-composed appstore-connect.env, distribution-signing.env, and sparkle.env files are also accepted. Provisioning profiles are synced from App Store Connect credentials in CI. The temporary keychain password is always the distribution certificate password. Secrets: all appstore-connect distribution-signing sparkle EOF } SOURCE_DIR="${BURROW_RELEASE_SECRET_SOURCE_DIR:-${REPO_ROOT}/intake/release}" ONLY="all" while [[ $# -gt 0 ]]; do case "$1" in --source-dir) SOURCE_DIR="${2:?missing value for --source-dir}" shift 2 ;; --only) ONLY="${2:?missing value for --only}" shift 2 ;; -h|--help) usage exit 0 ;; *) echo "unknown option: $1" >&2 usage >&2 exit 64 ;; esac done case "$ONLY" in all|appstore-connect|app-store|appstore|distribution-signing|apple-signing|sparkle) ;; *) echo "unknown release secret selector: $ONLY" >&2 usage >&2 exit 64 ;; esac require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "missing required command: $1" >&2 exit 1 fi } require_cmd age require_cmd nix require_cmd python3 if [[ ! -d "$SOURCE_DIR" ]]; then echo "release secret source directory does not exist: $SOURCE_DIR" >&2 exit 1 fi tmpdir="$(mktemp -d)" cleanup() { rm -rf "$tmpdir" } trap cleanup EXIT seal_secret() { local target="$1" local source_path="$2" local required="${3:-required}" local recipients_file="${tmpdir}/$(basename "${target}").recipients" if [[ ! -s "$source_path" ]]; then if [[ "$required" == "optional" ]]; then return 0 fi echo "required release secret source missing or empty: $source_path" >&2 exit 1 fi mkdir -p "${REPO_ROOT}/$(dirname "$target")" nix eval --impure --json --expr "let s = import ${REPO_ROOT}/secrets.nix; in s.\"${target}\".publicKeys" \ | python3 -c 'import json, sys; [print(item) for item in json.load(sys.stdin)]' \ > "$recipients_file" age -R "$recipients_file" -o "${REPO_ROOT}/${target}" "$source_path" chmod 600 "${REPO_ROOT}/${target}" echo "Sealed ${target}" } base64_file() { python3 -c 'import base64, pathlib, sys; print(base64.b64encode(pathlib.Path(sys.argv[1]).read_bytes()).decode())' "$1" } read_secret_text() { python3 -c 'import pathlib, sys; sys.stdout.write(pathlib.Path(sys.argv[1]).read_text().strip())' "$1" } base64_text() { python3 -c 'import base64, sys; print(base64.b64encode(sys.stdin.buffer.read()).decode())' } reject_placeholder() { local path="$1" if grep -Eq '^(TODO|PASTE_|REPLACE_|#)' "$path"; then echo "intake file still contains placeholder text: $path" >&2 exit 1 fi } reject_placeholder_text() { local path="$1" if grep -Iq . "$path"; then reject_placeholder "$path" fi } first_existing() { local path for path in "$@"; do if [[ -s "$path" ]]; then printf '%s\n' "$path" return 0 fi done return 1 } compose_appstore_connect_env() { local direct="${SOURCE_DIR}/appstore-connect.env" if [[ -s "$direct" ]]; then printf '%s\n' "$direct" return 0 fi local key_id_file="${SOURCE_DIR}/appstore-key-id.txt" local issuer_id_file="${SOURCE_DIR}/appstore-key-issuer-id.txt" local key_file="${SOURCE_DIR}/appstore-key.p8" if [[ ! -s "$key_file" ]]; then echo "required App Store Connect source missing or empty: appstore-key.p8" >&2 exit 1 fi for path in "$key_id_file" "$issuer_id_file" "$key_file"; do if [[ ! -s "$path" ]]; then echo "required App Store Connect source missing or empty: $path" >&2 exit 1 fi reject_placeholder_text "$path" done local out="${tmpdir}/appstore-connect.env" { printf 'ASC_API_KEY_ID=%s\n' "$(read_secret_text "$key_id_file")" printf 'ASC_API_ISSUER_ID=%s\n' "$(read_secret_text "$issuer_id_file")" printf 'ASC_API_KEY_BASE64=%s\n' "$(base64_file "$key_file")" } > "$out" printf '%s\n' "$out" } compose_distribution_signing_env() { local direct="${SOURCE_DIR}/distribution-signing.env" if [[ -s "$direct" ]]; then printf '%s\n' "$direct" return 0 fi local cert_file cert_file="${SOURCE_DIR}/distribution-cert.p12" local password_file="${SOURCE_DIR}/distribution-cert-password.txt" if [[ ! -s "$cert_file" ]]; then echo "required distribution signing source missing or empty: distribution-cert.p12" >&2 exit 1 fi for path in "$password_file"; do if [[ ! -s "$path" ]]; then echo "required distribution signing source missing or empty: $path" >&2 exit 1 fi reject_placeholder_text "$path" done local out="${tmpdir}/distribution-signing.env" { printf 'APPLE_SIGNING_CERTIFICATE_BASE64=%s\n' "$(base64_file "$cert_file")" printf 'APPLE_SIGNING_CERTIFICATE_PASSWORD_BASE64=%s\n' "$(read_secret_text "$password_file" | base64_text)" } > "$out" printf '%s\n' "$out" } compose_sparkle_env() { local direct="${SOURCE_DIR}/sparkle.env" if [[ -s "$direct" ]]; then printf '%s\n' "$direct" return 0 fi local key_file key_file="${SOURCE_DIR}/sparkle-eddsa.key" if [[ ! -s "$key_file" ]]; then return 1 fi reject_placeholder_text "$key_file" local out="${tmpdir}/sparkle.env" printf 'SPARKLE_EDDSA_KEY_BASE64=%s\n' "$(base64_file "$key_file")" > "$out" printf '%s\n' "$out" } case "$ONLY" in all) appstore_env="$(compose_appstore_connect_env)" seal_secret "secrets/apple/appstore-connect.env.age" "$appstore_env" distribution_env="$(compose_distribution_signing_env)" seal_secret "secrets/apple/distribution-signing.env.age" "$distribution_env" if sparkle_env="$(compose_sparkle_env)"; then seal_secret "secrets/apple/sparkle.env.age" "$sparkle_env" fi ;; appstore-connect|app-store|appstore) appstore_env="$(compose_appstore_connect_env)" seal_secret "secrets/apple/appstore-connect.env.age" "$appstore_env" ;; distribution-signing|apple-signing) distribution_env="$(compose_distribution_signing_env)" seal_secret "secrets/apple/distribution-signing.env.age" "$distribution_env" ;; sparkle) if sparkle_env="$(compose_sparkle_env)"; then seal_secret "secrets/apple/sparkle.env.age" "$sparkle_env" else echo "required Sparkle source missing or empty: sparkle-eddsa.key" >&2 exit 1 fi ;; esac echo "Release secrets are sealed. Commit the .age files and deploy/use CI with an authorized age identity."