name: Decrypt Release Secrets description: Decrypts age-sealed Burrow release secrets and exports CI paths/env. inputs: root: description: Repository root required: false default: . runs: using: composite steps: - shell: bash run: | set -euo pipefail ROOT_INPUT='${{ inputs.root }}' ROOT="$(cd "$ROOT_INPUT" && pwd)" resolve_nix_bin() { local candidate for candidate in \ "${NIX_BIN:-}" \ "$(command -v nix 2>/dev/null || true)" \ /nix/var/nix/profiles/default/bin/nix \ "${HOME:-}/.nix-profile/bin/nix" \ "${HOME:-}/.local/state/nix/profile/bin/nix" \ /Users/runner/.nix-profile/bin/nix \ /Users/runner/.local/state/nix/profile/bin/nix \ /home/runner/.nix-profile/bin/nix \ /home/runner/.local/state/nix/profile/bin/nix \ ; do if [[ -n "$candidate" && -x "$candidate" ]]; then printf '%s\n' "$candidate" return 0 fi done return 1 } resolve_decrypt_tool() { local candidate for candidate in \ "${AGE_BIN:-}" \ "$(command -v age 2>/dev/null || true)" \ /opt/homebrew/bin/age \ /usr/local/bin/age \ /usr/bin/age \ /run/current-system/sw/bin/age \ /etc/profiles/per-user/runner/bin/age \ "${HOME:-}/.nix-profile/bin/age" \ "${HOME:-}/.local/state/nix/profile/bin/age" \ /Users/runner/.nix-profile/bin/age \ /Users/runner/.local/state/nix/profile/bin/age \ /home/runner/.nix-profile/bin/age \ /home/runner/.local/state/nix/profile/bin/age \ ; do if [[ -n "$candidate" && -x "$candidate" ]]; then printf 'age:%s\n' "$candidate" return 0 fi done for candidate in \ "${AGENIX_BIN:-}" \ "$(command -v agenix 2>/dev/null || true)" \ ; do if [[ -n "$candidate" && -x "$candidate" ]]; then printf 'agenix:%s\n' "$candidate" return 0 fi done local nix_bin nix_bin="$(resolve_nix_bin || true)" if [[ -n "$nix_bin" ]]; then local built built="$("$nix_bin" --extra-experimental-features 'nix-command flakes' build --no-link "$ROOT#age" --print-out-paths 2>/dev/null | tail -n 1 || true)" if [[ -n "$built" && -x "$built/bin/age" ]]; then printf 'age:%s\n' "$built/bin/age" return 0 fi built="$("$nix_bin" --extra-experimental-features 'nix-command flakes' build --no-link "$ROOT#agenix" --print-out-paths 2>/dev/null | tail -n 1 || true)" if [[ -n "$built" && -x "$built/bin/agenix" ]]; then printf 'agenix:%s\n' "$built/bin/agenix" return 0 fi fi echo "::error ::age or agenix is required to decrypt release secrets but neither is available." >&2 exit 1 } decrypt_tool="$(resolve_decrypt_tool)" decrypt_kind="${decrypt_tool%%:*}" decrypt_bin="${decrypt_tool#*:}" KEY="${AGE_IDENTITY_PATH:-}" if [[ -z "$KEY" || ! -s "$KEY" ]]; then echo "::error ::Missing age identity. Run Scripts/resolve-age-identity.sh before this action." >&2 exit 1 fi decrypt_age_file() { local file_path="$1" if [[ "$decrypt_kind" == "agenix" ]]; then local agenix_path="$file_path" if [[ "$file_path" == "$ROOT/"* ]]; then agenix_path="${file_path#"$ROOT"/}" fi (cd "$ROOT" && "$decrypt_bin" --identity "$KEY" --decrypt "$agenix_path") else "$decrypt_bin" --decrypt -i "$KEY" "$file_path" fi } write_env() { local name="$1" local value="$2" echo "${name}=${value}" >> "$GITHUB_ENV" } mask_multiline() { local value="$1" echo "::add-mask::${value}" while IFS= read -r line; do [[ -n "$line" ]] || continue echo "::add-mask::${line}" done <<<"$value" } decrypt_env_optional() { local name="$1" local file="$2" if [[ ! -f "$file" ]]; then echo "::notice ::Skipping optional release secret ${file}" return 0 fi local value value="$(decrypt_age_file "$file")" mask_multiline "$value" { echo "${name}<> "$GITHUB_ENV" } decrypt_file_optional() { local name="$1" local file="$2" local suffix="$3" if [[ ! -f "$file" ]]; then echo "::notice ::Skipping optional release secret ${file}" return 0 fi local temp_dir="${RUNNER_TEMP:-/tmp}" local temp_file temp_file="$(mktemp "${temp_dir}/${name}.XXXXXX")" if [[ -n "$suffix" ]]; then local with_suffix="${temp_file}.${suffix}" mv "$temp_file" "$with_suffix" temp_file="$with_suffix" fi decrypt_age_file "$file" > "$temp_file" chmod 600 "$temp_file" write_env "$name" "$temp_file" } decrypt_dotenv_optional() { local file="$1" if [[ ! -f "$file" ]]; then echo "::notice ::Skipping optional release secret ${file}" return 0 fi local temp_file temp_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-release-dotenv.XXXXXX")" decrypt_age_file "$file" > "$temp_file" # shellcheck disable=SC1090 set -a source "$temp_file" set +a rm -f "$temp_file" if [[ -z "${MINIO_ROOT_USER:-}" || -z "${MINIO_ROOT_PASSWORD:-}" ]]; then echo "::error ::${file} must define MINIO_ROOT_USER and MINIO_ROOT_PASSWORD." >&2 exit 1 fi echo "::add-mask::${MINIO_ROOT_USER}" echo "::add-mask::${MINIO_ROOT_PASSWORD}" { echo "AWS_ACCESS_KEY_ID=${MINIO_ROOT_USER}" echo "AWS_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}" echo "AWS_DEFAULT_REGION=${BLOB_REGION:-hel1}" echo "AWS_REGION=${BLOB_REGION:-hel1}" echo "AWS_S3_ENDPOINT_URL=https://${BLOB_ENDPOINT:-hel1.your-objectstorage.com}" } >> "$GITHUB_ENV" } decode_base64_stream() { if command -v base64 >/dev/null 2>&1; then base64 -d 2>/dev/null || base64 -D elif command -v openssl >/dev/null 2>&1; then openssl base64 -d -A else echo "::error ::base64 or openssl is required to decode release secrets." >&2 return 127 fi } decode_base64_to_file() { local output="$1" decode_base64_stream > "$output" } decrypt_appstore_connect() { local file="$ROOT/secrets/apple/appstore-connect.env.age" if [[ ! -f "$file" ]]; then echo "::notice ::Skipping optional release secret ${file}" return 0 fi local temp_file key_path key_value temp_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-asc-env.XXXXXX")" decrypt_age_file "$file" > "$temp_file" # shellcheck disable=SC1090 set -a source "$temp_file" set +a rm -f "$temp_file" if [[ -z "${ASC_API_KEY_ID:-}" || -z "${ASC_API_ISSUER_ID:-}" ]]; then echo "::error ::${file} must define ASC_API_KEY_ID and ASC_API_ISSUER_ID." >&2 exit 1 fi key_path="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-asc-key.XXXXXX.p8")" if [[ -n "${ASC_API_KEY_BASE64:-}" ]]; then printf '%s' "$ASC_API_KEY_BASE64" | decode_base64_to_file "$key_path" elif [[ -n "${ASC_API_KEY_P8_BASE64:-}" ]]; then printf '%s' "$ASC_API_KEY_P8_BASE64" | decode_base64_to_file "$key_path" elif [[ -n "${APPSTORE_KEY:-}" ]]; then printf '%s\n' "$APPSTORE_KEY" > "$key_path" elif [[ -n "${ASC_API_KEY:-}" ]]; then printf '%s\n' "$ASC_API_KEY" > "$key_path" else echo "::error ::${file} must define ASC_API_KEY_BASE64 or ASC_API_KEY." >&2 exit 1 fi chmod 600 "$key_path" key_value="$(cat "$key_path")" mask_multiline "$key_value" echo "::add-mask::${ASC_API_KEY_ID}" echo "::add-mask::${ASC_API_ISSUER_ID}" write_env ASC_API_KEY_ID "$ASC_API_KEY_ID" write_env ASC_API_ISSUER_ID "$ASC_API_ISSUER_ID" write_env ASC_API_KEY_PATH "$key_path" } decrypt_distribution_signing() { local file="$ROOT/secrets/apple/distribution-signing.env.age" if [[ ! -f "$file" ]]; then echo "::notice ::Skipping optional release secret ${file}" return 0 fi local temp_file cert_path cert_password cert_base64 temp_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-signing-env.XXXXXX")" decrypt_age_file "$file" > "$temp_file" # shellcheck disable=SC1090 set -a source "$temp_file" set +a rm -f "$temp_file" cert_base64="${APPLE_SIGNING_CERTIFICATE_BASE64:-${APPLE_DISTRIBUTION_CERTIFICATE_BASE64:-}}" cert_password="${APPLE_SIGNING_CERTIFICATE_PASSWORD:-${APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD:-}}" if [[ -z "$cert_password" && -n "${APPLE_SIGNING_CERTIFICATE_PASSWORD_BASE64:-}" ]]; then cert_password="$(printf '%s' "$APPLE_SIGNING_CERTIFICATE_PASSWORD_BASE64" | decode_base64_stream)" fi if [[ -z "$cert_base64" || -z "$cert_password" ]]; then echo "::error ::${file} must define APPLE_SIGNING_CERTIFICATE_BASE64 and APPLE_SIGNING_CERTIFICATE_PASSWORD." >&2 exit 1 fi cert_path="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-apple-signing.XXXXXX.p12")" printf '%s' "$cert_base64" | decode_base64_to_file "$cert_path" chmod 600 "$cert_path" echo "::add-mask::${cert_password}" write_env APPLE_SIGNING_CERTIFICATE_PATH "$cert_path" write_env APPLE_SIGNING_CERTIFICATE_PASSWORD "$cert_password" write_env APPLE_KEYCHAIN_PASSWORD "$cert_password" } decrypt_sparkle() { local file="$ROOT/secrets/apple/sparkle.env.age" if [[ ! -f "$file" ]]; then echo "::notice ::Skipping optional release secret ${file}" return 0 fi local temp_file key_path key_base64 key_value temp_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-sparkle-env.XXXXXX")" decrypt_age_file "$file" > "$temp_file" # shellcheck disable=SC1090 set -a source "$temp_file" set +a rm -f "$temp_file" key_base64="${SPARKLE_EDDSA_KEY_BASE64:-${SPARKLE_PRIVATE_KEY_BASE64:-}}" key_path="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-sparkle-eddsa.XXXXXX.key")" if [[ -n "$key_base64" ]]; then printf '%s' "$key_base64" | decode_base64_to_file "$key_path" elif [[ -n "${SPARKLE_EDDSA_KEY:-}" ]]; then printf '%s\n' "$SPARKLE_EDDSA_KEY" > "$key_path" elif [[ -n "${SPARKLE_PRIVATE_KEY:-}" ]]; then printf '%s\n' "$SPARKLE_PRIVATE_KEY" > "$key_path" else echo "::error ::${file} must define SPARKLE_EDDSA_KEY_BASE64 or SPARKLE_EDDSA_KEY." >&2 exit 1 fi chmod 600 "$key_path" key_value="$(cat "$key_path")" mask_multiline "$key_value" write_env SPARKLE_EDDSA_KEY_PATH "$key_path" } decrypt_appstore_connect decrypt_distribution_signing decrypt_sparkle decrypt_dotenv_optional "$ROOT/secrets/hetzner/object-storage.age"