← back to Nineoh Guide
TK-12: turnkey finish-tk12.sh — one command post-ASC-key drives build, holds at TestFlight submit
3b23bfc8669777c415fd6aea9d004f2c46f5dc85 · 2026-07-28 07:00:05 -0700 · Steve Abrams
Driver takes 3 args (.p8, Key ID, Issuer ID) and: routes key via secrets-manager
(last-4 only, never echoed), wires eas.json submit.production.ios with
ascApiKeyPath/ascApiKeyId/ascApiKeyIssuerId + clears SET-AT-SUBMIT sentinel +
asserts build.production.distribution=store, runs eas build -p ios --profile
production --non-interactive, then PRINTS (not runs) the eas submit command and
STOPS for Steve's final TestFlight go. Idempotent, bash -n clean, .p8 gitignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M .gitignoreA finish-tk12.sh
Diff
commit 3b23bfc8669777c415fd6aea9d004f2c46f5dc85
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 28 07:00:05 2026 -0700
TK-12: turnkey finish-tk12.sh — one command post-ASC-key drives build, holds at TestFlight submit
Driver takes 3 args (.p8, Key ID, Issuer ID) and: routes key via secrets-manager
(last-4 only, never echoed), wires eas.json submit.production.ios with
ascApiKeyPath/ascApiKeyId/ascApiKeyIssuerId + clears SET-AT-SUBMIT sentinel +
asserts build.production.distribution=store, runs eas build -p ios --profile
production --non-interactive, then PRINTS (not runs) the eas submit command and
STOPS for Steve's final TestFlight go. Idempotent, bash -n clean, .p8 gitignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
.gitignore | 3 +
finish-tk12.sh | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 175 insertions(+)
diff --git a/.gitignore b/.gitignore
index 955515d..3227687 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,6 @@ android/
.vercel
*.tsbuildinfo
cta/
+# TK-12: App Store Connect API key material — never commit
+.credentials/
+*.p8
diff --git a/finish-tk12.sh b/finish-tk12.sh
new file mode 100755
index 0000000..0a4492e
--- /dev/null
+++ b/finish-tk12.sh
@@ -0,0 +1,172 @@
+#!/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
← c0e6ddb chore(mobile): TK-12 App Store/TestFlight prep — prefill eas
·
back to Nineoh Guide
·
(newest)