127 lines
4.2 KiB
Rust
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,
|
|
}
|