Files
pleno-vue/scripts/mobile/create-ios-release-manifest.mjs
T
Jeppe B 88eda43560 Automate signed iOS App Store releases (#192)
## 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
2026-07-20 17:59:43 +02:00

37 lines
1.4 KiB
JavaScript

import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { env, exit } from "node:process";
const required = (name) => {
if (!env[name]) throw new Error(`Missing ${name}`);
return env[name];
};
try {
const ipaPath = required("IOS_IPA_PATH");
const output = env.IOS_RELEASE_MANIFEST_PATH || "output/ios-release/ios-release-manifest.json";
const manifest = {
schemaVersion: 1,
repository: required("GITHUB_REPOSITORY"),
sourceSha: required("IOS_SOURCE_SHA").toLowerCase(),
marketingVersion: required("IOS_MARKETING_VERSION"),
buildNumber: required("IOS_BUILD_NUMBER"),
appStoreBuildId: required("APP_STORE_BUILD_ID"),
appStoreConnectAppId: required("APP_STORE_CONNECT_APP_ID"),
bundleId: required("IOS_BUNDLE_ID"),
xcodeVersion: required("XCODE_VERSION"),
sdkVersion: required("IOS_SDK_VERSION"),
workflowRunId: required("GITHUB_RUN_ID"),
workflowRunAttempt: required("GITHUB_RUN_ATTEMPT"),
ipaSha256: createHash("sha256").update(readFileSync(ipaPath)).digest("hex"),
createdAt: new Date().toISOString(),
};
mkdirSync(dirname(output), { recursive: true });
writeFileSync(output, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
console.log(`Created ${output}.`);
} catch (error) {
console.error(error instanceof Error ? error.message : error);
exit(1);
}