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
+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('.')));
}
}