Files
el/ui/vessels/el-publish/src/apple.rs
T

127 lines
4.2 KiB
Rust

//! Apple App Store Connect API publisher.
//!
//! Models the App Store Connect API calls. Stub the actual HTTP — structure is
//! correct and complete. A future agent fills in the `reqwest` calls.
//!
//! API reference: https://developer.apple.com/documentation/appstoreconnectapi
use crate::{
config::{AppleConfig, PublishConfig},
metadata::StoreMetadata,
PublishError, PublishOutcome, PublishResult,
};
/// Publishes app builds to Apple App Store Connect / TestFlight.
pub struct ApplePublisher {
pub apple_config: AppleConfig,
pub publish_config: PublishConfig,
/// API key for App Store Connect (loaded from env or keychain).
#[allow(dead_code)]
api_key: Option<String>,
}
impl ApplePublisher {
pub fn new(publish_config: PublishConfig) -> PublishResult<Self> {
let apple_config = publish_config.apple.clone().ok_or_else(|| {
PublishError::Config("no [publish.apple] section in el.toml".into())
})?;
Ok(Self {
apple_config,
publish_config,
api_key: std::env::var("APP_STORE_CONNECT_API_KEY").ok(),
})
}
/// Upload a build to TestFlight.
///
/// In production, this calls the App Store Connect API:
/// POST /v1/builds
/// PUT /v1/builds/{id}/betaAppReviewDetail
/// POST /v1/betaTestersConfigurations
pub fn upload_to_testflight(
&self,
ipa_path: &str,
) -> PublishResult<String> {
// TODO: use reqwest to call App Store Connect API:
// 1. Authenticate with JWT from API key
// 2. POST /v1/builds with IPA binary
// 3. Poll build status until "READY_FOR_BETA_SUBMISSION"
// 4. Submit to TestFlight review
let _ = ipa_path;
let submission_id = format!(
"TF-{}-{}",
self.apple_config.bundle_id,
self.publish_config.build_number
);
Ok(submission_id)
}
/// Submit to App Store review.
///
/// In production:
/// POST /v1/appStoreVersionSubmissions
/// POST /v1/appStoreVersions/{id}/appStoreVersionLocalizations (for metadata)
pub fn submit_to_app_store(
&self,
build_id: &str,
metadata: &StoreMetadata,
) -> PublishResult<String> {
// TODO: actual API calls
let _ = (build_id, metadata);
let review_id = format!("AS-{}", self.publish_config.build_number);
Ok(review_id)
}
/// Set staged rollout percentage on App Store.
/// Only available after the initial release.
pub fn set_phased_release(&self, percent: u8) -> PublishResult<()> {
if percent > 100 {
return Err(PublishError::Config(format!(
"rollout percent {} exceeds 100",
percent
)));
}
// TODO: PATCH /v1/appStoreVersionPhasedReleases/{id}
let _ = percent;
Ok(())
}
/// Full publish flow: build → upload → submit.
pub fn publish(&self, ipa_path: &str, to_beta: bool) -> PublishResult<PublishOutcome> {
let metadata = StoreMetadata::load_from_dir(&self.publish_config.metadata_dir)?;
let build_id = self.upload_to_testflight(ipa_path)?;
let track = if to_beta { "testflight" } else { "app_store" };
if !to_beta {
let review_id = self.submit_to_app_store(&build_id, &metadata)?;
let _ = review_id;
}
if let Some(rollout) = &self.publish_config.rollout {
self.set_phased_release(rollout.initial_percent)?;
}
Ok(PublishOutcome::new("apple", &self.publish_config, track)
.with_submission_id(build_id))
}
/// List existing builds from App Store Connect.
pub fn list_builds(&self) -> PublishResult<Vec<BuildInfo>> {
// TODO: GET /v1/builds?filter[bundleId]=...
Ok(vec![BuildInfo {
id: format!("build-{}", self.publish_config.build_number),
version: self.publish_config.version.clone(),
status: "READY_FOR_DISTRIBUTION".into(),
}])
}
}
/// Info about a build in App Store Connect.
#[derive(Debug, Clone)]
pub struct BuildInfo {
pub id: String,
pub version: String,
pub status: String,
}