fix(sync): 固化 release flow 状态码

This commit is contained in:
2026-07-31 15:57:07 +08:00
parent 99e6b3a23a
commit 4cc143f66d
12 changed files with 619 additions and 36 deletions
+216 -7
View File
@@ -15,11 +15,11 @@ use bat_infrastructure::{
OfficialServerInfoSource, OfficialTextUnitQuery, OfficialUpdateConfig, OfficialUpdateProgress,
OfficialUpdateReport, OfficialUpdateService, OfficialUpdateSnapshot, OfficialUpdateStatus,
OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState, PatchApplyKind,
PatchApplyParams, PatchApplyReport, SqliteResourceRepository, UnityFsFieldPatchParams,
UnityFsPatchReport, UnityFsStringFieldPatchParams, UnityFsTextAssetPatchParams,
LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_VERSIONS_DIR,
LOCALIZED_VERSION_STATE_FILE, OFFICIAL_PARSE_CACHE_FILE, OFFICIAL_TEXTUNIT_INDEX_FILE,
PRIVATE_FILE_MODE,
PatchApplyParams, PatchApplyReport, ReleaseFlowStatusCode, SqliteResourceRepository,
UnityFsFieldPatchParams, UnityFsPatchReport, UnityFsStringFieldPatchParams,
UnityFsTextAssetPatchParams, LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE,
LOCALIZED_VERSIONS_DIR, LOCALIZED_VERSION_STATE_FILE, OFFICIAL_PARSE_CACHE_FILE,
OFFICIAL_TEXTUNIT_INDEX_FILE, PRIVATE_FILE_MODE,
};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
@@ -649,6 +649,8 @@ struct DaemonStatusFile {
#[serde(default)]
current_stage: Option<String>,
#[serde(default)]
status_code: Option<String>,
#[serde(default)]
current_message: Option<String>,
#[serde(default)]
download_progress: Option<DaemonDownloadProgress>,
@@ -2450,9 +2452,55 @@ fn read_daemon_resource_state(
Ok((status_file, version_state))
}
fn flow_status_fields(
code: ReleaseFlowStatusCode,
) -> (&'static str, &'static str, &'static str, bool, bool) {
(
code.status(),
code.as_str(),
code.phase(),
code.terminal(),
code.retryable(),
)
}
fn build_resource_state_report(state_dir: &Path) -> anyhow::Result<serde_json::Value> {
let (status_file, version_state) = read_daemon_resource_state(state_dir)?;
let flow_status_code = status_file
.as_ref()
.and_then(|status| status.status_code.as_deref())
.and_then(ReleaseFlowStatusCode::from_str)
.or_else(|| {
if version_state
.as_ref()
.and_then(|state| state.in_progress_version.as_ref())
.is_some()
{
Some(ReleaseFlowStatusCode::OfficialDownloading)
} else if version_state
.as_ref()
.is_some_and(|state| !state.failed_versions.is_empty())
{
Some(ReleaseFlowStatusCode::OfficialFailed)
} else if version_state
.as_ref()
.and_then(|state| state.current_completed_version.as_ref())
.is_some()
{
Some(ReleaseFlowStatusCode::OfficialPublished)
} else {
Some(ReleaseFlowStatusCode::OfficialUnavailable)
}
})
.unwrap_or(ReleaseFlowStatusCode::OfficialUnavailable);
let (status, status_code, status_phase, status_terminal, status_retryable) =
flow_status_fields(flow_status_code);
Ok(serde_json::json!({
"status": status,
"status_code": status_code,
"status_phase": status_phase,
"status_terminal": status_terminal,
"status_retryable": status_retryable,
"resource_output_root": status_file
.as_ref()
.map(|status| status.resource_output_root.clone()),
@@ -2477,15 +2525,39 @@ fn build_catalog_status_report(state_dir: &Path) -> anyhow::Result<serde_json::V
.and_then(|state| state.current_completed_version.as_ref());
let snapshot = current.and_then(|record| read_snapshot(&record.snapshot_path).ok().flatten());
let (Some(record), Some(snapshot)) = (current, snapshot) else {
return Ok(serde_json::json!({ "available": false }));
let (status, status_code, status_phase, status_terminal, status_retryable) =
flow_status_fields(ReleaseFlowStatusCode::OfficialUnavailable);
let (distribution_status, distribution_status_code, _, _, _) =
flow_status_fields(ReleaseFlowStatusCode::DistributionBlocked);
return Ok(serde_json::json!({
"available": false,
"status": status,
"status_code": status_code,
"status_phase": status_phase,
"status_terminal": status_terminal,
"status_retryable": status_retryable,
"distribution_status": distribution_status,
"distribution_status_code": distribution_status_code,
}));
};
let official_seed_hash_marker_count = snapshot
.endpoint_markers
.iter()
.filter(|marker| marker.role == OfficialEndpointMarkerRole::OfficialSeedHash)
.count();
let (status, status_code, status_phase, status_terminal, status_retryable) =
flow_status_fields(ReleaseFlowStatusCode::OfficialPublished);
let (distribution_status, distribution_status_code, _, _, _) =
flow_status_fields(ReleaseFlowStatusCode::DistributionReady);
Ok(serde_json::json!({
"available": true,
"status": status,
"status_code": status_code,
"status_phase": status_phase,
"status_terminal": status_terminal,
"status_retryable": status_retryable,
"distribution_status": distribution_status,
"distribution_status_code": distribution_status_code,
"version": {
"id": record.id,
"completed_unix_seconds": record.completed_unix_seconds,
@@ -2712,13 +2784,37 @@ fn build_parse_status_report(state_dir: &Path) -> anyhow::Result<serde_json::Val
.as_ref()
.and_then(|state| state.current_completed_version.as_ref());
let Some(record) = current else {
return Ok(serde_json::json!({ "available": false }));
let (status, status_code, status_phase, status_terminal, status_retryable) =
flow_status_fields(ReleaseFlowStatusCode::ParseBlockedOfficial);
let (translation_status, translation_status_code, _, _, _) =
flow_status_fields(ReleaseFlowStatusCode::TranslationUnavailable);
return Ok(serde_json::json!({
"available": false,
"status": status,
"status_code": status_code,
"status_phase": status_phase,
"status_terminal": status_terminal,
"status_retryable": status_retryable,
"translation_status": translation_status,
"translation_status_code": translation_status_code,
}));
};
let cache_path = record.resource_root.join(OFFICIAL_PARSE_CACHE_FILE);
let Some(cache) = read_parse_cache_at(&record.resource_root).map_err(anyhow::Error::msg)?
else {
let (status, status_code, status_phase, status_terminal, status_retryable) =
flow_status_fields(ReleaseFlowStatusCode::ParsePending);
let (translation_status, translation_status_code, _, _, _) =
flow_status_fields(ReleaseFlowStatusCode::TranslationUnavailable);
return Ok(serde_json::json!({
"available": false,
"status": status,
"status_code": status_code,
"status_phase": status_phase,
"status_terminal": status_terminal,
"status_retryable": status_retryable,
"translation_status": translation_status,
"translation_status_code": translation_status_code,
"current_version_id": record.id,
"resource_root": record.resource_root,
"cache_path": cache_path,
@@ -2732,8 +2828,31 @@ fn build_parse_status_report(state_dir: &Path) -> anyhow::Result<serde_json::Val
let textunit_index_path = record.resource_root.join(OFFICIAL_TEXTUNIT_INDEX_FILE);
let textunit_index =
read_textunit_index_at(&record.resource_root).map_err(anyhow::Error::msg)?;
let parse_status_code = if cache.summary.failed_count > 0 {
ReleaseFlowStatusCode::ParseCompletedWithErrors
} else {
ReleaseFlowStatusCode::ParseCompleted
};
let queue_summary = textunit_queue.as_ref().map(|queue| &queue.summary);
let translation_status_code =
if queue_summary.is_some_and(|summary| summary.queued_task_count > 0) {
ReleaseFlowStatusCode::TranslationQueuedOffline
} else {
ReleaseFlowStatusCode::TranslationUnavailable
};
let (status, status_code, status_phase, status_terminal, status_retryable) =
flow_status_fields(parse_status_code);
let (translation_status, translation_status_code, _, _, _) =
flow_status_fields(translation_status_code);
Ok(serde_json::json!({
"available": true,
"status": status,
"status_code": status_code,
"status_phase": status_phase,
"status_terminal": status_terminal,
"status_retryable": status_retryable,
"translation_status": translation_status,
"translation_status_code": translation_status_code,
"current_version_id": record.id,
"resource_root": record.resource_root,
"cache_path": cache_path,
@@ -2880,11 +2999,19 @@ fn build_localized_status_report(
let mut patch_file_count = None;
let mut patch_text_asset_operation_count = None;
let mut rollback_previous_current_target = None;
let mut flow_status_code = if official_version_id.is_some() {
ReleaseFlowStatusCode::LocalizedPending
} else {
ReleaseFlowStatusCode::LocalizedBlockedOfficial
};
if let Some(localized_state) = state.as_ref() {
matches_current_official_release = official_version_id
.as_deref()
.is_some_and(|id| localized_state.official_release_id == id);
if official_version_id.is_some() && !matches_current_official_release {
flow_status_code = ReleaseFlowStatusCode::LocalizedStale;
}
if localized_state.status == "localized" && matches_current_official_release {
if let Some(release_id) = localized_state.current_release_id.as_deref() {
let candidate = localized_root.join(LOCALIZED_VERSIONS_DIR).join(release_id);
@@ -2907,15 +3034,22 @@ fn build_localized_status_report(
&& patch_manifest_matches_release
{
status = "localized";
flow_status_code = ReleaseFlowStatusCode::LocalizedPublished;
published_version_path = Some(candidate);
}
}
}
}
let (_, status_code, status_phase, status_terminal, status_retryable) =
flow_status_fields(flow_status_code);
Ok(serde_json::json!({
"available": state.is_some(),
"status": status,
"status_code": status_code,
"status_phase": status_phase,
"status_terminal": status_terminal,
"status_retryable": status_retryable,
"official_current_version_id": official_version_id,
"localized_output_root": localized_root,
"state_path": state_path,
@@ -3314,6 +3448,7 @@ struct DaemonStatusReport {
last_error: Option<String>,
next_retry_seconds: Option<u64>,
current_stage: Option<String>,
status_code: Option<String>,
current_message: Option<String>,
download_progress: Option<DaemonDownloadProgress>,
version_state_path: Option<PathBuf>,
@@ -3462,6 +3597,7 @@ fn start_daemon_with_args(
last_error: None,
next_retry_seconds: None,
current_stage: None,
status_code: None,
current_message: None,
download_progress: None,
pending_scheduled_force: false,
@@ -3691,6 +3827,9 @@ fn build_daemon_status_report(state_dir: &Path) -> anyhow::Result<DaemonStatusRe
current_stage: status_file
.as_ref()
.and_then(|status| status.current_stage.clone()),
status_code: status_file
.as_ref()
.and_then(|status| status.status_code.clone()),
current_message: status_file
.as_ref()
.and_then(|status| status.current_message.clone()),
@@ -4702,6 +4841,7 @@ impl HumanReport for OfficialUpdateReport {
fn print_human(&self) -> anyhow::Result<()> {
print_title("官方资源同步");
print_field("状态", self.update_status.as_str());
print_field("状态码", self.status_code.as_str());
print_field("应用版本", &self.app_version);
print_optional_field("Bundle 版本", self.bundle_version.as_deref());
print_field("连接组", &self.connection_group);
@@ -5692,6 +5832,7 @@ fn update_daemon_status(state_dir: &Path, update: DaemonStatusUpdate<'_>) -> any
last_error: None,
next_retry_seconds: None,
current_stage: None,
status_code: None,
current_message: None,
download_progress: None,
pending_scheduled_force: false,
@@ -5701,6 +5842,21 @@ fn update_daemon_status(state_dir: &Path, update: DaemonStatusUpdate<'_>) -> any
status.pid = std::process::id();
status.state = update.state.to_string();
status.updated_unix_seconds = unix_seconds_now();
status.status_code = match update.state {
"running" => Some(ReleaseFlowStatusCode::OfficialChecking.as_str().to_string()),
"waiting" => Some(
ReleaseFlowStatusCode::OfficialWaitingForResources
.as_str()
.to_string(),
),
"error" => Some(ReleaseFlowStatusCode::OfficialFailed.as_str().to_string()),
"sleeping" => update
.last_update_status
.as_deref()
.map(ReleaseFlowStatusCode::from_update_status)
.map(|code| code.as_str().to_string()),
_ => None,
};
status.last_update_status = update.last_update_status;
status.last_error = update.last_error;
status.next_retry_seconds = update.next_retry_seconds;
@@ -5728,6 +5884,7 @@ fn update_daemon_progress(state_dir: &Path, event: &OfficialUpdateProgress) -> a
status.pid = std::process::id();
status.updated_unix_seconds = unix_seconds_now();
status.current_stage = Some(event.stage.to_string());
status.status_code = Some(event.status_code.as_str().to_string());
status.current_message = Some(event.message.clone());
status.download_progress = match (event.download_index, event.download_total) {
(Some(index), Some(total)) => Some(DaemonDownloadProgress {
@@ -6269,6 +6426,8 @@ impl RotatingStructuredLogger {
"level": "info",
"stage": event.stage,
"stage_label": localized_stage(event.stage),
"status_code": event.status_code.as_str(),
"status_phase": event.status_code.phase(),
"message": event.message.as_str(),
"download": event.download_index.map(|index| serde_json::json!({
"index": index,
@@ -9788,6 +9947,7 @@ mod tests {
last_error: None,
next_retry_seconds: Some(60),
current_stage: None,
status_code: Some(ReleaseFlowStatusCode::OfficialUpToDate.as_str().to_string()),
current_message: None,
download_progress: None,
pending_scheduled_force: false,
@@ -9860,6 +10020,29 @@ mod tests {
current_dir
}
#[test]
fn dispatch_resource_state_reports_status_code_for_bat_api() {
let temp = tempfile::TempDir::new().unwrap();
let state_dir = temp.path().join("state");
let output_root = temp.path().join("output");
write_catalog_fixture(&state_dir, &output_root, "bundle-b2", None);
let envelope = dispatch_rpc_method(
&rpc_request("resource.state", None),
&state_dir,
&new_daemon_control(),
&test_task_context(),
"req-state-1".to_string(),
);
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["status"], "up_to_date");
assert_eq!(value["data"]["status_code"], "official.up_to_date");
assert_eq!(value["data"]["status_phase"], "official_sync");
assert_eq!(value["data"]["status_terminal"], true);
assert_eq!(value["data"]["last_update_status"], "up_to_date");
}
#[test]
fn dispatch_catalog_status_unavailable_without_state() {
let temp = tempfile::TempDir::new().unwrap();
@@ -9873,6 +10056,12 @@ mod tests {
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["available"], false);
assert_eq!(value["data"]["status"], "unavailable");
assert_eq!(value["data"]["status_code"], "official.unavailable");
assert_eq!(
value["data"]["distribution_status_code"],
"distribution.blocked"
);
}
#[test]
@@ -9893,6 +10082,12 @@ mod tests {
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["available"], true);
assert_eq!(value["data"]["bundle_version"], "bundle-b2");
assert_eq!(value["data"]["status"], "published");
assert_eq!(value["data"]["status_code"], "official.published");
assert_eq!(
value["data"]["distribution_status_code"],
"distribution.ready"
);
assert_eq!(value["data"]["version"]["id"], "v-current");
assert_eq!(value["data"]["endpoint_count"], 0);
assert_eq!(value["data"]["connection_group_name"], "Prod-Audit");
@@ -9938,6 +10133,12 @@ mod tests {
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["available"], true);
assert_eq!(value["data"]["status"], "completed");
assert_eq!(value["data"]["status_code"], "parse.completed");
assert_eq!(
value["data"]["translation_status_code"],
"translation.queued_offline"
);
assert_eq!(value["data"]["current_version_id"], "v-current");
assert_eq!(value["data"]["previous_version_id"], "v-previous");
assert_eq!(value["data"]["previous_snapshot_missing"], false);
@@ -10475,6 +10676,12 @@ mod tests {
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["available"], false);
assert_eq!(value["data"]["status"], "pending");
assert_eq!(value["data"]["status_code"], "parse.pending");
assert_eq!(
value["data"]["translation_status_code"],
"translation.unavailable"
);
assert_eq!(value["data"]["current_version_id"], "v-current");
}
@@ -10543,6 +10750,7 @@ mod tests {
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["available"], true);
assert_eq!(value["data"]["status"], "localized");
assert_eq!(value["data"]["status_code"], "localized.published");
assert_eq!(value["data"]["official_current_version_id"], "v-current");
assert_eq!(value["data"]["matches_current_official_release"], true);
assert_eq!(value["data"]["current_points_to_published_version"], true);
@@ -10601,6 +10809,7 @@ mod tests {
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["available"], true);
assert_eq!(value["data"]["status"], "not_localized");
assert_eq!(value["data"]["status_code"], "localized.stale");
assert_eq!(value["data"]["matches_current_official_release"], false);
assert_eq!(
value["data"]["published_version_path"],
+2
View File
@@ -26,6 +26,7 @@ pub mod official_textunit_queue;
pub mod official_update;
pub mod patch_ops;
pub mod path_security;
pub mod release_flow;
pub mod resources;
mod zip_validation;
@@ -121,6 +122,7 @@ pub use path_security::{
open_append_file, read_file_no_symlink, set_file_mode, validate_output_root,
validate_runtime_state_dir, write_file_atomic, PRIVATE_FILE_MODE, STATE_FILE_MODE,
};
pub use release_flow::ReleaseFlowStatusCode;
pub use resources::{InMemoryResourceRepository, SqliteResourceRepository};
/// Infrastructure 版本号
+32
View File
@@ -28,6 +28,7 @@ use crate::path_security::{
ensure_path_within_root, ensure_safe_directory_path, ensure_safe_file_target, lexical_absolute,
read_file_no_symlink, validate_output_root, write_file_atomic, STATE_FILE_MODE,
};
use crate::release_flow::ReleaseFlowStatusCode;
use crate::{
build_official_pull_plan_for_platform_inventory, build_official_sync_plan,
changed_endpoint_urls, default_official_platforms, DownloadError,
@@ -227,6 +228,16 @@ impl OfficialUpdateStatus {
Self::Downloaded => "downloaded",
}
}
/// Returns the stable cross-module flow status code.
pub const fn flow_status_code(self) -> ReleaseFlowStatusCode {
match self {
Self::UpToDate => ReleaseFlowStatusCode::OfficialUpToDate,
Self::WouldDownload => ReleaseFlowStatusCode::OfficialUpdateAvailable,
Self::WaitingForOfficialResources => ReleaseFlowStatusCode::OfficialWaitingForResources,
Self::Downloaded => ReleaseFlowStatusCode::OfficialPublished,
}
}
}
/// Publication state for localized resources associated with an official release.
@@ -247,6 +258,15 @@ impl LocalizedReleaseStatus {
Self::Localized => "localized",
}
}
/// Returns the stable status code for an official release that is known
/// to match the currently selected localized state.
pub const fn flow_status_code(self) -> ReleaseFlowStatusCode {
match self {
Self::NotLocalized => ReleaseFlowStatusCode::LocalizedPending,
Self::Localized => ReleaseFlowStatusCode::LocalizedPublished,
}
}
}
/// Snapshot of the official JP update state observed at a point in time.
@@ -712,6 +732,8 @@ impl OfficialVerificationSummary {
pub struct OfficialUpdateReport {
/// Final update status.
pub update_status: OfficialUpdateStatus,
/// Stable cross-module flow status code.
pub status_code: ReleaseFlowStatusCode,
/// Selected connection group.
pub connection_group: String,
/// Selected app version.
@@ -842,6 +864,8 @@ pub struct OfficialUpdateReport {
pub struct OfficialUpdateProgress {
/// Stable progress stage label.
pub stage: &'static str,
/// Stable cross-module flow status code derived from `stage`.
pub status_code: ReleaseFlowStatusCode,
/// Human-readable status line.
pub message: String,
/// One-based download index when the event represents URL download work.
@@ -877,6 +901,7 @@ impl OfficialUpdateProgress {
pub fn new(stage: &'static str, message: impl Into<String>) -> Self {
Self {
stage,
status_code: ReleaseFlowStatusCode::from_progress_stage(stage),
message: message.into(),
download_index: None,
download_total: None,
@@ -1579,6 +1604,11 @@ impl OfficialUpdateService {
} else {
OfficialUpdateStatus::UpToDate
},
status_code: if should_download {
ReleaseFlowStatusCode::OfficialUpdateAvailable
} else {
ReleaseFlowStatusCode::OfficialUpToDate
},
connection_group: current_snapshot.connection_group_name.clone(),
app_version: current_snapshot.app_version.clone(),
bundle_version: current_snapshot.bundle_version.clone(),
@@ -1942,6 +1972,7 @@ impl OfficialUpdateService {
};
report.update_status = OfficialUpdateStatus::Downloaded;
report.status_code = report.update_status.flow_status_code();
report.active_resource_root = published_version_path.clone();
report.published_version_path = Some(published_version_path.clone());
report.snapshot_path = final_snapshot_path.clone();
@@ -2404,6 +2435,7 @@ fn waiting_for_official_resources_report(
let localized_info = localized_release_info_for(config, Some(active_release_id.as_str()));
OfficialUpdateReport {
update_status: OfficialUpdateStatus::WaitingForOfficialResources,
status_code: ReleaseFlowStatusCode::OfficialWaitingForResources,
connection_group: base_snapshot.connection_group_name.clone(),
app_version: base_snapshot.app_version.clone(),
bundle_version: base_snapshot.bundle_version.clone(),
+295
View File
@@ -0,0 +1,295 @@
//! Stable status codes for the official-resource to localized-release flow.
//!
//! The codes describe observable lifecycle state. They are deliberately
//! separate from `BAT-ERR-*`: an error code explains why an operation failed,
//! while a flow status code explains what a caller can do next.
use serde::{Deserialize, Serialize};
/// Stable status code shared by Rust reports and read-only RPC data.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReleaseFlowStatusCode {
/// No published official release is currently available.
#[serde(rename = "official.unavailable")]
OfficialUnavailable,
/// The producer is discovering remote and local official-resource state.
#[serde(rename = "official.checking")]
OfficialChecking,
/// The producer determined that a new or repaired official release is needed.
#[serde(rename = "official.update_available")]
OfficialUpdateAvailable,
/// Official resources are being downloaded or reused into staging.
#[serde(rename = "official.downloading")]
OfficialDownloading,
/// Downloaded official resources are being verified.
#[serde(rename = "official.validating")]
OfficialValidating,
/// A verified official release is being staged or atomically published.
#[serde(rename = "official.publishing")]
OfficialPublishing,
/// A verified official release has been published.
#[serde(rename = "official.published")]
OfficialPublished,
/// The published official release already matches the observed remote state.
#[serde(rename = "official.up_to_date")]
OfficialUpToDate,
/// Launcher/server-info has advanced before required CDN resources are readable.
#[serde(rename = "official.waiting_for_resources")]
OfficialWaitingForResources,
/// Official-resource production failed before a publishable state was reached.
#[serde(rename = "official.failed")]
OfficialFailed,
/// Parsing is blocked because there is no published official release.
#[serde(rename = "parse.blocked_official")]
ParseBlockedOfficial,
/// A published official release exists but parse cache is not present yet.
#[serde(rename = "parse.pending")]
ParsePending,
/// Parse cache or TextUnit index generation is running.
#[serde(rename = "parse.running")]
ParseRunning,
/// Parse cache and TextUnit indexes are present without recorded parse failures.
#[serde(rename = "parse.completed")]
ParseCompleted,
/// Parse cache exists but contains parser or extraction failures.
#[serde(rename = "parse.completed_with_errors")]
ParseCompletedWithErrors,
/// Translation worker integration is not available for the observed release.
#[serde(rename = "translation.unavailable")]
TranslationUnavailable,
/// Translation handoff files are being prepared from the official change set.
#[serde(rename = "translation.handoff_preparing")]
TranslationHandoffPreparing,
/// Translation tasks have been queued to local offline handoff files.
#[serde(rename = "translation.queued_offline")]
TranslationQueuedOffline,
/// Localized publication is blocked because there is no official release.
#[serde(rename = "localized.blocked_official")]
LocalizedBlockedOfficial,
/// The current official release has no matching localized publication yet.
#[serde(rename = "localized.pending")]
LocalizedPending,
/// A localized state exists but it does not match the current official release.
#[serde(rename = "localized.stale")]
LocalizedStale,
/// A localized release is published and matches the current official release.
#[serde(rename = "localized.published")]
LocalizedPublished,
/// Distribution cannot serve a usable release for the observed channel.
#[serde(rename = "distribution.blocked")]
DistributionBlocked,
/// Distribution can serve the published release.
#[serde(rename = "distribution.ready")]
DistributionReady,
}
impl ReleaseFlowStatusCode {
/// Returns the stable wire label.
pub const fn as_str(self) -> &'static str {
match self {
Self::OfficialUnavailable => "official.unavailable",
Self::OfficialChecking => "official.checking",
Self::OfficialUpdateAvailable => "official.update_available",
Self::OfficialDownloading => "official.downloading",
Self::OfficialValidating => "official.validating",
Self::OfficialPublishing => "official.publishing",
Self::OfficialPublished => "official.published",
Self::OfficialUpToDate => "official.up_to_date",
Self::OfficialWaitingForResources => "official.waiting_for_resources",
Self::OfficialFailed => "official.failed",
Self::ParseBlockedOfficial => "parse.blocked_official",
Self::ParsePending => "parse.pending",
Self::ParseRunning => "parse.running",
Self::ParseCompleted => "parse.completed",
Self::ParseCompletedWithErrors => "parse.completed_with_errors",
Self::TranslationUnavailable => "translation.unavailable",
Self::TranslationHandoffPreparing => "translation.handoff_preparing",
Self::TranslationQueuedOffline => "translation.queued_offline",
Self::LocalizedBlockedOfficial => "localized.blocked_official",
Self::LocalizedPending => "localized.pending",
Self::LocalizedStale => "localized.stale",
Self::LocalizedPublished => "localized.published",
Self::DistributionBlocked => "distribution.blocked",
Self::DistributionReady => "distribution.ready",
}
}
/// Returns the short status value used alongside `status_code` in RPC
/// payloads. This keeps existing human-facing labels independent from the
/// namespaced wire code.
pub const fn status(self) -> &'static str {
match self {
Self::OfficialUnavailable => "unavailable",
Self::OfficialChecking => "checking",
Self::OfficialUpdateAvailable => "update_available",
Self::OfficialDownloading => "downloading",
Self::OfficialValidating => "validating",
Self::OfficialPublishing => "publishing",
Self::OfficialPublished => "published",
Self::OfficialUpToDate => "up_to_date",
Self::OfficialWaitingForResources => "waiting_for_resources",
Self::OfficialFailed => "failed",
Self::ParseBlockedOfficial => "blocked_official",
Self::ParsePending => "pending",
Self::ParseRunning => "running",
Self::ParseCompleted => "completed",
Self::ParseCompletedWithErrors => "completed_with_errors",
Self::TranslationUnavailable => "unavailable",
Self::TranslationHandoffPreparing => "handoff_preparing",
Self::TranslationQueuedOffline => "queued_offline",
Self::LocalizedBlockedOfficial => "blocked_official",
Self::LocalizedPending => "pending",
Self::LocalizedStale => "stale",
Self::LocalizedPublished => "published",
Self::DistributionBlocked => "blocked",
Self::DistributionReady => "ready",
}
}
/// Parses a status code read from a persisted daemon/RPC snapshot.
pub fn from_str(code: &str) -> Option<Self> {
Some(match code {
"official.unavailable" => Self::OfficialUnavailable,
"official.checking" => Self::OfficialChecking,
"official.update_available" => Self::OfficialUpdateAvailable,
"official.downloading" => Self::OfficialDownloading,
"official.validating" => Self::OfficialValidating,
"official.publishing" => Self::OfficialPublishing,
"official.published" => Self::OfficialPublished,
"official.up_to_date" => Self::OfficialUpToDate,
"official.waiting_for_resources" => Self::OfficialWaitingForResources,
"official.failed" => Self::OfficialFailed,
"parse.blocked_official" => Self::ParseBlockedOfficial,
"parse.pending" => Self::ParsePending,
"parse.running" => Self::ParseRunning,
"parse.completed" => Self::ParseCompleted,
"parse.completed_with_errors" => Self::ParseCompletedWithErrors,
"translation.unavailable" => Self::TranslationUnavailable,
"translation.handoff_preparing" => Self::TranslationHandoffPreparing,
"translation.queued_offline" => Self::TranslationQueuedOffline,
"localized.blocked_official" => Self::LocalizedBlockedOfficial,
"localized.pending" => Self::LocalizedPending,
"localized.stale" => Self::LocalizedStale,
"localized.published" => Self::LocalizedPublished,
"distribution.blocked" => Self::DistributionBlocked,
"distribution.ready" => Self::DistributionReady,
_ => return None,
})
}
/// Returns the broad flow phase represented by this code.
pub const fn phase(self) -> &'static str {
match self {
Self::OfficialUnavailable
| Self::OfficialChecking
| Self::OfficialUpdateAvailable
| Self::OfficialDownloading
| Self::OfficialValidating
| Self::OfficialPublishing
| Self::OfficialPublished
| Self::OfficialUpToDate
| Self::OfficialWaitingForResources
| Self::OfficialFailed => "official_sync",
Self::ParseBlockedOfficial
| Self::ParsePending
| Self::ParseRunning
| Self::ParseCompleted
| Self::ParseCompletedWithErrors => "parse",
Self::TranslationUnavailable
| Self::TranslationHandoffPreparing
| Self::TranslationQueuedOffline => "translation",
Self::LocalizedBlockedOfficial
| Self::LocalizedPending
| Self::LocalizedStale
| Self::LocalizedPublished => "localized_publish",
Self::DistributionBlocked | Self::DistributionReady => "distribution",
}
}
/// Returns whether the state is stable for the current observation.
pub const fn terminal(self) -> bool {
!matches!(
self,
Self::OfficialChecking
| Self::OfficialDownloading
| Self::OfficialValidating
| Self::OfficialPublishing
| Self::ParseRunning
| Self::TranslationHandoffPreparing
)
}
/// Returns whether the producer may retry the operation automatically.
pub const fn retryable(self) -> bool {
matches!(
self,
Self::OfficialWaitingForResources
| Self::OfficialFailed
| Self::ParsePending
| Self::ParseCompletedWithErrors
)
}
/// Maps an existing official update result to the stable flow code.
pub fn from_update_status(status: &str) -> Self {
match status {
"would_download" => Self::OfficialUpdateAvailable,
"waiting_for_official_resources" => Self::OfficialWaitingForResources,
"downloaded" => Self::OfficialPublished,
"up_to_date" => Self::OfficialUpToDate,
_ => Self::OfficialFailed,
}
}
/// Maps an existing progress stage to the stable flow code.
pub fn from_progress_stage(stage: &str) -> Self {
match stage {
"download" => Self::OfficialDownloading,
"audit" | "snapshot" => Self::OfficialValidating,
"publish" | "launcher-bootstrap" => Self::OfficialPublishing,
"parse" => Self::ParseRunning,
"changes" => Self::TranslationHandoffPreparing,
"finish" => Self::OfficialPublished,
_ => Self::OfficialChecking,
}
}
}
#[cfg(test)]
mod tests {
use super::ReleaseFlowStatusCode;
#[test]
fn status_codes_are_stable_and_namespaced() {
let codes = [
ReleaseFlowStatusCode::OfficialUnavailable,
ReleaseFlowStatusCode::OfficialChecking,
ReleaseFlowStatusCode::OfficialUpdateAvailable,
ReleaseFlowStatusCode::OfficialDownloading,
ReleaseFlowStatusCode::OfficialValidating,
ReleaseFlowStatusCode::OfficialPublishing,
ReleaseFlowStatusCode::OfficialPublished,
ReleaseFlowStatusCode::OfficialUpToDate,
ReleaseFlowStatusCode::OfficialWaitingForResources,
ReleaseFlowStatusCode::OfficialFailed,
ReleaseFlowStatusCode::ParseBlockedOfficial,
ReleaseFlowStatusCode::ParsePending,
ReleaseFlowStatusCode::ParseRunning,
ReleaseFlowStatusCode::ParseCompleted,
ReleaseFlowStatusCode::ParseCompletedWithErrors,
ReleaseFlowStatusCode::TranslationUnavailable,
ReleaseFlowStatusCode::TranslationHandoffPreparing,
ReleaseFlowStatusCode::TranslationQueuedOffline,
ReleaseFlowStatusCode::LocalizedBlockedOfficial,
ReleaseFlowStatusCode::LocalizedPending,
ReleaseFlowStatusCode::LocalizedStale,
ReleaseFlowStatusCode::LocalizedPublished,
ReleaseFlowStatusCode::DistributionBlocked,
ReleaseFlowStatusCode::DistributionReady,
];
let labels = codes.iter().map(|code| code.as_str()).collect::<Vec<_>>();
let unique = labels.iter().collect::<std::collections::BTreeSet<_>>();
assert_eq!(labels.len(), unique.len());
assert!(labels.iter().all(|label| label.contains('.')));
}
}