Skip to content

Commit cdb684e

Browse files
authored
Merge pull request #2122 from Websoft9/feature/appstore-publish-migration
fix
2 parents 2af4b23 + 4148437 commit cdb684e

2 files changed

Lines changed: 30 additions & 83 deletions

File tree

build/library_publish.py

Lines changed: 11 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -121,23 +121,7 @@ def v2_full_package_names(channel: str, dataset_version: str) -> tuple[str, str]
121121
return (f"{V2_FULL_PACKAGE_BASENAME}-{dataset_version}.zip", f"{V2_FULL_PACKAGE_BASENAME}-{channel}.zip")
122122

123123

124-
def _app_version_from_hash(app_hash: str) -> str:
125-
"""Derive a stable, content-addressed version identifier from an app's fingerprint hash.
126-
127-
Using the content hash means the same app content always produces the same
128-
versioned filename, so unchanged apps never need to be rebuilt or re-uploaded.
129-
"""
130-
return app_hash[:16]
131-
132-
133-
def v2_app_package_names(app_version: str) -> tuple[str, str]:
134-
"""Return (versioned_zip_name, latest_zip_name) for a single app.
135-
136-
``app_version`` should be a per-app content-derived identifier, not the
137-
global publish timestamp, so that only actually-changed apps get new
138-
versioned packages.
139-
"""
140-
return (f"{app_version}.zip", "latest.zip")
124+
APP_PACKAGE_NAME = "latest.zip"
141125

142126

143127
def sha256_file(path: Path) -> str:
@@ -197,30 +181,19 @@ def current_app_fingerprint(app_dir: Path) -> str:
197181
return digest.hexdigest()
198182

199183

200-
def build_app_package_entry(app_name: str, app_version: str) -> dict:
201-
versioned_name, latest_name = v2_app_package_names(app_version)
202-
app_base = f"apps/{app_name}"
203-
return {
204-
"versioned": f"{app_base}/{versioned_name}",
205-
"latest": f"{app_base}/{latest_name}",
206-
}
184+
def build_app_package_entry(app_name: str) -> dict:
185+
return {"latest": f"apps/{app_name}/{APP_PACKAGE_NAME}"}
207186

208187

209-
def build_app_checksum_entry(app_name: str, app_version: str) -> dict:
210-
package_entry = build_app_package_entry(app_name, app_version)
211-
return {
212-
"versioned": f"{package_entry['versioned']}.sha256",
213-
"latest": f"{package_entry['latest']}.sha256",
214-
}
188+
def build_app_checksum_entry(app_name: str) -> dict:
189+
return {"latest": f"apps/{app_name}/{APP_PACKAGE_NAME}.sha256"}
215190

216191

217192
def build_apps_index(dataset_version: str, channel: str, generated_at: str) -> dict:
218193
apps = []
219194
for app_dir in sorted(path for path in APPS_DIR.iterdir() if path.is_dir()):
220195
variables = load_variables_json(app_dir)
221196
app_name = app_dir.name
222-
app_hash = current_app_fingerprint(app_dir)
223-
app_version = _app_version_from_hash(app_hash)
224197
apps.append(
225198
{
226199
"app": app_name,
@@ -229,9 +202,9 @@ def build_apps_index(dataset_version: str, channel: str, generated_at: str) -> d
229202
"release": variables.get("release"),
230203
"versions": summarize_versions(variables.get("edition", [])),
231204
"path": f"apps/{app_name}",
232-
"hash": app_hash,
233-
"package": build_app_package_entry(app_name, app_version),
234-
"checksum": build_app_checksum_entry(app_name, app_version),
205+
"hash": current_app_fingerprint(app_dir),
206+
"package": build_app_package_entry(app_name),
207+
"checksum": build_app_checksum_entry(app_name),
235208
}
236209
)
237210

@@ -450,7 +423,7 @@ def validate_library_artifacts(output_dir: Path, manifest: dict, changed_app_nam
450423
continue
451424
package = entry.get("package") or {}
452425
checksum = entry.get("checksum") or {}
453-
for path in (package.get("versioned"), package.get("latest"), checksum.get("versioned"), checksum.get("latest")):
426+
for path in (package.get("latest"), checksum.get("latest")):
454427
if not path or not (output_dir / path).exists():
455428
raise SystemExit(f"missing app package artifact: {entry.get('app')} -> {path}")
456429

@@ -610,25 +583,16 @@ def build_v2_appstore_artifacts(
610583
if app_name not in changed_app_names:
611584
continue
612585

613-
# Look up the app's content-derived version identifier from the index.
614-
app_entry = next((e for e in apps_index["apps"] if e["app"] == app_name), None)
615-
if app_entry is None:
616-
continue
617-
app_version = _app_version_from_hash(app_entry["hash"])
618-
app_versioned_name, app_latest_name = v2_app_package_names(app_version)
619-
620586
app_output_dir = apps_packages_dir / app_name
621587
app_output_dir.mkdir(parents=True, exist_ok=True)
622588

623589
with tempfile.TemporaryDirectory() as tmp_dir_name:
624590
tmp_dir = Path(tmp_dir_name)
625591
app_package_root = tmp_dir / app_name
626592
shutil.copytree(app_dir, app_package_root)
627-
create_zip_from_directory(app_package_root, app_output_dir / app_versioned_name)
593+
create_zip_from_directory(app_package_root, app_output_dir / APP_PACKAGE_NAME)
628594

629-
shutil.copy2(app_output_dir / app_versioned_name, app_output_dir / app_latest_name)
630-
write_checksum_file(app_output_dir / app_versioned_name)
631-
write_checksum_file(app_output_dir / app_latest_name)
595+
write_checksum_file(app_output_dir / APP_PACKAGE_NAME)
632596

633597
# ── library – write index, delta, manifest ───────────────
634598
write_json(library_dir / apps_index_name, apps_index)

docs/devops.md

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -388,48 +388,34 @@ v2 workflow 的职责应为:
388388
3. `artifact/websoft9/v2/<channel>/appstore/library/apps-delta-<fromVersion>-to-<toVersion>.json`
389389
4. `artifact/websoft9/v2/<channel>/appstore/library/full/library-<datasetVersion>.zip`
390390
5. `artifact/websoft9/v2/<channel>/appstore/library/full/library-<channel>.zip`
391-
6. `artifact/websoft9/v2/<channel>/appstore/library/apps/<app>/<appVersion>.zip`
392-
7. `artifact/websoft9/v2/<channel>/appstore/library/apps/<app>/latest.zip`
391+
6. `artifact/websoft9/v2/<channel>/appstore/library/apps/<app>/latest.zip`
392+
7. `artifact/websoft9/v2/<channel>/appstore/library/apps/<app>/latest.zip.sha256`
393393

394394
说明:
395395

396396
1. `full/library-<datasetVersion>.zip` 是不可变的全量快照包,`<datasetVersion>` 使用全局发布时间戳
397397
2. `full/library-<channel>.zip` 是该通道下的全量最新别名,可被覆盖
398-
3. `apps/<app>/<appVersion>.zip` 是不可变的单 app 快照包,**`<appVersion>` 使用该 app 内容指纹哈希的前 16 位十六进制字符,而非全局发布时间戳**
399-
4. `apps/<app>/latest.zip` 是该 app 在该通道下的最新别名,可被覆盖
400-
5. `latest` 类对象只承担最新指针语义,不承担审计与回滚语义
401-
6. 真正的审计、回滚与复现必须依赖版本化对象
402-
403-
**`<appVersion>` 设计说明:**
404-
405-
单 app 包的版本标识采用内容寻址(content-addressed)策略:
406-
407-
- `<appVersion>` = SHA-256(`apps/<app>/` 下所有文件的内容 + 相对路径) 的前 16 位十六进制字符
408-
- 同一 app、同一内容 → 同一 `<appVersion>` → 文件名不变
409-
- 这确保了 **只有内容实际变更的 app 才产生新的版本化包**,未变更的 app 的已有版本化包无需重建、无需重新上传
410-
411-
此字段在 `apps-index``hash` 字段中以完整 SHA-256 形式存储,`<appVersion>` 是其截断派生值。
398+
3. `apps/<app>/latest.zip` 是该 app 在该通道下的唯一安装包,每次 app 内容变更时覆盖更新
399+
4. R2 上的历史审计与回滚能力由 git 仓库保证,R2 仅作为分发缓存
400+
5. 客户端通过 `apps-index` 中的 `hash` 字段判断 app 是否变更,无需版本化文件名
412401

413402
### 10.2 单 App 包构建优化
414403

415-
为避免每次发布都重建全部 300+ 个 app 的单 app 包,构建流程采用按需构建策略:
416-
417-
1. **首次发布**(无历史 `from_ref`):为所有 app 构建单 app 包(全量冷启动)
418-
2. **增量发布**(存在历史 `from_ref`):****`apps-delta` 中的 `addedApps``changedApps` 构建单 app 包
419-
3. 未变更 app 的单 app 包不重新构建、不重新上传——R2 上已有文件保持不变
404+
每个 app 只保留一个 `latest.zip`,构建流程采用按需覆盖策略:
420405

421-
此优化依赖两个前提:
406+
1. **首次发布**(R2 上 `apps/` 为空):为所有 app 构建 `latest.zip`(全量冷启动)
407+
2. **增量发布**(R2 已播种):****`apps-delta` 中的 `addedApps``changedApps` 构建/覆盖 `latest.zip`
408+
3. 未变更 app 的 `latest.zip` 不重新构建、不触碰——R2 上已有文件保持不变
422409

423-
1. `<appVersion>` 使用内容哈希(内容不变 → 文件名不变 → 无需重建)
424-
2. R2 上传步骤采用**追加/覆盖模式**,而非同步删除模式(sync-with-delete),避免覆盖掉未变更 app 的已有包
410+
此优化依赖 R2 上传步骤采用 `aws s3 sync` 默认行为(追加/覆盖,不删除),配合 `check_seed` 步骤检测首次发布。
425411

426412
优化效果(以 300 个 app、每次变更 2 个为例):
427413

428-
| 指标 | 优化前 | 优化后 |
429-
|------|--------|--------|
414+
| 指标 | 全量构建 | 增量构建 |
415+
|------|---------|---------|
430416
| 单 app zip 打包次数 | 300 | 2 |
431417
| R2 PUT 请求数(单 app 部分) | ~600 | ~4 |
432-
| 10 次发布后 R2 版本化包存量 | ~3000 | ~320 |
418+
| R2 上单 app 文件数 | 600 | 600(无孤儿堆积) |
433419

434420
注意:全量包 `full/library-<datasetVersion>.zip` 仍然每次发布都重建(包含所有 app),作为首装与灾备的兜底路径。
435421

@@ -632,21 +618,18 @@ v2 workflow 的职责应为:
632618

633619
### 11.4 apps-index 扩展要求
634620

635-
为支持单 app 下载,`apps-index` 中每个 app 条目建议最少包含
621+
每个 app 条目最少包含
636622

637623
1. `app`
638624
2. `hash`
639-
3. `package.versioned`
640-
4. `package.latest`
641-
5. `checksum.versioned`
642-
6. `checksum.latest`
625+
3. `package.latest`
626+
4. `checksum.latest`
643627

644628
说明:
645629

646-
1. `package.versioned` 指向不可变单 app 包
647-
2. `package.latest` 指向该 app 的最新别名
648-
3. 新客户端优先读取 `package.versioned`,以获得更强一致性
649-
4. 只关心当前通道最新值的轻量客户端可读取 `package.latest`
630+
1. `package.latest` 指向该 app 的唯一安装包(`latest.zip`
631+
2. `hash` 是该 app 目录内容的完整 SHA-256 指纹,客户端通过对比此字段判断 app 是否变更
632+
3.`hash` 与本地记录不同时,客户端下载 `latest.zip` 覆盖本地
650633

651634
## 14. 校验门禁
652635

0 commit comments

Comments
 (0)