Skip to content

Commit a658575

Browse files
authored
Merge pull request #2119 from Websoft9/feature/appstore-publish-migration
fix
2 parents 6c093f8 + 38411eb commit a658575

7 files changed

Lines changed: 10370 additions & 24 deletions

File tree

19.8 KB
Binary file not shown.

build/library_publish.py

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,23 @@ def v2_full_package_names(channel: str, dataset_version: str) -> tuple[str, str]
120120
return (f"{V2_FULL_PACKAGE_BASENAME}-{dataset_version}.zip", f"{V2_FULL_PACKAGE_BASENAME}-{channel}.zip")
121121

122122

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

126141

127142
def sha256_file(path: Path) -> str:
@@ -181,17 +196,17 @@ def current_app_fingerprint(app_dir: Path) -> str:
181196
return digest.hexdigest()
182197

183198

184-
def build_app_package_entry(app_name: str, dataset_version: str) -> dict:
185-
versioned_name, latest_name = v2_app_package_names(dataset_version)
199+
def build_app_package_entry(app_name: str, app_version: str) -> dict:
200+
versioned_name, latest_name = v2_app_package_names(app_version)
186201
app_base = f"apps/{app_name}"
187202
return {
188203
"versioned": f"{app_base}/{versioned_name}",
189204
"latest": f"{app_base}/{latest_name}",
190205
}
191206

192207

193-
def build_app_checksum_entry(app_name: str, dataset_version: str) -> dict:
194-
package_entry = build_app_package_entry(app_name, dataset_version)
208+
def build_app_checksum_entry(app_name: str, app_version: str) -> dict:
209+
package_entry = build_app_package_entry(app_name, app_version)
195210
return {
196211
"versioned": f"{package_entry['versioned']}.sha256",
197212
"latest": f"{package_entry['latest']}.sha256",
@@ -203,6 +218,8 @@ def build_apps_index(dataset_version: str, channel: str, generated_at: str) -> d
203218
for app_dir in sorted(path for path in APPS_DIR.iterdir() if path.is_dir()):
204219
variables = load_variables_json(app_dir)
205220
app_name = app_dir.name
221+
app_hash = current_app_fingerprint(app_dir)
222+
app_version = _app_version_from_hash(app_hash)
206223
apps.append(
207224
{
208225
"app": app_name,
@@ -211,9 +228,9 @@ def build_apps_index(dataset_version: str, channel: str, generated_at: str) -> d
211228
"release": variables.get("release"),
212229
"versions": summarize_versions(variables.get("edition", [])),
213230
"path": f"apps/{app_name}",
214-
"hash": current_app_fingerprint(app_dir),
215-
"package": build_app_package_entry(app_name, dataset_version),
216-
"checksum": build_app_checksum_entry(app_name, dataset_version),
231+
"hash": app_hash,
232+
"package": build_app_package_entry(app_name, app_version),
233+
"checksum": build_app_checksum_entry(app_name, app_version),
217234
}
218235
)
219236

@@ -402,7 +419,7 @@ def validate_catalog_artifacts(output_dir: Path, manifest: dict) -> None:
402419
raise SystemExit("missing catalog manifest.json")
403420

404421

405-
def validate_library_artifacts(output_dir: Path, manifest: dict) -> None:
422+
def validate_library_artifacts(output_dir: Path, manifest: dict, changed_app_names: set[str] | None = None) -> None:
406423
for name in (
407424
manifest["fullPackage"]["versioned"],
408425
manifest["fullPackage"]["latest"],
@@ -423,7 +440,13 @@ def validate_library_artifacts(output_dir: Path, manifest: dict) -> None:
423440
raise SystemExit(f"invalid apps delta structure: {key}")
424441

425442
apps_index = json.loads((output_dir / manifest["appsIndex"]).read_text(encoding="utf-8"))
443+
# Only validate per-app packages for apps that were actually (re)built
444+
# in this publish run. Unchanged apps keep their existing packages on R2.
445+
validate_set = changed_app_names if changed_app_names is not None else {e["app"] for e in apps_index.get("apps", [])}
446+
426447
for entry in apps_index.get("apps", []):
448+
if entry.get("app") not in validate_set:
449+
continue
427450
package = entry.get("package") or {}
428451
checksum = entry.get("checksum") or {}
429452
for path in (package.get("versioned"), package.get("latest"), checksum.get("versioned"), checksum.get("latest")):
@@ -561,11 +584,39 @@ def build_v2_appstore_artifacts(
561584

562585
shutil.copy2(full_dir / full_versioned_name, full_dir / full_latest_name)
563586

587+
# ── library – compute index & delta BEFORE per-app zips ──
588+
apps_index = build_apps_index(dataset_version, channel, generated_at)
589+
apps_delta = build_apps_delta(
590+
apps_index=apps_index,
591+
from_ref=from_ref,
592+
from_version=from_version,
593+
to_version=dataset_version,
594+
channel=channel,
595+
generated_at=generated_at,
596+
)
597+
598+
# Determine which apps actually need (re)built packages.
599+
# On the very first publish (from_ref is None) every app is new.
600+
if from_ref is None:
601+
changed_app_names: set[str] = {entry["app"] for entry in apps_index["apps"]}
602+
else:
603+
changed_app_names = set(apps_delta.get("addedApps", [])) | set(apps_delta.get("changedApps", []))
604+
605+
# ── library – per-app packages (only for changed apps) ───
564606
for app_dir in sorted(path for path in APPS_DIR.iterdir() if path.is_dir()):
565607
app_name = app_dir.name
608+
if app_name not in changed_app_names:
609+
continue
610+
611+
# Look up the app's content-derived version identifier from the index.
612+
app_entry = next((e for e in apps_index["apps"] if e["app"] == app_name), None)
613+
if app_entry is None:
614+
continue
615+
app_version = _app_version_from_hash(app_entry["hash"])
616+
app_versioned_name, app_latest_name = v2_app_package_names(app_version)
617+
566618
app_output_dir = apps_packages_dir / app_name
567619
app_output_dir.mkdir(parents=True, exist_ok=True)
568-
app_versioned_name, app_latest_name = v2_app_package_names(dataset_version)
569620

570621
with tempfile.TemporaryDirectory() as tmp_dir_name:
571622
tmp_dir = Path(tmp_dir_name)
@@ -577,15 +628,7 @@ def build_v2_appstore_artifacts(
577628
write_checksum_file(app_output_dir / app_versioned_name)
578629
write_checksum_file(app_output_dir / app_latest_name)
579630

580-
apps_index = build_apps_index(dataset_version, channel, generated_at)
581-
apps_delta = build_apps_delta(
582-
apps_index=apps_index,
583-
from_ref=from_ref,
584-
from_version=from_version,
585-
to_version=dataset_version,
586-
channel=channel,
587-
generated_at=generated_at,
588-
)
631+
# ── library – write index, delta, manifest ───────────────
589632
write_json(library_dir / apps_index_name, apps_index)
590633
write_json(library_dir / apps_delta_name, apps_delta)
591634

@@ -610,7 +653,7 @@ def build_v2_appstore_artifacts(
610653
)
611654
write_json(library_dir / "manifest.json", library_manifest)
612655
write_checksum_file(library_dir / "manifest.json")
613-
validate_library_artifacts(library_dir, library_manifest)
656+
validate_library_artifacts(library_dir, library_manifest, changed_app_names)
614657

615658
appstore_manifest = build_appstore_manifest(dataset_version, channel, generated_at)
616659
write_json(manifests_dir / "appstore-manifest.json", appstore_manifest)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

0 commit comments

Comments
 (0)