← back to Homesonspec

apps/mobile/finish-homesonspec-appstore.sh

189 lines

#!/usr/bin/env bash
# finish-homesonspec-appstore.sh — TURNKEY driver for homesonspec App Store onboarding
#
# ONE command, run the MOMENT Steve has an App Store Connect API key:
#     bash apps/mobile/finish-homesonspec-appstore.sh <path-to-.p8> <Key ID> <Issuer ID>
#
# What it does, in order:
#   (a) validates args + copies .p8 into gitignored apps/mobile/.credentials/;
#       routes Key ID / Issuer ID via secrets-manager (last-4 only, NEVER echoes key)
#   (b) wires eas.json submit.production.ios (ascApiKeyPath / ascApiKeyId / ascApiKeyIssuerId)
#       and asserts production.distribution = "store"
#   (c) runs   eas build --platform ios --profile production   (headless; ~$0 free-tier or ~$1-2 credit)
#   (d) PRINTS but does NOT run the   eas submit   command, then STOPS
#
# SAFE + IDEMPOTENT: re-running with the same args re-wires eas.json (no-op) and re-copies the .p8.
# It NEVER submits, NEVER creates Apple credentials interactively, NEVER pushes to a remote.
#
# HARD RAILS: never submits, never pushes to a remote, never echoes the key.
# A production build consumes 1 EAS build slot — the only paid side-effect, explicitly gated.
#
# Modeled on ~/Projects/nineoh-guide/finish-tk12.sh

set -euo pipefail

# ----- locate self / project --------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$SCRIPT_DIR"                        # apps/mobile/
EAS_JSON="$APP_DIR/eas.json"
CRED_DIR="$APP_DIR/.credentials"            # gitignored — never committed
# secrets-manager cli
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 apps/mobile/finish-homesonspec-appstore.sh <path-to-.p8> <Key ID> <Issuer ID>

  <path-to-.p8>  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 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 directory?"
command -v node >/dev/null 2>&1 || die "node not found on PATH"

bold "Homes on Spec — App Store turnkey driver"
echo "  app dir : $APP_DIR"
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 string IDs ----------
bold "[a] Installing .p8 + routing Key ID / Issuer ID via secrets-manager"

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: ensure .credentials/ cannot be committed.
GI="$APP_DIR/.gitignore"
if [[ -f "$GI" ]] && ! grep -qxF ".credentials/" "$GI"; then
  printf '\n# App Store Connect API key material — never commit\n.credentials/\n*.p8\n' >> "$GI"
  echo "  added .credentials/ + *.p8 to .gitignore"
fi

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

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

# Relative path from apps/mobile/eas.json → apps/mobile/.credentials/asc-api-key.p8
ASC_KEY_PATH_REL=".credentials/asc-api-key.p8"

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;

// Clear the sentinel so eas submit auto-detects by bundle ID / creates the ASC record.
if (ios.ascAppId === 'SET-AT-SUBMIT') delete ios.ascAppId;

// Assert / repair 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');
console.log('  build.production.distribution = ' + j.build.production.distribution);
NODE
echo

# ----- PRE-FLIGHT: require eas init to have been run -------------------------
if node -e "
  const j=JSON.parse(require('fs').readFileSync('$EAS_JSON','utf8'));
  const id=j?.extra?.eas?.projectId||j?.expo?.extra?.eas?.projectId||'';
  if(id==='SET-AT-EAS-INIT'||!id){process.exit(1);}
" 2>/dev/null; then
  echo "  EAS project ID confirmed in eas.json / app.json"
else
  bold "[PRE-FLIGHT BLOCKED] EAS project ID is not set."
  cat <<EOF

  You must run   eas init   once (inside apps/mobile/) to register the app with
  Expo's build service. This creates the project ID and requires an EAS login.

  Run:
    cd $APP_DIR
    npx eas-cli init

  Then re-run this script. The build step below cannot proceed without a project ID.
EOF
  exit 3
fi

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

cd "$APP_DIR"
npx eas-cli build --platform ios --profile production --non-interactive

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

# ----- (d) PRINT 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 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 will create the App Store Connect app record for com.abrams.homesonspec
    and print a numeric ascAppId — paste it into $EAS_JSON
    (submit.production.ios.ascAppId) for reuse on future submits.
  • Build appears in TestFlight approximately 5-30 minutes after Apple finishes processing.
  • App Review: ensure screenshots include the Saved and Map native tabs so
    the reviewer can clearly see native value beyond the WebView (Guideline 4.2 defense).
  • icon.png and splash.png in assets/ are PLACEHOLDERS — replace with final
    artwork before the production build or App Review will reject.

Turnkey run complete: key routed, eas.json wired, production build fired, HELD at submit.
EOF