This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-ui/vessels/el-publish/src/main.el
T
Will Anderson f4abfe6fdc feat: rename crates/ → vessels/ + add El ports per sub-vessel
Belated rename commit for foundation/el-ui — was missed in the
workspace-wide crates→vessels pass earlier today. Same structural
intent as the rename in the other repos: 'crates' is the Rust word,
'vessel' is El's, and the directory rename is the marker that this
slot holds an El buildable unit even if its current contents are
still Rust pending port.

Plus the El ports themselves — manifest.el + src/main.el per sub-
vessel (el-aop, el-auth, el-config, el-i18n, el-identity, el-layout,
el-platform, el-publish, el-secrets, el-services, el-style, el-ui-
compiler). The ui-compiler is a stub: elc only emits C right now;
generating browser-target JS/Wasm is the biggest open language gap
and gets its own project. Until then, el-ui-compiler emits a JS
module that throws elc.backend_missing so callers fail loudly.
Cross-repo path dependencies in Cargo.toml updated to vessels/.
2026-04-30 18:18:39 -05:00

244 lines
11 KiB
EmacsLisp

// 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)