Skip to content

Commit 1e953bb

Browse files
committed
refactor(v2): simplify full package to latest-only, unify catalog and library model
- Remove versioned full packages from both catalog and library (v2 R2) - Catalog and library full/ now only contain latest.zip + .sha256 - Channel expressed by directory path only, filenames no longer carry channel semantics - Add dist/ to .gitignore to exclude build artifacts - Update release spec to reflect simplified model
1 parent b68099f commit 1e953bb

3 files changed

Lines changed: 42 additions & 55 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ mk.sh
88
.claude
99
_bmad-output
1010

11-
# Pre-downloaded app sources
11+
# Pre-downloaded app sourcesdist/

build/library_publish.py

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"rc": "library-rc.zip",
2323
"release": "library-latest.zip",
2424
}
25-
V2_FULL_PACKAGE_BASENAME = "library"
25+
V2_FULL_LATEST_NAME = "latest.zip"
2626
CATALOG_FILE_NAMES = (
2727
"catalog_en.json",
2828
"catalog_zh.json",
@@ -116,10 +116,6 @@ def summarize_versions(edition_list: list[dict]) -> list[str]:
116116
return versions
117117

118118

119-
def v2_full_package_names(channel: str, dataset_version: str) -> tuple[str, str]:
120-
return (f"{V2_FULL_PACKAGE_BASENAME}-{dataset_version}.zip", f"{V2_FULL_PACKAGE_BASENAME}-{channel}.zip")
121-
122-
123119
APP_PACKAGE_NAME = "latest.zip"
124120

125121

@@ -412,8 +408,7 @@ def validate_catalog_artifacts(output_dir: Path, manifest: dict) -> None:
412408

413409
def validate_library_artifacts(output_dir: Path, manifest: dict, changed_app_names: set[str] | None = None) -> None:
414410
for name in (
415-
manifest["fullPackage"]["versioned"],
416-
manifest["fullPackage"]["latest"],
411+
manifest["fullPackage"],
417412
manifest["appsIndex"],
418413
manifest["appsDelta"],
419414
"manifest.json",
@@ -567,16 +562,20 @@ def build_v2_appstore_artifacts(
567562
# ── catalog full package ─────────────────────────────────
568563
catalog_full_dir = catalog_dir / "full"
569564
catalog_full_dir.mkdir(parents=True, exist_ok=True)
570-
catalog_zip_name = f"catalog-{dataset_version}.zip"
565+
catalog_zip_latest_name = V2_FULL_LATEST_NAME
571566
with tempfile.TemporaryDirectory() as tmp_dir_name:
572567
tmp_dir = Path(tmp_dir_name)
573568
for file_name in CATALOG_FILE_NAMES:
574569
shutil.copy2(catalog_dir / file_name, tmp_dir / file_name)
575-
create_zip_from_directory(tmp_dir, catalog_full_dir / catalog_zip_name)
576-
catalog_zip_checksum = write_checksum_file(catalog_full_dir / catalog_zip_name)
577-
catalog_checksums["fullPackage"] = f"full/{catalog_zip_checksum}"
578-
579-
catalog_manifest = build_catalog_manifest(catalog_dsv, catalog_checksums, generated_at, f"full/{catalog_zip_name}")
570+
create_zip_from_directory(tmp_dir, catalog_full_dir / catalog_zip_latest_name)
571+
catalog_checksums["fullPackage"] = f"full/{write_checksum_file(catalog_full_dir / catalog_zip_latest_name)}"
572+
573+
catalog_manifest = build_catalog_manifest(
574+
catalog_dsv,
575+
catalog_checksums,
576+
generated_at,
577+
f"full/{catalog_zip_latest_name}",
578+
)
580579
write_json(catalog_dir / "manifest.json", catalog_manifest)
581580
write_checksum_file(catalog_dir / "manifest.json")
582581
validate_catalog_artifacts(catalog_dir, catalog_manifest)
@@ -586,20 +585,17 @@ def build_v2_appstore_artifacts(
586585
full_dir.mkdir(parents=True, exist_ok=True)
587586
apps_packages_dir.mkdir(parents=True, exist_ok=True)
588587

589-
full_versioned_name, full_latest_name = v2_full_package_names(channel, dataset_version)
588+
# ── library – compute index & delta BEFORE per-app zips ──
589+
apps_index = build_apps_index(catalog_dsv, channel, generated_at)
590+
serialized_index = json.dumps(apps_index, sort_keys=True, ensure_ascii=False)
591+
library_dsv = _hash_content(serialized_index)
592+
full_latest_name = V2_FULL_LATEST_NAME
590593

591594
with tempfile.TemporaryDirectory() as tmp_dir_name:
592595
tmp_dir = Path(tmp_dir_name)
593596
package_dir = tmp_dir / PACKAGE_ROOT_NAME
594597
copy_package_contents(package_dir, packaged_library_json)
595-
create_zip_from_directory(package_dir, full_dir / full_versioned_name)
596-
597-
shutil.copy2(full_dir / full_versioned_name, full_dir / full_latest_name)
598-
599-
# ── library – compute index & delta BEFORE per-app zips ──
600-
apps_index = build_apps_index(catalog_dsv, channel, generated_at)
601-
serialized_index = json.dumps(apps_index, sort_keys=True, ensure_ascii=False)
602-
library_dsv = _hash_content(serialized_index)
598+
create_zip_from_directory(package_dir, full_dir / full_latest_name)
603599

604600
apps_delta = build_apps_delta(
605601
apps_index=apps_index,
@@ -642,19 +638,15 @@ def build_v2_appstore_artifacts(
642638
write_json(library_dir / apps_delta_name, apps_delta)
643639

644640
library_checksums = {
645-
"fullPackageVersioned": f"full/{write_checksum_file(full_dir / full_versioned_name)}",
646-
"fullPackageLatest": f"full/{write_checksum_file(full_dir / full_latest_name)}",
641+
"fullPackage": f"full/{write_checksum_file(full_dir / full_latest_name)}",
647642
"appsIndex": write_checksum_file(library_dir / apps_index_name),
648643
"appsDelta": write_checksum_file(library_dir / apps_delta_name),
649644
}
650645

651646
library_manifest = build_library_manifest(
652647
dataset_version=library_dsv,
653648
channel=channel,
654-
full_package_names={
655-
"versioned": f"full/{full_versioned_name}",
656-
"latest": f"full/{full_latest_name}",
657-
},
649+
full_package_names=f"full/{full_latest_name}",
658650
apps_index_name=apps_index_name,
659651
apps_delta_name=apps_delta_name,
660652
checksum_names=library_checksums,
@@ -680,10 +672,7 @@ def build_v2_appstore_artifacts(
680672
},
681673
"library": {
682674
"manifest": "library/manifest.json",
683-
"fullPackage": {
684-
"versioned": f"full/{full_versioned_name}",
685-
"latest": f"full/{full_latest_name}",
686-
},
675+
"fullPackage": f"full/{full_latest_name}",
687676
"appPackagesBase": "apps/",
688677
"appsIndex": apps_index_name,
689678
"appsDelta": apps_delta_name,

docs/appstore-release-spec.md

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ artifact/appstore/<channel>/
106106
├── catalog/
107107
│ ├── manifest.json
108108
│ ├── full/
109-
│ │ └── catalog-<datasetVersion>.zip
109+
│ │ └── latest.zip
110110
│ ├── catalog_en.json
111111
│ ├── catalog_zh.json
112112
│ ├── product_en.json
@@ -117,8 +117,7 @@ artifact/appstore/<channel>/
117117
│ ├── apps-index-<datasetVersion>.json
118118
│ ├── apps-delta-<fromVersion>-to-<toVersion>.json
119119
│ ├── full/
120-
│ │ ├── library-<datasetVersion>.zip
121-
│ │ └── library-<channel>.zip
120+
│ │ └── latest.zip
122121
│ └── apps/
123122
│ └── <app>/
124123
│ ├── latest.zip
@@ -132,6 +131,9 @@ artifact/appstore/<channel>/
132131
| 要点 | 说明 |
133132
|------|------|
134133
| 全量包兜底 | 每次发布都重建,用于首装、灾备、回退 |
134+
| 通道只由目录表达 | `artifact/appstore/<channel>/` 已隔离 `dev` / `rc` / `release`,v2 文件名不再重复携带通道语义 |
135+
| catalog full 仅保留 latest | catalog 已有稳定命名的 JSON 文件与 checksum,`full/latest.zip` 仅承担冷启动与兜底恢复 |
136+
| library full 仅保留 latest | library 与 catalog 保持一致,v2 R2 不保留全量历史快照,仅保留固定入口 `full/latest.zip` |
135137
| 单 app 包仅 `latest.zip` | 不保留版本化包,R2 仅作分发缓存;审计追溯由 git 保证 |
136138
| 增量构建 |`addedApps` + `changedApps` 触发重建,未变更 app 不触碰 |
137139
| 上传策略 | `aws s3 sync`(默认不删除),首次发布由 `check_seed` 检测并全量播种 |
@@ -170,7 +172,7 @@ artifact/appstore/<channel>/
170172
"schemaVersion": "1",
171173
"datasetVersion": "7ee7a10b7da49f38",
172174
"source": "contentful",
173-
"fullPackage": "full/catalog-2026.06.09.120000.zip",
175+
"fullPackage": "full/latest.zip",
174176
"files": {
175177
"catalogEn": "catalog_en.json",
176178
"catalogZh": "catalog_zh.json",
@@ -182,7 +184,7 @@ artifact/appstore/<channel>/
182184
"catalogZh": "catalog_zh.json.sha256",
183185
"productEn": "product_en.json.sha256",
184186
"productZh": "product_zh.json.sha256",
185-
"fullPackage": "full/catalog-2026.06.09.120000.zip.sha256"
187+
"fullPackage": "full/latest.zip.sha256"
186188
},
187189
"generatedAt": "2026-06-09T12:00:00Z"
188190
}
@@ -195,17 +197,13 @@ artifact/appstore/<channel>/
195197
"schemaVersion": "1",
196198
"datasetVersion": "85f687824c03ef93",
197199
"channel": "release",
198-
"fullPackage": {
199-
"versioned": "full/library-2026.06.09.120000.zip",
200-
"latest": "full/library-release.zip"
201-
},
200+
"fullPackage": "full/latest.zip",
202201
"appsIndex": "apps-index-85f687824c03ef93.json",
203202
"appsDelta": "apps-delta-7ee7a10b7da49f38-to-85f687824c03ef93.json",
204203
"appPackagesBase": "apps/",
205204
"supportsPartialUpdate": true,
206205
"checksum": {
207-
"fullPackageVersioned": "full/library-2026.06.09.120000.zip.sha256",
208-
"fullPackageLatest": "full/library-release.zip.sha256",
206+
"fullPackage": "full/latest.zip.sha256",
209207
"appsIndex": "apps-index-85f687824c03ef93.json.sha256",
210208
"appsDelta": "apps-delta-7ee7a10b7da49f38-to-85f687824c03ef93.json.sha256"
211209
},
@@ -249,7 +247,7 @@ artifact/appstore/<channel>/
249247
3. 检查 R2 apps/ 是否为空 → 空则标记首次发布(--all-apps)
250248
4. 从 Contentful 拉取 catalog 源数据
251249
5. 运行 library_publish.py 构建所有制品(v2 + legacy)
252-
6. 上传 v2 制品到新 R2 路径
250+
6. 上传 v2 制品到新 R2 路径(`channel` 只由目录表达,full 固定入口统一为 `latest.zip`)
253251
7. 上传 legacy 制品到旧 R2 路径(兼容旧消费者)
254252
8. 清除 Cloudflare 缓存
255253
9. release 通道额外创建 GitHub Release
@@ -303,12 +301,12 @@ artifact/appstore/<channel>/
303301

304302
| # | 事项 |
305303
|---|------|
306-
| 1 | 保留 legacy workflow,新增独立 v2 workflow |
307-
| 2 | 新增 `v2/<channel>/appstore/{catalog,library,manifests}` 目录结构 |
304+
| 1 | 同一 workflow 同时产出 v2 与 legacy,两者目录和命名规则彼此独立 |
305+
| 2 | 新增 `artifact/appstore/<channel>/{catalog,library,manifests}` 目录结构 |
308306
| 3 | 为三层分别建立 `schemaVersion` + `datasetVersion` |
309307
| 4 | 旧 library / catalog 构建逻辑迁回本项目 |
310308
| 5 | Library 更新粒度固定为 app 级 |
311-
| 6 | 全量包 + 单 app 包(`latest.zip`)双输出 |
309+
| 6 | v2 catalog full 与 library full 均仅输出 `full/latest.zip`单 app 包继续使用 `latest.zip` |
312310
| 7 | Manifest + checksum 覆盖所有制品 |
313311
| 8 | 首次发布自动全量播种,后续增量构建 |
314312

@@ -342,13 +340,13 @@ artifact/appstore/<channel>/
342340
343341
3. 下载 catalog 全量数据:
344342
→ 下载 catalog/manifest.json
345-
→ 下载 catalog-<datasetVersion>.zip(全量包,1 次请求 1 次校验
346-
→ 校验 catalog-<datasetVersion>.zip.sha256
343+
→ 下载 full/latest.zip(固定入口
344+
→ 校验 full/latest.zip.sha256
347345
→ 解压到本地
348346
349347
4. 下载 library 全量数据:
350-
→ 下载 full/library-<channel>.zip(全量包,含所有 app 模板)
351-
→ 校验 full/library-<channel>.zip.sha256
348+
→ 下载 full/latest.zip(全量包,含所有 app 模板)
349+
→ 校验 full/latest.zip.sha256
352350
→ 解压到本地 apps/ 目录
353351
354352
5. 保存本次下载的 appstore-manifest.json 到本地作为状态锚点
@@ -384,13 +382,12 @@ artifact/appstore/<channel>/
384382

385383
```
386384
增量更新中任一文件下载或校验失败:
387-
→ 回退策略:下载 full/library-<channel>.zip 全量覆盖本地 apps/
385+
→ 回退策略:下载 library/full/latest.zip 全量覆盖本地 apps/
388386
→ 用本次下载的 manifest 替换本地缓存
389387
390388
schemaVersion 不在本地支持列表中:
391389
→ 不更新任何数据,保持本地缓存的旧 manifest
392390
→ 提示用户升级客户端
393-
→ 提示用户升级客户端
394391
```
395392

396393
### 11.4 缓存策略建议
@@ -399,8 +396,9 @@ schemaVersion 不在本地支持列表中:
399396
|------|------|------|
400397
| `appstore-manifest.json` | Cache-Control: 60s | 入口文件,需及时感知更新 |
401398
| `catalog/manifest.json` | Cache-Control: 60s | 体积极小,变更频率低 |
399+
| `catalog/full/latest.zip` | Cache-Control: 60s | 冷启动与兜底恢复入口,覆盖发布后需尽快生效 |
402400
| `apps-delta-*.json` | Cache-Control: 60s | 体积极小 |
403401
| `apps/{app}/latest.zip` | Cache-Control: 60s | 模板文件体积极小,变更后需立即生效 |
404-
| `full/library-{dsv}.zip` | immutable | 文件名含内容版本,永不变 |
402+
| `library/full/latest.zip` | Cache-Control: 60s | library 全量固定入口 |
405403
| `*.sha256` | 跟随对应文件 | 校验与被校验文件同步缓存 |
406404
| `catalog_*.json / product_*.json` | Cache-Control: 3600 | 描述数据,变更频率中等 |

0 commit comments

Comments
 (0)