## What changed - adds production iOS identity, localized storefront metadata, native privacy declarations, App Store-safe artwork, and account-deletion UX - mirrors the live Danish Google Play title, short description, and long description in the App Store metadata source - generates Android launcher/store icons from the opaque iOS marketing master so both platforms use the same white background - adds guarded GitHub Actions workflows for storefront readiness, credential health, signed TestFlight uploads, and App Store candidate preparation - adds pinned Fastlane configuration with a committed dependency lock, release manifest tooling, and an operational App Store runbook - preserves the upstream iOS safe-area implementation while retaining opaque App Store icon assets ## Why The repository previously supported development-signed device bundles but had no production App Store identity, reproducible storefront source of truth, or protected signed-release pipeline. Apple also requires in-app account deletion for apps that support account creation. The Android icon master was transparent, which rendered as black on dark store/device surfaces. ## Impact Automation remains fail-closed behind `APP_STORE_AUTOMATION_ENABLED=false`. No build can upload to TestFlight or change App Store metadata until the switch is deliberately enabled after merge and the remaining release gates are satisfied. ## Validation - focused App Store, iOS icon, and cross-platform icon-background tests pass - every generated Android store/launcher icon is opaque with pure-white corners; iOS marketing artwork is checked the same way - Android icon drift check passes for all 19 generated files - production Vite build and the broader focused release checks completed successfully - storefront metadata is valid; only the two expected screenshot-set warnings remain - App Store Readiness is green at head `4445fecc` - Apple Distribution certificate and App Store profile were independently verified for `HP3FJ4GVL7.io.truckwash.app` - live App Store Connect API authentication succeeded for app `6792777794` - App Store record, free Denmark-only availability, and automatic `Internal QA` TestFlight group are configured - EU trader status, Content Rights, 4+ age rating, and the published App Privacy label are completed in App Store Connect - iPhone and iPad accessibility declarations are configured honestly as pre-release drafts ## Remaining external gates - reviewed iPhone and iPad screenshot sets are still required - an App Review login must be supplied without creating or exposing customer credentials - the first signed TestFlight candidate must run after merge and deliberate automation enablement
118 lines
5.9 KiB
YAML
118 lines
5.9 KiB
YAML
name: iOS Credential Health
|
|
|
|
on:
|
|
schedule:
|
|
- cron: "17 6 * * 1"
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: ios-credential-health
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
gate:
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
enabled: ${{ steps.gate.outputs.enabled }}
|
|
steps:
|
|
- id: gate
|
|
env:
|
|
ENABLED: ${{ vars.APP_STORE_AUTOMATION_ENABLED || 'false' }}
|
|
run: |
|
|
enabled=false
|
|
[[ "$ENABLED" == true ]] && enabled=true
|
|
echo "enabled=$enabled" >> "$GITHUB_OUTPUT"
|
|
if [[ "$enabled" != true ]]; then
|
|
echo "App Store automation is disabled; credential health did not access its environment." >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
|
|
validate:
|
|
needs: gate
|
|
if: needs.gate.outputs.enabled == 'true'
|
|
runs-on: macos-15
|
|
timeout-minutes: 15
|
|
environment: app-store-signing
|
|
env:
|
|
IOS_BUNDLE_ID: ${{ vars.IOS_BUNDLE_ID || 'io.truckwash.app' }}
|
|
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
|
APP_STORE_CONNECT_API_KEY_ID: ${{ vars.APP_STORE_CONNECT_API_KEY_ID }}
|
|
APP_STORE_CONNECT_ISSUER_ID: ${{ vars.APP_STORE_CONNECT_ISSUER_ID || '' }}
|
|
APP_STORE_CONNECT_APP_ID: ${{ vars.APP_STORE_CONNECT_APP_ID }}
|
|
APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_PRIVATE_KEY_BASE64 }}
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
|
with:
|
|
node-version: 22
|
|
|
|
- name: Validate API key and app access
|
|
run: node scripts/mobile/app-store-connect.mjs verify-credentials
|
|
|
|
- name: Validate certificate and profile identity and expiry
|
|
shell: bash
|
|
env:
|
|
IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64 }}
|
|
IOS_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.IOS_DISTRIBUTION_CERTIFICATE_PASSWORD }}
|
|
IOS_APP_STORE_PROFILE_BASE64: ${{ secrets.IOS_APP_STORE_PROFILE_BASE64 }}
|
|
run: |
|
|
set -euo pipefail
|
|
cert_p12="$RUNNER_TEMP/distribution.p12"
|
|
cert_pem="$RUNNER_TEMP/distribution.pem"
|
|
cert_der="$RUNNER_TEMP/distribution.der"
|
|
profile="$RUNNER_TEMP/distribution.mobileprovision"
|
|
profile_plist="$RUNNER_TEMP/distribution-profile.plist"
|
|
keychain="$RUNNER_TEMP/credential-health.keychain-db"
|
|
keychain_password="$(openssl rand -hex 24)"
|
|
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_DISTRIBUTION_CERTIFICATE_P12_BASE64.replace(/\\s/g,''),'base64'))" "$cert_p12"
|
|
node -e "const fs=require('fs');fs.writeFileSync(process.argv[1],Buffer.from(process.env.IOS_APP_STORE_PROFILE_BASE64.replace(/\\s/g,''),'base64'))" "$profile"
|
|
chmod 600 "$cert_p12" "$profile"
|
|
security create-keychain -p "$keychain_password" "$keychain"
|
|
security unlock-keychain -p "$keychain_password" "$keychain"
|
|
security import "$cert_p12" -P "$IOS_DISTRIBUTION_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain"
|
|
security list-keychains -d user -s "$keychain"
|
|
security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" >/dev/null
|
|
security find-identity -v -p codesigning "$keychain" | grep -q 'Apple Distribution' || {
|
|
echo "Distribution P12 does not contain a usable private signing identity." >&2
|
|
exit 1
|
|
}
|
|
openssl pkcs12 -in "$cert_p12" -clcerts -nokeys -passin env:IOS_DISTRIBUTION_CERTIFICATE_PASSWORD -out "$cert_pem"
|
|
openssl x509 -in "$cert_pem" -noout -subject -issuer -dates
|
|
openssl x509 -in "$cert_pem" -checkend 2592000 -noout || { echo "Distribution certificate expires within 30 days." >&2; exit 1; }
|
|
openssl x509 -in "$cert_pem" -outform DER -out "$cert_der"
|
|
security cms -D -i "$profile" > "$profile_plist"
|
|
CERT_DER="$cert_der" PROFILE_PLIST="$profile_plist" python3 <<'PY'
|
|
import datetime, hashlib, os, plistlib, sys
|
|
with open(os.environ["PROFILE_PLIST"], "rb") as handle: profile = plistlib.load(handle)
|
|
with open(os.environ["CERT_DER"], "rb") as handle: cert_sha = hashlib.sha1(handle.read()).hexdigest().upper()
|
|
expiration = profile.get("ExpirationDate")
|
|
if expiration and expiration.tzinfo is None: expiration = expiration.replace(tzinfo=datetime.timezone.utc)
|
|
warning = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=30)
|
|
ent = profile.get("Entitlements", {})
|
|
checks = {
|
|
"team": os.environ["APPLE_TEAM_ID"] in profile.get("TeamIdentifier", []),
|
|
"bundle": ent.get("application-identifier") == f'{os.environ["APPLE_TEAM_ID"]}.{os.environ["IOS_BUNDLE_ID"]}',
|
|
"distribution": ent.get("get-task-allow") is False and not profile.get("ProvisionedDevices"),
|
|
"profile expiry beyond 30 days": expiration is not None and expiration > warning,
|
|
"certificate belongs to profile": cert_sha in {hashlib.sha1(value).hexdigest().upper() for value in profile.get("DeveloperCertificates", [])},
|
|
}
|
|
failed = [name for name, ok in checks.items() if not ok]
|
|
if failed:
|
|
print("Credential health failed:", *[f"- {name}" for name in failed], sep="\n", file=sys.stderr)
|
|
sys.exit(1)
|
|
print(f"Provisioning profile is healthy through {expiration.isoformat()}.")
|
|
PY
|
|
|
|
- name: Clean temporary credential files
|
|
if: always()
|
|
run: |
|
|
security delete-keychain "$RUNNER_TEMP/credential-health.keychain-db" 2>/dev/null || true
|
|
rm -f "$RUNNER_TEMP"/distribution.{p12,pem,der,mobileprovision} "$RUNNER_TEMP/distribution-profile.plist"
|