// el-publish — App Store + Play Store publishing pipeline. // // Two providers (Apple, Google), one publish flow: // 1. Validate config + certs // 2. Build artifact (delegated to el-ui-compiler / xcodebuild / gradle) // 3. Capture metadata + screenshots // 4. Upload to App Store Connect / Play Developer API // 5. Monitor rollout // ── Errors ─────────────────────────────────────────────────────────────────── let PUB_ERR_CONFIG: String = "publish.config" let PUB_ERR_BUILD: String = "publish.build" let PUB_ERR_UPLOAD: String = "publish.upload" let PUB_ERR_CERT: String = "publish.certificate" let PUB_ERR_METADATA: String = "publish.metadata" let PUB_ERR_API: String = "publish.api" let PUB_ERR_IO: String = "publish.io" // ── Tracks ─────────────────────────────────────────────────────────────────── let TRACK_PRODUCTION: String = "production" let TRACK_BETA: String = "beta" // TestFlight on Apple; "beta" on Google let TRACK_INTERNAL: String = "internal" let TRACK_ALPHA: String = "alpha" // ── Apple config ───────────────────────────────────────────────────────────── type AppleConfig { account: String // Apple ID bundle_id: String // ai.neurontechnologies.myapp team_id: String api_key_id: String // App Store Connect API key api_issuer: String } // ── Google config ──────────────────────────────────────────────────────────── type GoogleConfig { package: String // ai.neurontechnologies.myapp track: String // production | beta | internal | alpha service_account_json_ref: String // secret ref to service-account JSON } // ── Rollout (staged) ───────────────────────────────────────────────────────── type RolloutConfig { initial_percent: Int // 0..100 target_percent: Int bake_hours: Int // hours between rollout stages } fn rollout_default() -> RolloutConfig { { "initial_percent": 5, "target_percent": 100, "bake_hours": 24 } } // ── Top-level publish config ──────────────────────────────────────────────── type PublishConfig { version: String build_number: Int apple_json: String // empty if not configured google_json: String rollout_json: String // RolloutConfig as JSON } fn publish_config_new(version: String, build_number: Int) -> PublishConfig { { "version": version, "build_number": build_number, "apple_json": "", "google_json": "", "rollout_json": json_encode(rollout_default()) } } // ── Outcome ────────────────────────────────────────────────────────────────── type PublishOutcome { platform: String version: String build_number: Int track: String rollout_percent: Int submission_id: String } fn outcome_new(platform: String, cfg: PublishConfig, track: String) -> PublishOutcome { let pct: Int = 100 if !str_eq(cfg.rollout_json, "") { let pct = str_to_int(json_get(cfg.rollout_json, "initial_percent")) } { "platform": platform, "version": cfg.version, "build_number": cfg.build_number, "track": track, "rollout_percent": pct, "submission_id": "" } } // ── Cert store ────────────────────────────────────────────────────────────── // // Tracks .p12 / .mobileprovision (Apple) and signing keystores (Google). type CertInfo { kind: String // "apple_p12" | "apple_provisioning" | "google_keystore" path: String expires_at: String // ISO 8601 fingerprint: String } fn cert_is_valid(c: CertInfo) -> Bool { !time_after(time_now_iso(), c.expires_at) } fn cert_store_load(dir: String) -> String { // Returns JSON array of CertInfo loaded from `dir`. let files: String = fs_list(dir) let out: String = "[]" let n: Int = json_array_len(files) let i: Int = 0 while i < n { let path: String = json_array_get(files, i) let info: CertInfo = cert_inspect(path) let out = json_array_push(out, json_encode(info)) let i = i + 1 } out } fn cert_inspect(path: String) -> CertInfo { let kind: String = "apple_p12" if str_ends_with(path, ".mobileprovision") { let kind = "apple_provisioning" } if str_ends_with(path, ".jks") { let kind = "google_keystore" } if str_ends_with(path, ".keystore") { let kind = "google_keystore" } let exp: String = exec_capture("openssl", "x509 -enddate -noout -in " + path) { "kind": kind, "path": path, "expires_at": exp, "fingerprint": "" } } // ── Metadata ──────────────────────────────────────────────────────────────── type StoreMetadata { title: String subtitle: String description: String keywords: String // comma-separated privacy_url: String support_url: String locale: String // e.g. "en-US" } fn metadata_load(path: String, locale: String) -> StoreMetadata { let raw: String = fs_read(path + "/" + locale + ".toml") let json: String = toml_to_json(raw) { "title": json_get(json, "title"), "subtitle": json_get(json, "subtitle"), "description": json_get(json, "description"), "keywords": json_get(json, "keywords"), "privacy_url": json_get(json, "privacy_url"), "support_url": json_get(json, "support_url"), "locale": locale } } // ── Screenshots ───────────────────────────────────────────────────────────── type ScreenshotTarget { platform: String // "apple" | "google" device_class: String // "iphone-6.7" | "ipad-12.9" | "phone" | "tablet" | "tv" locale: String expected_resolution: String // "1290x2796" } fn target_iphone_6_7(locale: String) -> ScreenshotTarget { { "platform": "apple", "device_class": "iphone-6.7", "locale": locale, "expected_resolution": "1290x2796" } } fn target_phone_google(locale: String) -> ScreenshotTarget { { "platform": "google", "device_class": "phone", "locale": locale, "expected_resolution": "1080x1920" } } fn screenshot_capture(target: ScreenshotTarget, simulator_id: String, output_dir: String) -> String { // Drives simulator/emulator to capture at expected resolution. let path: String = output_dir + "/" + target.platform + "_" + target.device_class + "_" + target.locale + ".png" exec_capture("xcrun", "simctl io " + simulator_id + " screenshot " + path) path } // ── Apple publisher ───────────────────────────────────────────────────────── fn apple_publish(cfg: PublishConfig, apple: AppleConfig, ipa_path: String, track: String) -> PublishOutcome { let api_key: String = secret_lookup("apple.api_key") let resp: String = exec_capture("xcrun", "altool --upload-app -f " + ipa_path + " --type ios --apiKey " + apple.api_key_id + " --apiIssuer " + apple.api_issuer) let outcome: PublishOutcome = outcome_new("apple", cfg, track) let submission: String = json_get(resp, "submission_id") { "platform": outcome.platform, "version": outcome.version, "build_number": outcome.build_number, "track": outcome.track, "rollout_percent": outcome.rollout_percent, "submission_id": submission } } // ── Google publisher ──────────────────────────────────────────────────────── fn google_publish(cfg: PublishConfig, google: GoogleConfig, aab_path: String) -> PublishOutcome { let sa_json: String = secret_lookup(google.service_account_json_ref) let token: String = google_oauth_token(sa_json) let edit: String = google_play_edit_create(google.package, token) let upload: String = google_play_upload_aab(google.package, edit, aab_path, token) let assigned: String = google_play_track_assign(google.package, edit, google.track, cfg.version, cfg.build_number, token) google_play_edit_commit(google.package, edit, token) let outcome: PublishOutcome = outcome_new("google", cfg, google.track) { "platform": outcome.platform, "version": outcome.version, "build_number": outcome.build_number, "track": outcome.track, "rollout_percent": outcome.rollout_percent, "submission_id": json_get(upload, "id") } } // ── Rollout monitor ───────────────────────────────────────────────────────── fn rollout_monitor(outcome: PublishOutcome, cfg: RolloutConfig) -> Bool { // Polls store status, advances rollout percentage in stages. let current: Int = outcome.rollout_percent while current < cfg.target_percent { sleep_seconds(cfg.bake_hours * 3600) let crash_rate: Int = fetch_crash_rate(outcome.platform, outcome.version) if crash_rate > 100 { // 1% crash threshold (per 10000) println("[el-publish] rollout halted: crash rate too high") return false } let next: Int = current + 25 if next > cfg.target_percent { let next = cfg.target_percent } rollout_advance(outcome, next) let current = next } true } fn rollout_advance(outcome: PublishOutcome, percent: Int) -> Bool { println("[el-publish] " + outcome.platform + " rollout -> " + int_to_str(percent) + "%") true } // ── Entry — smoke test ────────────────────────────────────────────────────── let cfg: PublishConfig = publish_config_new("1.0.0", 42) let apple: AppleConfig = { "account": "will@neurontechnologies.ai", "bundle_id": "ai.neurontechnologies.myapp", "team_id": "ABC123", "api_key_id": "KEY", "api_issuer": "ISS" } println("[el-publish] " + cfg.version + " (" + int_to_str(cfg.build_number) + ") for " + apple.bundle_id)