Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions .github/workflows/runtime-snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
- ".github/workflows/runtime-snapshots.yml"
- "scripts/runtime_artifact.py"
- "scripts/qualify_runtime_artifact.py"
- "tests/test_runtime_snapshot_publication.py"
- "tests/test_update_artifact.py"
- "scripts/install*.sh"
- "loopx/cli_commands/update*.py"
- "loopx/cli_commands/support_control_update*.py"
Expand Down Expand Up @@ -43,7 +45,7 @@ jobs:
- name: Prepare test environment
run: python -m pip install -e '.[test]'
- name: Qualify update transport and ownership
run: python -m pytest -q tests/test_update_artifact.py tests/test_update_operation.py tests/test_self_update_runtime_activation.py tests/test_archive_installer_commit_response.py
run: python -m pytest -q tests/test_runtime_snapshot_publication.py tests/test_update_artifact.py tests/test_update_operation.py tests/test_self_update_runtime_activation.py tests/test_archive_installer_commit_response.py
- name: Build qualified frontend
run: python scripts/chat_bundle.py ensure
- name: Build immutable runtime and resource inventory
Expand Down Expand Up @@ -88,12 +90,14 @@ jobs:
run: |
set -euo pipefail
revision="$(python -c 'import json; print(json.load(open("runtime/loopx-runtime.json"))["source_revision"])')"
# One dedicated channel release keeps developer snapshots out of
# product release history and bounded stable-predecessor discovery.
tag=runtime-main
# Keep each immutable snapshot below GitHub's per-release asset cap.
# The existing channel release retains its historical assets and URL.
tag="runtime-main-${revision}"
if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release create "$tag" --repo "$GITHUB_REPOSITORY" --target "$revision" --prerelease --title "Latest qualified main runtime" --notes "Mutable channel metadata; revision-addressed archives and manifests are immutable. Stable users continue with PyPI or the stable runtime."
gh release create "$tag" --repo "$GITHUB_REPOSITORY" --target "$revision" --prerelease --latest=false --title "Qualified main runtime ${revision}" --notes "Immutable runtime snapshot. Stable users continue with PyPI or the stable runtime."
fi
actual="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${tag}" --jq .sha)"
if [[ "$actual" != "$revision" ]]; then exit 1; fi
# A retry reuses immutable bytes. Never clobber an immutable asset.
mkdir -p existing
for file in runtime/*; do
Expand All @@ -109,6 +113,9 @@ jobs:
# newer main is still building; never publish a non-main revision.
relation="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${revision}...main" --jq .status)"
if [[ "$relation" != ahead && "$relation" != identical ]]; then exit 1; fi
if ! gh release view runtime-main --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release create runtime-main --repo "$GITHUB_REPOSITORY" --target "$revision" --prerelease --latest=false --title "Latest qualified main runtime" --notes "Mutable channel metadata points to immutable revision releases. Stable users continue with PyPI or the stable runtime."
fi
# Rerunning an older successful workflow may retain its immutable
# assets, but must not move the qualified main channel backwards.
current="$(gh api "repos/${GITHUB_REPOSITORY}/commits/runtime-main" --jq .sha)"
Expand All @@ -117,3 +124,6 @@ jobs:
if [[ "$promotion" != ahead && "$promotion" != identical ]]; then exit 1; fi
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/runtime-main" -f sha="$revision" -F force=false >/dev/null
gh release upload runtime-main runtime/loopx-runtime.json --repo "$GITHUB_REPOSITORY" --clobber
mkdir -p channel-readback
gh release download runtime-main --repo "$GITHUB_REPOSITORY" --pattern loopx-runtime.json --dir channel-readback --clobber
cmp runtime/loopx-runtime.json channel-readback/loopx-runtime.json
21 changes: 18 additions & 3 deletions docs/guides/installing-loopx.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,24 @@ Release rollout: publish and qualify the stable runtime before deploying the
new public bootstrap. The Pages lane checks this dependency and retains the
previous deployed site if it is unavailable. `runtime-snapshots.yml` builds
main artifacts; `release-artifacts.yml` reuses the release frontend/wheel lane
for stable. Main archives and revision-addressed metadata share one dedicated
`runtime-main` release, keeping product release history and stable-predecessor
discovery intact; only the latest-channel metadata is replaced. Clean
for stable. New main archives and revision-addressed metadata use one prerelease
per revision, `runtime-main-<full SHA>`, so the channel cannot exhaust GitHub's
1,000-assets-per-release limit. `runtime-main/loopx-runtime.json` remains the
latest-channel endpoint; only that metadata is replaced, after all immutable
assets are uploaded and checked. Historical assets in `runtime-main` stay
available, and current clients fall back to their old SHA address only on HTTP
404. Older clients can follow the unchanged `main` endpoint, but must update
before pinning a newly published SHA. Stable release selection still excludes
these prereleases; the existing all-release pagination grows with snapshot
history. No historical assets are deleted or moved by this publisher.

中文:新 main 快照各自使用 `runtime-main-<完整 SHA>` 预发布,避免单个 release
达到 1,000 个资产上限。最新入口仍为 `runtime-main/loopx-runtime.json`,所有不可变
资产完成后才更新;旧资产不删除、不迁移。新版客户端仅在新 SHA 地址返回 404 时
回读旧地址。旧客户端可先升级 main,再固定新发布的 SHA。stable 仍排除这些预发布,
但现有 release 全量分页的开销会随快照历史增长。

Clean
install/upgrade, package contents, byte size, provenance and
phase timings are release evidence. A 20 MiB runtime is an evaluation target,
not a waived dependency check or a global p50/p95 promise.
Expand Down
12 changes: 10 additions & 2 deletions loopx/cli_commands/update_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,20 @@ def resolve_artifact(repo: str, ref: str) -> dict[str, Any]:
elif ref == "main":
url = f"https://github.com/{repo}/releases/download/runtime-main/loopx-runtime.json"
elif SHA.fullmatch(ref):
url = f"https://github.com/{repo}/releases/download/runtime-main/loopx-runtime-{ref}.json"
url = f"https://github.com/{repo}/releases/download/runtime-main-{ref}/loopx-runtime-{ref}.json"
elif re.fullmatch(r"v[0-9][A-Za-z0-9.+-]*", ref):
url = f"https://github.com/{repo}/releases/download/{ref}/loopx-runtime.json"
else:
raise ValueError("This ref has no runtime feed; explicitly use --source to build it")
artifact = validate_manifest(read_json(url), revision=ref if SHA.fullmatch(ref) else None)
try:
metadata = read_json(url)
except ValueError as exc:
# Older snapshots stay in the original release. Only a missing new
# address permits that lookup; corrupt metadata/auth/TLS failures stop.
if not (SHA.fullmatch(ref) and isinstance(exc.__cause__, HTTPError) and exc.__cause__.code == 404):
raise
metadata = read_json(f"https://github.com/{repo}/releases/download/runtime-main/loopx-runtime-{ref}.json")
artifact = validate_manifest(metadata, revision=ref if SHA.fullmatch(ref) else None)
if ref in {"main", "stable"} and artifact.get("channel") != ref:
raise ValueError("Runtime artifact belongs to another channel")
return artifact
Expand Down
2 changes: 1 addition & 1 deletion scripts/runtime_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def build(root: Path, output: Path, *, repository: str, channel: str) -> dict:
"sha256": hashlib.sha256(identity).hexdigest()})
if git.wait() != 0 or not version:
raise ValueError("Git runtime archive failed")
tag = "runtime-main" if channel == "main" else "v" + version
tag = f"runtime-main-{revision}" if channel == "main" else "v" + version
archive = output / filename
result = validate_manifest({"schema_version": RUNTIME_ARTIFACT_SCHEMA_VERSION, "source_revision": revision,
"package_version": version, "channel": channel, "size": archive.stat().st_size,
Expand Down
2 changes: 2 additions & 0 deletions tests/presentation/test_chat_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ def release(tag, *, draft=False, prerelease=False):
}

releases = [
[release(f"runtime-main-{index:040x}", prerelease=True) for index in range(100)],
[release(f"runtime-main-{index:040x}", prerelease=True) for index in range(100, 200)],
[
release("v1.0.4"),
release("v1.0.5"),
Expand Down
Loading
Loading