← back to Nineoh Guide

finish-tk12.sh

173 lines

#!/usr/bin/env bash
# finish-tk12.sh — TURNKEY driver for TK-12: nineoh "Unofficial 90210 Guide" → App Store / TestFlight
#
# ONE command, run the MOMENT Steve has an App Store Connect API key:
#     bash finish-tk12.sh <path-to-.p8> <Key ID> <Issuer ID>
#
# What it does, in order:
#   (a) routes the Key ID / Issuer ID via the secrets-manager (last-4 only, NEVER echoes the key),
#       and installs the .p8 into a GITIGNORED local credentials dir EAS can read;
#   (b) wires eas.json submit.production.ios (ascApiKeyPath / ascApiKeyId / ascApiKeyIssuerId)
#       + confirms the production build profile is distribution:store;
#   (c) runs   eas build --platform ios --profile production   (headless, ASC key = no prompts);
#   (d) then PRINTS (does NOT run) the exact  eas submit  command and STOPS,
#       so Steve gives the final TestFlight go himself.
#
# SAFE + IDEMPOTENT: re-running with the same args re-wires eas.json to the same values
# (a no-op) and re-copies the .p8. It never submits, never creates Apple credentials
# interactively, never pushes to a remote. A production build consumes 1 EAS build slot
# ($0 on the free tier / ~$1-2 of credit) — that is the only paid side effect, and it is
# the explicitly-approved step B.
#
# HARD RAILS honored: no App Store submit fired; the ASC key value is never printed or
# committed; the .p8 lives only in a gitignored dir; no remote push.

set -euo pipefail

# ----- locate self / project -------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$SCRIPT_DIR"                 # ~/Projects/nineoh-guide
APP_DIR="$PROJECT_ROOT/apps/mobile"
EAS_JSON="$APP_DIR/eas.json"
CRED_DIR="$PROJECT_ROOT/.credentials"      # gitignored — never committed
SECRETS_CLI="$HOME/Projects/secrets-manager/cli.js"

bold() { printf '\033[1m%s\033[0m\n' "$*"; }
die()  { printf '\033[31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
last4() { local s="$1"; printf '…%s' "${s: -4}"; }

# ----- arg validation --------------------------------------------------------
if [[ $# -ne 3 ]]; then
  cat >&2 <<EOF
usage: bash finish-tk12.sh <path-to-.p8> <Key ID> <Issuer ID>

  <path-to-.p8>  the App Store Connect API key file you downloaded ONCE
                 (e.g. ~/Downloads/AuthKey_ABC123XYZ.p8)
  <Key ID>       the Key ID shown next to the key in App Store Connect
  <Issuer ID>    the Issuer ID at the top of the Integrations → App Store Connect API page
EOF
  exit 2
fi

P8_SRC="$1"; KEY_ID="$2"; ISSUER_ID="$3"

[[ -f "$P8_SRC" ]] || die "no .p8 file at: $P8_SRC"
[[ "$P8_SRC" == *.p8 ]] || die "expected a .p8 file, got: $P8_SRC"
[[ -n "$KEY_ID" ]]    || die "empty Key ID"
[[ -n "$ISSUER_ID" ]] || die "empty Issuer ID"
[[ -f "$EAS_JSON" ]]  || die "eas.json not found at $EAS_JSON — wrong project?"
command -v node >/dev/null 2>&1 || die "node not found on PATH"

bold "TK-12 turnkey driver — nineoh Unofficial 90210 Guide → App Store/TestFlight"
echo "  project : $PROJECT_ROOT"
echo "  ASC key : Key ID $(last4 "$KEY_ID")  ·  Issuer ID $(last4 "$ISSUER_ID")  ·  .p8 $(basename "$P8_SRC")"
echo "  (key values are never printed in full or committed — last-4 only)"
echo

# ----- (a) install .p8 into gitignored creds dir + route the string IDs -------
bold "[a] Installing .p8 + routing Key ID / Issuer ID"

mkdir -p "$CRED_DIR"
chmod 700 "$CRED_DIR"
P8_DEST="$CRED_DIR/asc-api-key.p8"
cp "$P8_SRC" "$P8_DEST"
chmod 600 "$P8_DEST"
echo "  .p8 -> $P8_DEST (gitignored, chmod 600)"

# Belt-and-suspenders: make sure the creds dir can never be committed.
GI_ROOT="$PROJECT_ROOT/.gitignore"
if [[ -f "$GI_ROOT" ]] && ! grep -qxF ".credentials/" "$GI_ROOT"; then
  printf '\n# TK-12: App Store Connect API key material — never commit\n.credentials/\n*.p8\n' >> "$GI_ROOT"
  echo "  added .credentials/ + *.p8 to $GI_ROOT"
fi

# Route the two string IDs via secrets-manager (idempotent add). NEVER routes the .p8 body.
if [[ -f "$SECRETS_CLI" ]]; then
  node "$SECRETS_CLI" add NINEOH_ASC_API_KEY_ID    "$KEY_ID"    >/dev/null 2>&1 \
    && echo "  routed NINEOH_ASC_API_KEY_ID    ($(last4 "$KEY_ID"))    via secrets-manager" \
    || echo "  (secrets add for Key ID returned non-zero — continuing; eas.json is the source of truth)"
  node "$SECRETS_CLI" add NINEOH_ASC_API_ISSUER_ID "$ISSUER_ID" >/dev/null 2>&1 \
    && echo "  routed NINEOH_ASC_API_ISSUER_ID ($(last4 "$ISSUER_ID")) via secrets-manager" \
    || echo "  (secrets add for Issuer ID returned non-zero — continuing; eas.json is the source of truth)"
else
  echo "  (secrets-manager cli not found at $SECRETS_CLI — skipping key routing; eas.json still wired below)"
fi
echo

# ----- (b) wire eas.json + confirm build profile -----------------------------
bold "[b] Wiring eas.json submit.production.ios + confirming production build profile"

# Use a relative ascApiKeyPath so eas.json stays portable and carries no absolute home path.
# eas.json lives in apps/mobile; the creds dir is at project root → ../../.credentials/...
ASC_KEY_PATH_REL="../../.credentials/asc-api-key.p8"

# Rewrite eas.json deterministically in Node (no jq dependency). Idempotent — sets the three
# ASC fields, drops the SET-AT-SUBMIT ascAppId sentinel so eas auto-detects/creates the record,
# and asserts production.distribution === "store".
KEY_ID="$KEY_ID" ISSUER_ID="$ISSUER_ID" ASC_KEY_PATH_REL="$ASC_KEY_PATH_REL" EAS_JSON="$EAS_JSON" \
node <<'NODE'
const fs = require('fs');
const p = process.env.EAS_JSON;
const j = JSON.parse(fs.readFileSync(p, 'utf8'));

j.submit = j.submit || {};
j.submit.production = j.submit.production || {};
const ios = j.submit.production.ios = j.submit.production.ios || {};

ios.ascApiKeyPath     = process.env.ASC_KEY_PATH_REL;
ios.ascApiKeyId       = process.env.KEY_ID;
ios.ascApiKeyIssuerId = process.env.ISSUER_ID;

// Drop the non-numeric sentinel so `eas submit` auto-detects by bundle id / creates the ASC
// app record on first run. (Leaving "SET-AT-SUBMIT" in place makes eas submit error confusingly.)
if (ios.ascAppId === 'SET-AT-SUBMIT') delete ios.ascAppId;

// Assert / repair the production BUILD profile is a store build.
j.build = j.build || {};
j.build.production = j.build.production || {};
if (j.build.production.distribution !== 'store') {
  j.build.production.distribution = 'store';
  console.log('  (repaired build.production.distribution -> "store")');
}

fs.writeFileSync(p, JSON.stringify(j, null, 2) + '\n');
console.log('  eas.json wired: ascApiKeyPath / ascApiKeyId / ascApiKeyIssuerId set; ascAppId sentinel cleared');
console.log('  build.production.distribution = ' + j.build.production.distribution);
NODE
echo

# ----- (c) production build ---------------------------------------------------
bold "[c] Running production build (1 EAS slot, \$0 free-tier / ~\$1-2 credit)"
echo "  cd $APP_DIR && npx eas-cli build --platform ios --profile production --non-interactive"
echo

cd "$APP_DIR"
# --non-interactive is safe now: the ASC API key removes every Apple prompt for the store build.
npx eas-cli build --platform ios --profile production --non-interactive

echo
bold "[c] Production build finished (see the build URL above)."
echo

# ----- (d) PRINT the submit command and STOP ---------------------------------
bold "[d] ⛔ HOLDING at TestFlight submit — this is Steve's final go-live call."
cat <<EOF

The store build is done and signed. The App Store submit is GATED (go-live) and is
NOT run by this script. To send the build to TestFlight, run this yourself:

    cd $APP_DIR
    npx eas-cli submit --platform ios --profile production --latest

Notes:
  • First submit auto-creates the App Store Connect app record and prints a numeric
    ascAppId — paste it back into $EAS_JSON (submit.production.ios.ascAppId) for reuse.
  • Build appears in TestFlight ~5-30 min after Apple finishes processing.
  • ⚠️  App Review risk (Guideline 4.1 Copycats / 5.2 IP): "Unofficial 90210 Guide"
     references a trademarked property. The app already ships the UNOFFICIAL badge +
     disclaimers, but Apple can still reject at its discretion — treat that as a real
     possibility, not an edge case.

TK-12 turnkey run complete: key routed, eas.json wired, production build fired, held at submit.
EOF