mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
fix(sync): 固化 release flow 状态码
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ Rust 侧官方日服资源链路已经从实验验证推进到正式入口:
|
|||||||
5. 下载后使用本地 manifest 的 size + BLAKE3 校验复用文件;所有 `.zip` 在下载验收、复用、本地 audit/verify 时做 ZIP 结构校验;官方 seed `.hash` 使用标准 `xxHash32(seed=0)` 强校验(早期实现的非标准 avalanche 常量已修正)。
|
5. 下载后使用本地 manifest 的 size + BLAKE3 校验复用文件;所有 `.zip` 在下载验收、复用、本地 audit/verify 时做 ZIP 结构校验;官方 seed `.hash` 使用标准 `xxHash32(seed=0)` 强校验(早期实现的非标准 avalanche 常量已修正)。
|
||||||
6. 支持 `.part` 断点续传、失败后 clean retry、本地 manifest audit/repair、失败 staging 恢复复用、403/404/5xx 分类重试(重试带指数退避)、下载 quarantine 诊断,以及旧 launcher 包官方 primary/backup CDN 切换。启动器/server-info 先行更新但 client-patch seed marker 或必需 seed catalog 尚未开放时,会进入 `waiting_for_official_resources`,保留现有 `current`,不创建失败 staging,也不写入失败版本循环;启用 `--auto-discover` 的非 dry-run 会写入 `<output>/official-launcher-bootstrap.pending.json` 作为维护期证据。下载执行保持顺序处理;manifest/quarantine 簿记与 seed `.hash` 校验仍逐项执行,`fail-fast` 与「不发布不完整资源」不变量不变。下载进度按已完成数量单调上报,不再使用 plan 序号计算百分比。
|
6. 支持 `.part` 断点续传、失败后 clean retry、本地 manifest audit/repair、失败 staging 恢复复用、403/404/5xx 分类重试(重试带指数退避)、下载 quarantine 诊断,以及旧 launcher 包官方 primary/backup CDN 切换。启动器/server-info 先行更新但 client-patch seed marker 或必需 seed catalog 尚未开放时,会进入 `waiting_for_official_resources`,保留现有 `current`,不创建失败 staging,也不写入失败版本循环;启用 `--auto-discover` 的非 dry-run 会写入 `<output>/official-launcher-bootstrap.pending.json` 作为维护期证据。下载执行保持顺序处理;manifest/quarantine 簿记与 seed `.hash` 校验仍逐项执行,`fail-fast` 与「不发布不完整资源」不变量不变。下载进度按已完成数量单调上报,不再使用 plan 序号计算百分比。
|
||||||
7. 支持 curl 传输层本地代理:默认自动检测 `HTTPS_PROXY` / `ALL_PROXY` / `HTTP_PROXY` 及小写环境变量(带凭据的代理推荐用环境变量配置),也可用 `--proxy <URL>` 显式指定或 `--no-proxy` 强制直连;代理决策会写入 progress log、daemon log 和 `bat doctor` 诊断输出。代理凭据不落世界可读位置:日志/`status` 脱敏,传给 curl 经 `ALL_PROXY` 环境变量而非 argv,`--daemon` 下经环境变量下传后台子进程、不进子进程 argv 或 `bat-status.json`,复用凭据存于 `bat-proxy.secret`(`0600`)且 `clean-stable` 会清除。
|
7. 支持 curl 传输层本地代理:默认自动检测 `HTTPS_PROXY` / `ALL_PROXY` / `HTTP_PROXY` 及小写环境变量(带凭据的代理推荐用环境变量配置),也可用 `--proxy <URL>` 显式指定或 `--no-proxy` 强制直连;代理决策会写入 progress log、daemon log 和 `bat doctor` 诊断输出。代理凭据不落世界可读位置:日志/`status` 脱敏,传给 curl 经 `ALL_PROXY` 环境变量而非 argv,`--daemon` 下经环境变量下传后台子进程、不进子进程 argv 或 `bat-status.json`,复用凭据存于 `bat-proxy.secret`(`0600`)且 `clean-stable` 会清除。
|
||||||
8. `bat --watch` 可常驻运行,`bat --daemon` 可后台运行并用 `bat status` / `bat stop` / `bat restart` / `bat reload` / `bat logs` 管理;daemon 使用 `bat.sock` Unix socket JSON-RPC 作为 live 控制通道,PID/状态/日志文件作为快照和 fallback,`bat-events.jsonl` 记录带轮转的结构化事件日志,`bat-control.lock` 串行化控制命令;正常检查默认每 1 小时一次;远端和本地一致时默认静默,失败后默认 60 秒快速重试,官方资源端尚未开放时状态为 `waiting` 并同样按错误重试间隔探测;CLI 默认向 stdout 输出人类可读摘要,向 stderr 输出 ASCII banner、progress log、失败分类和 quarantine 状态,需要机器输出时使用 `--json --no-progress`。
|
8. `bat --watch` 可常驻运行,`bat --daemon` 可后台运行并用 `bat status` / `bat stop` / `bat restart` / `bat reload` / `bat logs` 管理;daemon 使用 `bat.sock` Unix socket JSON-RPC 作为 live 控制通道,PID/状态/日志文件作为快照和 fallback,`bat-events.jsonl` 记录带轮转的结构化事件日志,`bat-control.lock` 串行化控制命令;正常检查默认每 1 小时一次;远端和本地一致时默认静默,失败后默认 60 秒快速重试,官方资源端尚未开放时状态为 `waiting` 并同样按错误重试间隔探测;`resource.state` / `catalog.status` / `parse.status` / `localized.status` 会返回 `status` 与稳定 `status_code`(如 `official.up_to_date`、`official.published`、`parse.completed`、`translation.queued_offline`、`localized.published`、`distribution.ready`),供 `bat-api` 等读侧判断阶段、终态和重试属性;CLI 默认向 stdout 输出人类可读摘要,向 stderr 输出 ASCII banner、progress log、失败分类和 quarantine 状态,需要机器输出时使用 `--json --no-progress`。
|
||||||
9. 远端 snapshot 未变化但输出目录为空时,会按首次运行执行全量拉取;官方 seed `.hash` 校验失败时会清理对应 manifest 条目,避免失败产物被后续本地 audit 误判为可复用。
|
9. 远端 snapshot 未变化但输出目录为空时,会按首次运行执行全量拉取;官方 seed `.hash` 校验失败时会清理对应 manifest 条目,避免失败产物被后续本地 audit 误判为可复用。
|
||||||
10. 默认官方原版资源目录是 `./bat-resources`,默认汉化产物目录是 `./bat-localized`,默认后台状态目录是 `/tmp/bat-pid`;官方资源目录是发布根目录,包含 `current` symlink、`versions/<id>` 和 `.staging/<id>`,非 dry-run 会先写 staging,校验完成后发布 versioned 目录并原子切换 `current`;启用 `--auto-discover` 的 release 会包含 `official-launcher-bootstrap.json`,up-to-date 轮询会为旧 release 补写该产物;如果上一轮同一 app version、bundle version 和 Addressables root 的 staging 失败但目录仍安全存在,下一轮会复用该 staging 并按 manifest 逐文件校验/补下载;后台状态目录包含 `bat.sock`、`bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl`、任务历史 `bat-tasks.json` 和短生命周期 `bat-control.lock`;非 dry-run 使用 `--output/.official-sync.lock` 防止并发写同一资源目录,live daemon 会阻止前台写命令直接修改它正在管理的同一目录。
|
10. 默认官方原版资源目录是 `./bat-resources`,默认汉化产物目录是 `./bat-localized`,默认后台状态目录是 `/tmp/bat-pid`;官方资源目录是发布根目录,包含 `current` symlink、`versions/<id>` 和 `.staging/<id>`,非 dry-run 会先写 staging,校验完成后发布 versioned 目录并原子切换 `current`;启用 `--auto-discover` 的 release 会包含 `official-launcher-bootstrap.json`,up-to-date 轮询会为旧 release 补写该产物;如果上一轮同一 app version、bundle version 和 Addressables root 的 staging 失败但目录仍安全存在,下一轮会复用该 staging 并按 manifest 逐文件校验/补下载;后台状态目录包含 `bat.sock`、`bat.pid`、`bat-status.json`、`bat-daemon.log`、`bat-events.jsonl`、任务历史 `bat-tasks.json` 和短生命周期 `bat-control.lock`;非 dry-run 使用 `--output/.official-sync.lock` 防止并发写同一资源目录,live daemon 会阻止前台写命令直接修改它正在管理的同一目录。
|
||||||
11. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
11. 官方同步会拒绝危险输出目录、路径逃逸和现有 symlink 路径组件;下载目标、`.part`、manifest、snapshot、PID、status、log 和控制锁文件不会跟随 symlink,daemon 状态类文件默认以 `0600` 权限创建。
|
||||||
|
|||||||
@@ -118,6 +118,15 @@ SQLite `ResourceRepository`,索引不存在时返回 `ok=true` 且
|
|||||||
`text_unit_error_count` 等字段。旧索引库会通过 `metadata_json` 迁移列得到
|
`text_unit_error_count` 等字段。旧索引库会通过 `metadata_json` 迁移列得到
|
||||||
默认空 metadata。
|
默认空 metadata。
|
||||||
|
|
||||||
|
`resource.state`、`catalog.status`、`parse.status` 和 `localized.status`
|
||||||
|
都会返回当前观察面的短状态 `status` 与稳定状态码 `status_code`。`status_code`
|
||||||
|
使用命名空间格式,例如 `official.up_to_date`、`official.published`、
|
||||||
|
`parse.completed`、`translation.queued_offline`、`localized.published` 和
|
||||||
|
`distribution.ready`。这些状态码描述资源/解析/翻译 handoff/汉化/分发生命周期;
|
||||||
|
失败原因仍使用 `BAT-ERR-*` 错误码,二者不混用。响应还会包含
|
||||||
|
`status_phase`、`status_terminal` 和 `status_retryable`,供 `bat-api` 等读侧
|
||||||
|
决定展示、重试或 readiness。
|
||||||
|
|
||||||
官方资源完整新版本发布后,Rust 侧会先比较上一完整 release 与当前 release
|
官方资源完整新版本发布后,Rust 侧会先比较上一完整 release 与当前 release
|
||||||
的 download manifest,并在当前 release 根目录写出:
|
的 download manifest,并在当前 release 根目录写出:
|
||||||
|
|
||||||
@@ -151,7 +160,9 @@ SQLite `ResourceRepository`,索引不存在时返回 `ok=true` 且
|
|||||||
`textunit_index_available=true`、`textunit_index_path` 和
|
`textunit_index_available=true`、`textunit_index_path` 和
|
||||||
`textunit_index_summary`;存在 `official-textunit-tasks.json` 时,响应会包含
|
`textunit_index_summary`;存在 `official-textunit-tasks.json` 时,响应会包含
|
||||||
`textunit_queue_available=true`、`textunit_task_queue_path` 和
|
`textunit_queue_available=true`、`textunit_task_queue_path` 和
|
||||||
`textunit_task_summary`。
|
`textunit_task_summary`。当 TextUnit 队列存在且有离线任务时,
|
||||||
|
`translation_status_code=translation.queued_offline`;真实 Crowdin worker
|
||||||
|
尚未接入时不会返回翻译完成状态。
|
||||||
|
|
||||||
`parse.text_units` / `parse.errors` 是只读查询;没有当前 release 或没有
|
`parse.text_units` / `parse.errors` 是只读查询;没有当前 release 或没有
|
||||||
`official-textunit-index.json` 时返回 `ok=true` 且 `data.available=false`。
|
`official-textunit-index.json` 时返回 `ok=true` 且 `data.available=false`。
|
||||||
@@ -202,6 +213,9 @@ offset 和 error。TypeTree-covered managed reference 字段会进入结构化
|
|||||||
| `catalog.refresh` | 已实现 | `{ "force": false }` | `{ "task_id": "...", "kind": "catalog.refresh" }`。 |
|
| `catalog.refresh` | 已实现 | `{ "force": false }` | `{ "task_id": "...", "kind": "catalog.refresh" }`。 |
|
||||||
|
|
||||||
只读查询在没有可用版本时返回 `ok=true` 且 `data.available=false`。
|
只读查询在没有可用版本时返回 `ok=true` 且 `data.available=false`。
|
||||||
|
`catalog.status` 可用时会返回 `status_code=official.published`,并用
|
||||||
|
`distribution_status_code=distribution.ready` 表示该官方 release 可被读侧分发;
|
||||||
|
不可用时对应 `official.unavailable` / `distribution.blocked`。
|
||||||
|
|
||||||
### task
|
### task
|
||||||
|
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
- `cmd/bat-api`:组织 Rust `bat` 已发布 release 的启动前资源入口,并只读分发官方 CDN host/path 形态资源。
|
- `cmd/bat-api`:组织 Rust `bat` 已发布 release 的启动前资源入口,并只读分发官方 CDN host/path 形态资源。
|
||||||
- **拉取归属 Rust `bat`**;`bat-api` 不做下载器。
|
- **拉取归属 Rust `bat`**;`bat-api` 不做下载器。
|
||||||
- 发现经 `bat.sock`:先 `daemon.status`,再 `daemon.doctor`,再 `catalog.status` / `resource.manifest`。
|
- 发现经 `bat.sock`:先 `daemon.status`,再 `daemon.doctor`,再 `catalog.status` / `resource.manifest`。
|
||||||
|
- `bat-api` 通过 RPC 读取的 `resource.state`、`catalog.status`、`parse.status` 和 `localized.status` 会获得短状态 `status` 与稳定生命周期状态码 `status_code`;错误原因仍以 `BAT-ERR-*` 为准。
|
||||||
- 生产与 Rust `bat` 同环境运行,资源根来自 RPC 返回的 `resource_root`;`--resource-root` 仅用于 fixture 或应急只读诊断。
|
- 生产与 Rust `bat` 同环境运行,资源根来自 RPC 返回的 `resource_root`;`--resource-root` 仅用于 fixture 或应急只读诊断。
|
||||||
- `.env` 配置端口 / public base / RPC socket / RPC 刷新周期;预留 database/redis。
|
- `.env` 配置端口 / public base / RPC socket / RPC 刷新周期;预留 database/redis。
|
||||||
- `/v1/bootstrap` 返回 RPC 健康、release 摘要、server-info URL、client-patch base 和改写后的 Addressables root。
|
- `/v1/bootstrap` 返回 RPC 健康、release 摘要、server-info URL、client-patch base 和改写后的 Addressables root。
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
| H | 生产文件字节从 RPC 返回的 `resource_root` 读盘;`bat-api` 与 daemon 同服务器/同容器/共享文件系统部署;`--resource-root` 仅 fixture 或应急只读诊断 |
|
| H | 生产文件字节从 RPC 返回的 `resource_root` 读盘;`bat-api` 与 daemon 同服务器/同容器/共享文件系统部署;`--resource-root` 仅 fixture 或应急只读诊断 |
|
||||||
| I | 真数据在**已全量拉取且长期运行 Rust `bat` 的远程服务器**;开发机不跑全量 `bat`,用 fixture、mock RPC 和 Go 门禁验证;远程联调等连接信息 |
|
| I | 真数据在**已全量拉取且长期运行 Rust `bat` 的远程服务器**;开发机不跑全量 `bat`,用 fixture、mock RPC 和 Go 门禁验证;远程联调等连接信息 |
|
||||||
| J | 索引以 **manifest + 磁盘 Present/size** 为准 |
|
| J | 索引以 **manifest + 磁盘 Present/size** 为准 |
|
||||||
|
| J2 | RPC 状态以 Rust 返回的 `status` / `status_code` 为准;`bat-api` 只读消费,不自行推导同步状态 |
|
||||||
|
|
||||||
### 进程配置
|
### 进程配置
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ use bat_infrastructure::{
|
|||||||
OfficialServerInfoSource, OfficialTextUnitQuery, OfficialUpdateConfig, OfficialUpdateProgress,
|
OfficialServerInfoSource, OfficialTextUnitQuery, OfficialUpdateConfig, OfficialUpdateProgress,
|
||||||
OfficialUpdateReport, OfficialUpdateService, OfficialUpdateSnapshot, OfficialUpdateStatus,
|
OfficialUpdateReport, OfficialUpdateService, OfficialUpdateSnapshot, OfficialUpdateStatus,
|
||||||
OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState, PatchApplyKind,
|
OfficialVerificationSummary, OfficialVersionRecord, OfficialVersionState, PatchApplyKind,
|
||||||
PatchApplyParams, PatchApplyReport, SqliteResourceRepository, UnityFsFieldPatchParams,
|
PatchApplyParams, PatchApplyReport, ReleaseFlowStatusCode, SqliteResourceRepository,
|
||||||
UnityFsPatchReport, UnityFsStringFieldPatchParams, UnityFsTextAssetPatchParams,
|
UnityFsFieldPatchParams, UnityFsPatchReport, UnityFsStringFieldPatchParams,
|
||||||
LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE, LOCALIZED_VERSIONS_DIR,
|
UnityFsTextAssetPatchParams, LOCALIZED_CURRENT_LINK, LOCALIZED_PATCH_MANIFEST_FILE,
|
||||||
LOCALIZED_VERSION_STATE_FILE, OFFICIAL_PARSE_CACHE_FILE, OFFICIAL_TEXTUNIT_INDEX_FILE,
|
LOCALIZED_VERSIONS_DIR, LOCALIZED_VERSION_STATE_FILE, OFFICIAL_PARSE_CACHE_FILE,
|
||||||
PRIVATE_FILE_MODE,
|
OFFICIAL_TEXTUNIT_INDEX_FILE, PRIVATE_FILE_MODE,
|
||||||
};
|
};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -649,6 +649,8 @@ struct DaemonStatusFile {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
current_stage: Option<String>,
|
current_stage: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
status_code: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
current_message: Option<String>,
|
current_message: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
download_progress: Option<DaemonDownloadProgress>,
|
download_progress: Option<DaemonDownloadProgress>,
|
||||||
@@ -2450,9 +2452,55 @@ fn read_daemon_resource_state(
|
|||||||
Ok((status_file, version_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> {
|
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 (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!({
|
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
|
"resource_output_root": status_file
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|status| status.resource_output_root.clone()),
|
.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());
|
.and_then(|state| state.current_completed_version.as_ref());
|
||||||
let snapshot = current.and_then(|record| read_snapshot(&record.snapshot_path).ok().flatten());
|
let snapshot = current.and_then(|record| read_snapshot(&record.snapshot_path).ok().flatten());
|
||||||
let (Some(record), Some(snapshot)) = (current, snapshot) else {
|
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
|
let official_seed_hash_marker_count = snapshot
|
||||||
.endpoint_markers
|
.endpoint_markers
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|marker| marker.role == OfficialEndpointMarkerRole::OfficialSeedHash)
|
.filter(|marker| marker.role == OfficialEndpointMarkerRole::OfficialSeedHash)
|
||||||
.count();
|
.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!({
|
Ok(serde_json::json!({
|
||||||
"available": true,
|
"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": {
|
"version": {
|
||||||
"id": record.id,
|
"id": record.id,
|
||||||
"completed_unix_seconds": record.completed_unix_seconds,
|
"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()
|
.as_ref()
|
||||||
.and_then(|state| state.current_completed_version.as_ref());
|
.and_then(|state| state.current_completed_version.as_ref());
|
||||||
let Some(record) = current else {
|
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 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)?
|
let Some(cache) = read_parse_cache_at(&record.resource_root).map_err(anyhow::Error::msg)?
|
||||||
else {
|
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!({
|
return Ok(serde_json::json!({
|
||||||
"available": false,
|
"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,
|
"current_version_id": record.id,
|
||||||
"resource_root": record.resource_root,
|
"resource_root": record.resource_root,
|
||||||
"cache_path": cache_path,
|
"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_path = record.resource_root.join(OFFICIAL_TEXTUNIT_INDEX_FILE);
|
||||||
let textunit_index =
|
let textunit_index =
|
||||||
read_textunit_index_at(&record.resource_root).map_err(anyhow::Error::msg)?;
|
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!({
|
Ok(serde_json::json!({
|
||||||
"available": true,
|
"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,
|
"current_version_id": record.id,
|
||||||
"resource_root": record.resource_root,
|
"resource_root": record.resource_root,
|
||||||
"cache_path": cache_path,
|
"cache_path": cache_path,
|
||||||
@@ -2880,11 +2999,19 @@ fn build_localized_status_report(
|
|||||||
let mut patch_file_count = None;
|
let mut patch_file_count = None;
|
||||||
let mut patch_text_asset_operation_count = None;
|
let mut patch_text_asset_operation_count = None;
|
||||||
let mut rollback_previous_current_target = 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() {
|
if let Some(localized_state) = state.as_ref() {
|
||||||
matches_current_official_release = official_version_id
|
matches_current_official_release = official_version_id
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.is_some_and(|id| localized_state.official_release_id == id);
|
.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 localized_state.status == "localized" && matches_current_official_release {
|
||||||
if let Some(release_id) = localized_state.current_release_id.as_deref() {
|
if let Some(release_id) = localized_state.current_release_id.as_deref() {
|
||||||
let candidate = localized_root.join(LOCALIZED_VERSIONS_DIR).join(release_id);
|
let candidate = localized_root.join(LOCALIZED_VERSIONS_DIR).join(release_id);
|
||||||
@@ -2907,15 +3034,22 @@ fn build_localized_status_report(
|
|||||||
&& patch_manifest_matches_release
|
&& patch_manifest_matches_release
|
||||||
{
|
{
|
||||||
status = "localized";
|
status = "localized";
|
||||||
|
flow_status_code = ReleaseFlowStatusCode::LocalizedPublished;
|
||||||
published_version_path = Some(candidate);
|
published_version_path = Some(candidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let (_, status_code, status_phase, status_terminal, status_retryable) =
|
||||||
|
flow_status_fields(flow_status_code);
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"available": state.is_some(),
|
"available": state.is_some(),
|
||||||
"status": status,
|
"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,
|
"official_current_version_id": official_version_id,
|
||||||
"localized_output_root": localized_root,
|
"localized_output_root": localized_root,
|
||||||
"state_path": state_path,
|
"state_path": state_path,
|
||||||
@@ -3314,6 +3448,7 @@ struct DaemonStatusReport {
|
|||||||
last_error: Option<String>,
|
last_error: Option<String>,
|
||||||
next_retry_seconds: Option<u64>,
|
next_retry_seconds: Option<u64>,
|
||||||
current_stage: Option<String>,
|
current_stage: Option<String>,
|
||||||
|
status_code: Option<String>,
|
||||||
current_message: Option<String>,
|
current_message: Option<String>,
|
||||||
download_progress: Option<DaemonDownloadProgress>,
|
download_progress: Option<DaemonDownloadProgress>,
|
||||||
version_state_path: Option<PathBuf>,
|
version_state_path: Option<PathBuf>,
|
||||||
@@ -3462,6 +3597,7 @@ fn start_daemon_with_args(
|
|||||||
last_error: None,
|
last_error: None,
|
||||||
next_retry_seconds: None,
|
next_retry_seconds: None,
|
||||||
current_stage: None,
|
current_stage: None,
|
||||||
|
status_code: None,
|
||||||
current_message: None,
|
current_message: None,
|
||||||
download_progress: None,
|
download_progress: None,
|
||||||
pending_scheduled_force: false,
|
pending_scheduled_force: false,
|
||||||
@@ -3691,6 +3827,9 @@ fn build_daemon_status_report(state_dir: &Path) -> anyhow::Result<DaemonStatusRe
|
|||||||
current_stage: status_file
|
current_stage: status_file
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|status| status.current_stage.clone()),
|
.and_then(|status| status.current_stage.clone()),
|
||||||
|
status_code: status_file
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|status| status.status_code.clone()),
|
||||||
current_message: status_file
|
current_message: status_file
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|status| status.current_message.clone()),
|
.and_then(|status| status.current_message.clone()),
|
||||||
@@ -4702,6 +4841,7 @@ impl HumanReport for OfficialUpdateReport {
|
|||||||
fn print_human(&self) -> anyhow::Result<()> {
|
fn print_human(&self) -> anyhow::Result<()> {
|
||||||
print_title("官方资源同步");
|
print_title("官方资源同步");
|
||||||
print_field("状态", self.update_status.as_str());
|
print_field("状态", self.update_status.as_str());
|
||||||
|
print_field("状态码", self.status_code.as_str());
|
||||||
print_field("应用版本", &self.app_version);
|
print_field("应用版本", &self.app_version);
|
||||||
print_optional_field("Bundle 版本", self.bundle_version.as_deref());
|
print_optional_field("Bundle 版本", self.bundle_version.as_deref());
|
||||||
print_field("连接组", &self.connection_group);
|
print_field("连接组", &self.connection_group);
|
||||||
@@ -5692,6 +5832,7 @@ fn update_daemon_status(state_dir: &Path, update: DaemonStatusUpdate<'_>) -> any
|
|||||||
last_error: None,
|
last_error: None,
|
||||||
next_retry_seconds: None,
|
next_retry_seconds: None,
|
||||||
current_stage: None,
|
current_stage: None,
|
||||||
|
status_code: None,
|
||||||
current_message: None,
|
current_message: None,
|
||||||
download_progress: None,
|
download_progress: None,
|
||||||
pending_scheduled_force: false,
|
pending_scheduled_force: false,
|
||||||
@@ -5701,6 +5842,21 @@ fn update_daemon_status(state_dir: &Path, update: DaemonStatusUpdate<'_>) -> any
|
|||||||
status.pid = std::process::id();
|
status.pid = std::process::id();
|
||||||
status.state = update.state.to_string();
|
status.state = update.state.to_string();
|
||||||
status.updated_unix_seconds = unix_seconds_now();
|
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_update_status = update.last_update_status;
|
||||||
status.last_error = update.last_error;
|
status.last_error = update.last_error;
|
||||||
status.next_retry_seconds = update.next_retry_seconds;
|
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.pid = std::process::id();
|
||||||
status.updated_unix_seconds = unix_seconds_now();
|
status.updated_unix_seconds = unix_seconds_now();
|
||||||
status.current_stage = Some(event.stage.to_string());
|
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.current_message = Some(event.message.clone());
|
||||||
status.download_progress = match (event.download_index, event.download_total) {
|
status.download_progress = match (event.download_index, event.download_total) {
|
||||||
(Some(index), Some(total)) => Some(DaemonDownloadProgress {
|
(Some(index), Some(total)) => Some(DaemonDownloadProgress {
|
||||||
@@ -6269,6 +6426,8 @@ impl RotatingStructuredLogger {
|
|||||||
"level": "info",
|
"level": "info",
|
||||||
"stage": event.stage,
|
"stage": event.stage,
|
||||||
"stage_label": localized_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(),
|
"message": event.message.as_str(),
|
||||||
"download": event.download_index.map(|index| serde_json::json!({
|
"download": event.download_index.map(|index| serde_json::json!({
|
||||||
"index": index,
|
"index": index,
|
||||||
@@ -9788,6 +9947,7 @@ mod tests {
|
|||||||
last_error: None,
|
last_error: None,
|
||||||
next_retry_seconds: Some(60),
|
next_retry_seconds: Some(60),
|
||||||
current_stage: None,
|
current_stage: None,
|
||||||
|
status_code: Some(ReleaseFlowStatusCode::OfficialUpToDate.as_str().to_string()),
|
||||||
current_message: None,
|
current_message: None,
|
||||||
download_progress: None,
|
download_progress: None,
|
||||||
pending_scheduled_force: false,
|
pending_scheduled_force: false,
|
||||||
@@ -9860,6 +10020,29 @@ mod tests {
|
|||||||
current_dir
|
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]
|
#[test]
|
||||||
fn dispatch_catalog_status_unavailable_without_state() {
|
fn dispatch_catalog_status_unavailable_without_state() {
|
||||||
let temp = tempfile::TempDir::new().unwrap();
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
@@ -9873,6 +10056,12 @@ mod tests {
|
|||||||
let value = serde_json::to_value(&envelope).unwrap();
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
assert_eq!(value["ok"], true);
|
assert_eq!(value["ok"], true);
|
||||||
assert_eq!(value["data"]["available"], false);
|
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]
|
#[test]
|
||||||
@@ -9893,6 +10082,12 @@ mod tests {
|
|||||||
assert_eq!(value["ok"], true);
|
assert_eq!(value["ok"], true);
|
||||||
assert_eq!(value["data"]["available"], true);
|
assert_eq!(value["data"]["available"], true);
|
||||||
assert_eq!(value["data"]["bundle_version"], "bundle-b2");
|
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"]["version"]["id"], "v-current");
|
||||||
assert_eq!(value["data"]["endpoint_count"], 0);
|
assert_eq!(value["data"]["endpoint_count"], 0);
|
||||||
assert_eq!(value["data"]["connection_group_name"], "Prod-Audit");
|
assert_eq!(value["data"]["connection_group_name"], "Prod-Audit");
|
||||||
@@ -9938,6 +10133,12 @@ mod tests {
|
|||||||
let value = serde_json::to_value(&envelope).unwrap();
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
assert_eq!(value["ok"], true);
|
assert_eq!(value["ok"], true);
|
||||||
assert_eq!(value["data"]["available"], 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"]["current_version_id"], "v-current");
|
||||||
assert_eq!(value["data"]["previous_version_id"], "v-previous");
|
assert_eq!(value["data"]["previous_version_id"], "v-previous");
|
||||||
assert_eq!(value["data"]["previous_snapshot_missing"], false);
|
assert_eq!(value["data"]["previous_snapshot_missing"], false);
|
||||||
@@ -10475,6 +10676,12 @@ mod tests {
|
|||||||
let value = serde_json::to_value(&envelope).unwrap();
|
let value = serde_json::to_value(&envelope).unwrap();
|
||||||
assert_eq!(value["ok"], true);
|
assert_eq!(value["ok"], true);
|
||||||
assert_eq!(value["data"]["available"], false);
|
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");
|
assert_eq!(value["data"]["current_version_id"], "v-current");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10543,6 +10750,7 @@ mod tests {
|
|||||||
assert_eq!(value["ok"], true);
|
assert_eq!(value["ok"], true);
|
||||||
assert_eq!(value["data"]["available"], true);
|
assert_eq!(value["data"]["available"], true);
|
||||||
assert_eq!(value["data"]["status"], "localized");
|
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"]["official_current_version_id"], "v-current");
|
||||||
assert_eq!(value["data"]["matches_current_official_release"], true);
|
assert_eq!(value["data"]["matches_current_official_release"], true);
|
||||||
assert_eq!(value["data"]["current_points_to_published_version"], 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["ok"], true);
|
||||||
assert_eq!(value["data"]["available"], true);
|
assert_eq!(value["data"]["available"], true);
|
||||||
assert_eq!(value["data"]["status"], "not_localized");
|
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"]["matches_current_official_release"], false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value["data"]["published_version_path"],
|
value["data"]["published_version_path"],
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub mod official_textunit_queue;
|
|||||||
pub mod official_update;
|
pub mod official_update;
|
||||||
pub mod patch_ops;
|
pub mod patch_ops;
|
||||||
pub mod path_security;
|
pub mod path_security;
|
||||||
|
pub mod release_flow;
|
||||||
pub mod resources;
|
pub mod resources;
|
||||||
mod zip_validation;
|
mod zip_validation;
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@ pub use path_security::{
|
|||||||
open_append_file, read_file_no_symlink, set_file_mode, validate_output_root,
|
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,
|
validate_runtime_state_dir, write_file_atomic, PRIVATE_FILE_MODE, STATE_FILE_MODE,
|
||||||
};
|
};
|
||||||
|
pub use release_flow::ReleaseFlowStatusCode;
|
||||||
pub use resources::{InMemoryResourceRepository, SqliteResourceRepository};
|
pub use resources::{InMemoryResourceRepository, SqliteResourceRepository};
|
||||||
|
|
||||||
/// Infrastructure 版本号
|
/// Infrastructure 版本号
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use crate::path_security::{
|
|||||||
ensure_path_within_root, ensure_safe_directory_path, ensure_safe_file_target, lexical_absolute,
|
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,
|
read_file_no_symlink, validate_output_root, write_file_atomic, STATE_FILE_MODE,
|
||||||
};
|
};
|
||||||
|
use crate::release_flow::ReleaseFlowStatusCode;
|
||||||
use crate::{
|
use crate::{
|
||||||
build_official_pull_plan_for_platform_inventory, build_official_sync_plan,
|
build_official_pull_plan_for_platform_inventory, build_official_sync_plan,
|
||||||
changed_endpoint_urls, default_official_platforms, DownloadError,
|
changed_endpoint_urls, default_official_platforms, DownloadError,
|
||||||
@@ -227,6 +228,16 @@ impl OfficialUpdateStatus {
|
|||||||
Self::Downloaded => "downloaded",
|
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.
|
/// Publication state for localized resources associated with an official release.
|
||||||
@@ -247,6 +258,15 @@ impl LocalizedReleaseStatus {
|
|||||||
Self::Localized => "localized",
|
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.
|
/// Snapshot of the official JP update state observed at a point in time.
|
||||||
@@ -712,6 +732,8 @@ impl OfficialVerificationSummary {
|
|||||||
pub struct OfficialUpdateReport {
|
pub struct OfficialUpdateReport {
|
||||||
/// Final update status.
|
/// Final update status.
|
||||||
pub update_status: OfficialUpdateStatus,
|
pub update_status: OfficialUpdateStatus,
|
||||||
|
/// Stable cross-module flow status code.
|
||||||
|
pub status_code: ReleaseFlowStatusCode,
|
||||||
/// Selected connection group.
|
/// Selected connection group.
|
||||||
pub connection_group: String,
|
pub connection_group: String,
|
||||||
/// Selected app version.
|
/// Selected app version.
|
||||||
@@ -842,6 +864,8 @@ pub struct OfficialUpdateReport {
|
|||||||
pub struct OfficialUpdateProgress {
|
pub struct OfficialUpdateProgress {
|
||||||
/// Stable progress stage label.
|
/// Stable progress stage label.
|
||||||
pub stage: &'static str,
|
pub stage: &'static str,
|
||||||
|
/// Stable cross-module flow status code derived from `stage`.
|
||||||
|
pub status_code: ReleaseFlowStatusCode,
|
||||||
/// Human-readable status line.
|
/// Human-readable status line.
|
||||||
pub message: String,
|
pub message: String,
|
||||||
/// One-based download index when the event represents URL download work.
|
/// 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 {
|
pub fn new(stage: &'static str, message: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
stage,
|
stage,
|
||||||
|
status_code: ReleaseFlowStatusCode::from_progress_stage(stage),
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
download_index: None,
|
download_index: None,
|
||||||
download_total: None,
|
download_total: None,
|
||||||
@@ -1579,6 +1604,11 @@ impl OfficialUpdateService {
|
|||||||
} else {
|
} else {
|
||||||
OfficialUpdateStatus::UpToDate
|
OfficialUpdateStatus::UpToDate
|
||||||
},
|
},
|
||||||
|
status_code: if should_download {
|
||||||
|
ReleaseFlowStatusCode::OfficialUpdateAvailable
|
||||||
|
} else {
|
||||||
|
ReleaseFlowStatusCode::OfficialUpToDate
|
||||||
|
},
|
||||||
connection_group: current_snapshot.connection_group_name.clone(),
|
connection_group: current_snapshot.connection_group_name.clone(),
|
||||||
app_version: current_snapshot.app_version.clone(),
|
app_version: current_snapshot.app_version.clone(),
|
||||||
bundle_version: current_snapshot.bundle_version.clone(),
|
bundle_version: current_snapshot.bundle_version.clone(),
|
||||||
@@ -1942,6 +1972,7 @@ impl OfficialUpdateService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
report.update_status = OfficialUpdateStatus::Downloaded;
|
report.update_status = OfficialUpdateStatus::Downloaded;
|
||||||
|
report.status_code = report.update_status.flow_status_code();
|
||||||
report.active_resource_root = published_version_path.clone();
|
report.active_resource_root = published_version_path.clone();
|
||||||
report.published_version_path = Some(published_version_path.clone());
|
report.published_version_path = Some(published_version_path.clone());
|
||||||
report.snapshot_path = final_snapshot_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()));
|
let localized_info = localized_release_info_for(config, Some(active_release_id.as_str()));
|
||||||
OfficialUpdateReport {
|
OfficialUpdateReport {
|
||||||
update_status: OfficialUpdateStatus::WaitingForOfficialResources,
|
update_status: OfficialUpdateStatus::WaitingForOfficialResources,
|
||||||
|
status_code: ReleaseFlowStatusCode::OfficialWaitingForResources,
|
||||||
connection_group: base_snapshot.connection_group_name.clone(),
|
connection_group: base_snapshot.connection_group_name.clone(),
|
||||||
app_version: base_snapshot.app_version.clone(),
|
app_version: base_snapshot.app_version.clone(),
|
||||||
bundle_version: base_snapshot.bundle_version.clone(),
|
bundle_version: base_snapshot.bundle_version.clone(),
|
||||||
|
|||||||
@@ -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('.')));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -558,11 +558,15 @@ func TestDiscoverCallsStatusBeforeDoctor(t *testing.T) {
|
|||||||
b := uint64(info.Size())
|
b := uint64(info.Size())
|
||||||
_ = bytes
|
_ = bytes
|
||||||
catalogObj := map[string]any{
|
catalogObj := map[string]any{
|
||||||
"available": true,
|
"available": true,
|
||||||
"app_version": "1.70.0",
|
"status": "published",
|
||||||
"bundle_version": "s8tloc7lo3",
|
"status_code": "official.published",
|
||||||
"connection_group_name": "Prod",
|
"distribution_status": "ready",
|
||||||
"addressables_root": "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture",
|
"distribution_status_code": "distribution.ready",
|
||||||
|
"app_version": "1.70.0",
|
||||||
|
"bundle_version": "s8tloc7lo3",
|
||||||
|
"connection_group_name": "Prod",
|
||||||
|
"addressables_root": "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture",
|
||||||
"version": map[string]any{
|
"version": map[string]any{
|
||||||
"id": "v1",
|
"id": "v1",
|
||||||
"resource_root": root,
|
"resource_root": root,
|
||||||
@@ -603,6 +607,13 @@ func TestDiscoverCallsStatusBeforeDoctor(t *testing.T) {
|
|||||||
if result.DoctorHealthy == nil || !*result.DoctorHealthy {
|
if result.DoctorHealthy == nil || !*result.DoctorHealthy {
|
||||||
t.Fatal("doctor healthy expected")
|
t.Fatal("doctor healthy expected")
|
||||||
}
|
}
|
||||||
|
summary := result.Index.Summary()
|
||||||
|
if summary.Snapshot.Status != "published" || summary.Snapshot.StatusCode != "official.published" {
|
||||||
|
t.Fatalf("snapshot status=%q code=%q", summary.Snapshot.Status, summary.Snapshot.StatusCode)
|
||||||
|
}
|
||||||
|
if summary.Snapshot.DistributionStatusCode != "distribution.ready" {
|
||||||
|
t.Fatalf("distribution status code=%q", summary.Snapshot.DistributionStatusCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadEnvFileDoesNotOverride(t *testing.T) {
|
func TestLoadEnvFileDoesNotOverride(t *testing.T) {
|
||||||
|
|||||||
@@ -22,14 +22,18 @@ type ResourceEntry struct {
|
|||||||
|
|
||||||
// SnapshotSummary is a subset of official-sync-snapshot.json / catalog.status.
|
// SnapshotSummary is a subset of official-sync-snapshot.json / catalog.status.
|
||||||
type SnapshotSummary struct {
|
type SnapshotSummary struct {
|
||||||
AppVersion string `json:"app_version,omitempty"`
|
Status string `json:"status,omitempty"`
|
||||||
BundleVersion string `json:"bundle_version,omitempty"`
|
StatusCode string `json:"status_code,omitempty"`
|
||||||
ConnectionGroupName string `json:"connection_group_name,omitempty"`
|
DistributionStatus string `json:"distribution_status,omitempty"`
|
||||||
AddressablesRoot string `json:"addressables_root,omitempty"`
|
DistributionStatusCode string `json:"distribution_status_code,omitempty"`
|
||||||
VersionID string `json:"version_id,omitempty"`
|
AppVersion string `json:"app_version,omitempty"`
|
||||||
CompletedUnixSeconds *uint64 `json:"completed_unix_seconds,omitempty"`
|
BundleVersion string `json:"bundle_version,omitempty"`
|
||||||
LauncherMetadata *LauncherMetadataSummary `json:"launcher_metadata,omitempty"`
|
ConnectionGroupName string `json:"connection_group_name,omitempty"`
|
||||||
GameMainConfig *GameMainConfigSummary `json:"game_main_config,omitempty"`
|
AddressablesRoot string `json:"addressables_root,omitempty"`
|
||||||
|
VersionID string `json:"version_id,omitempty"`
|
||||||
|
CompletedUnixSeconds *uint64 `json:"completed_unix_seconds,omitempty"`
|
||||||
|
LauncherMetadata *LauncherMetadataSummary `json:"launcher_metadata,omitempty"`
|
||||||
|
GameMainConfig *GameMainConfigSummary `json:"game_main_config,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LauncherMetadataSummary mirrors the resource-relevant part of Rust's launcher metadata snapshot.
|
// LauncherMetadataSummary mirrors the resource-relevant part of Rust's launcher metadata snapshot.
|
||||||
|
|||||||
+22
-14
@@ -195,14 +195,18 @@ func parseCatalogStatus(raw json.RawMessage) (*SnapshotSummary, string, bool) {
|
|||||||
return nil, "", false
|
return nil, "", false
|
||||||
}
|
}
|
||||||
var payload struct {
|
var payload struct {
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
AppVersion string `json:"app_version"`
|
Status string `json:"status"`
|
||||||
BundleVersion string `json:"bundle_version"`
|
StatusCode string `json:"status_code"`
|
||||||
ConnectionGroupName string `json:"connection_group_name"`
|
DistributionStatus string `json:"distribution_status"`
|
||||||
AddressablesRoot string `json:"addressables_root"`
|
DistributionStatusCode string `json:"distribution_status_code"`
|
||||||
LauncherMetadata *LauncherMetadataSummary `json:"launcher_metadata"`
|
AppVersion string `json:"app_version"`
|
||||||
GameMainConfig *GameMainConfigSummary `json:"game_main_config"`
|
BundleVersion string `json:"bundle_version"`
|
||||||
Version *struct {
|
ConnectionGroupName string `json:"connection_group_name"`
|
||||||
|
AddressablesRoot string `json:"addressables_root"`
|
||||||
|
LauncherMetadata *LauncherMetadataSummary `json:"launcher_metadata"`
|
||||||
|
GameMainConfig *GameMainConfigSummary `json:"game_main_config"`
|
||||||
|
Version *struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
CompletedUnixSeconds *uint64 `json:"completed_unix_seconds"`
|
CompletedUnixSeconds *uint64 `json:"completed_unix_seconds"`
|
||||||
ResourceRoot string `json:"resource_root"`
|
ResourceRoot string `json:"resource_root"`
|
||||||
@@ -212,12 +216,16 @@ func parseCatalogStatus(raw json.RawMessage) (*SnapshotSummary, string, bool) {
|
|||||||
return nil, "", false
|
return nil, "", false
|
||||||
}
|
}
|
||||||
snap := &SnapshotSummary{
|
snap := &SnapshotSummary{
|
||||||
AppVersion: payload.AppVersion,
|
Status: payload.Status,
|
||||||
BundleVersion: payload.BundleVersion,
|
StatusCode: payload.StatusCode,
|
||||||
ConnectionGroupName: payload.ConnectionGroupName,
|
DistributionStatus: payload.DistributionStatus,
|
||||||
AddressablesRoot: payload.AddressablesRoot,
|
DistributionStatusCode: payload.DistributionStatusCode,
|
||||||
LauncherMetadata: payload.LauncherMetadata,
|
AppVersion: payload.AppVersion,
|
||||||
GameMainConfig: payload.GameMainConfig,
|
BundleVersion: payload.BundleVersion,
|
||||||
|
ConnectionGroupName: payload.ConnectionGroupName,
|
||||||
|
AddressablesRoot: payload.AddressablesRoot,
|
||||||
|
LauncherMetadata: payload.LauncherMetadata,
|
||||||
|
GameMainConfig: payload.GameMainConfig,
|
||||||
}
|
}
|
||||||
root := ""
|
root := ""
|
||||||
if payload.Version != nil {
|
if payload.Version != nil {
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ type LogsReport struct {
|
|||||||
|
|
||||||
type DaemonStatusReport struct {
|
type DaemonStatusReport struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
StatusCode *string `json:"status_code,omitempty"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Running bool `json:"running"`
|
Running bool `json:"running"`
|
||||||
PID *int `json:"pid,omitempty"`
|
PID *int `json:"pid,omitempty"`
|
||||||
@@ -277,6 +278,11 @@ type DaemonStatusReport struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ResourceState struct {
|
type ResourceState struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
StatusCode string `json:"status_code,omitempty"`
|
||||||
|
StatusPhase string `json:"status_phase,omitempty"`
|
||||||
|
StatusTerminal bool `json:"status_terminal,omitempty"`
|
||||||
|
StatusRetryable bool `json:"status_retryable,omitempty"`
|
||||||
ResourceOutputRoot *string `json:"resource_output_root,omitempty"`
|
ResourceOutputRoot *string `json:"resource_output_root,omitempty"`
|
||||||
VersionState json.RawMessage `json:"version_state,omitempty"`
|
VersionState json.RawMessage `json:"version_state,omitempty"`
|
||||||
LastUpdateStatus *string `json:"last_update_status,omitempty"`
|
LastUpdateStatus *string `json:"last_update_status,omitempty"`
|
||||||
|
|||||||
Reference in New Issue
Block a user