From 459367131bb5104b69358359890dbb079bf62648 Mon Sep 17 00:00:00 2001 From: DTALEX66 <151195992+DTALEX66@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:09:36 +0800 Subject: [PATCH 1/2] ci(release): publish verified Windows artifacts --- .github/workflows/release.yml | 77 +++++++++++++++++++++++++ app/main.py | 4 +- app/release.py | 68 ++++++++++++++++++++++ desktop/scripts/verify_nsis_install.ps1 | 13 ++++- scripts/release_checksum.py | 10 +++- scripts/release_inject_identity.py | 32 ++++++---- tests/test_release_manifest.py | 77 +++++++++++++++++++++++-- 7 files changed, 263 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ba9021f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: read + +jobs: + publish: + if: ${{ github.ref_type == 'tag' }} + runs-on: windows-latest + timeout-minutes: 45 + permissions: + contents: write + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: "3.12" + - name: Require tag target to equal main + shell: pwsh + run: | + if ((git rev-parse origin/main).Trim() -ne $env:GITHUB_SHA) { + throw "release tag must point to the current main commit" + } + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e + with: + enable-cache: true + cache-dependency-glob: uv.lock + - name: Install locked build tooling + run: uv sync --frozen + - name: Install locked desktop tooling + working-directory: desktop + run: npm ci --ignore-scripts --no-audit --no-fund + - name: Prepare bundled runtime and inject exact release identity + shell: pwsh + run: | + python -m desktop.scripts.prepare_bundle --repository . --destination .hermes/desktop-runtime-v1 + $tree = git rev-parse "$env:GITHUB_SHA^{tree}" + python scripts/release_inject_identity.py ` + --commit $env:GITHUB_SHA ` + --tree $tree ` + --tag $env:GITHUB_REF_NAME ` + --version 0.4.0 ` + --url "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/releases/tag/$env:GITHUB_REF_NAME" ` + --ci-url "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID" ` + --output .hermes/desktop-runtime-v1/runtime/release-identity.json + - name: Build Windows NSIS installer + working-directory: desktop + run: npm run tauri -- build --bundles nsis + - name: Verify installed NSIS lifecycle + shell: pwsh + run: | + $installers = @(Get-ChildItem "desktop/src-tauri/target/release/bundle/nsis/*.exe") + if ($installers.Count -ne 1) { throw "expected exactly one NSIS installer, found $($installers.Count)" } + ./desktop/scripts/verify_nsis_install.ps1 -Installer $installers[0].FullName -RequireReleaseIdentity + - name: Build release wheel and checksums + shell: pwsh + run: | + New-Item -ItemType Directory -Force release-assets | Out-Null + uv build --wheel --out-dir release-assets + $wheel = @(Get-ChildItem "release-assets/*.whl") + $installers = @(Get-ChildItem "desktop/src-tauri/target/release/bundle/nsis/*.exe") + if ($wheel.Count -ne 1 -or $installers.Count -ne 1) { throw "expected one wheel and one NSIS installer" } + Copy-Item $installers[0].FullName release-assets/ + Copy-Item .hermes/desktop-runtime-v1/runtime/release-identity.json release-assets/ + python scripts/release_checksum.py --wheel $wheel[0].FullName --installer (Join-Path release-assets $installers[0].Name) --artifact release-assets/release-identity.json --output release-assets/SHA256SUMS.txt + - name: Publish verified assets + env: + GH_TOKEN: ${{ github.token }} + shell: pwsh + run: gh release create $env:GITHUB_REF_NAME release-assets/* --verify-tag --title "ArcheAxis OS $env:GITHUB_REF_NAME" --generate-notes diff --git a/app/main.py b/app/main.py index fb5a20e..5d08b92 100644 --- a/app/main.py +++ b/app/main.py @@ -401,14 +401,14 @@ def diagnostics(): @app.get("/version") def version(): - from app.release import load_release_manifest, safe_release_summary + from app.release import effective_capabilities, load_release_manifest, safe_release_summary manifest = load_release_manifest() return { "product": manifest["product"]["english_name"], "version": manifest["product"]["version"], "release": safe_release_summary(), - "capabilities": manifest["capabilities"], + "capabilities": effective_capabilities(), } diff --git a/app/release.py b/app/release.py index 524a3fe..eddc972 100644 --- a/app/release.py +++ b/app/release.py @@ -9,6 +9,8 @@ from typing import Any _MANIFEST_PATH = Path(__file__).with_name("release-manifest.json") +_ARTIFACT_IDENTITY_PATH: Path | None = None +_RELEASE_REPOSITORY_URL = "https://github.com/DTALEX66/Cognitive-Loop-OS" _ALLOWED_CAPABILITY_STATES = {"available", "dependency_required", "not_implemented"} _CAPABILITY_KEYS = { "local_url_file_github_intake", @@ -154,9 +156,67 @@ def load_release_manifest() -> dict[str, Any]: return manifest +@lru_cache(maxsize=1) +def load_artifact_release_identity() -> dict[str, Any] | None: + """Read verified release identity packaged alongside a bundled runtime.""" + identity_path = _ARTIFACT_IDENTITY_PATH or next( + (parent / "release-identity.json" for parent in _MANIFEST_PATH.parents if (parent / "release-identity.json").is_file()), + _MANIFEST_PATH.parent.parent / "release-identity.json", + ) + if not identity_path.is_file(): + return None + + identity = json.loads(identity_path.read_text(encoding="utf-8")) + _require_exact_keys(identity, {"schema_version", "release", "source"}, "artifact identity") + if identity["schema_version"] != "1.0.0": + raise RuntimeError("unsupported artifact release identity schema") + + release = _require_exact_keys( + identity["release"], {"tag", "version", "channel", "public", "url"}, "artifact release" + ) + source = _require_exact_keys( + identity["source"], {"commit", "tree", "ci_run", "ci_url"}, "artifact source" + ) + manifest = load_release_manifest() + if ( + release["tag"] != f"v{manifest['product']['version']}" + or release["version"] != manifest["product"]["version"] + or release["channel"] != "stable" + or release["public"] is not True + or not isinstance(release["url"], str) + or release["url"] != f"{_RELEASE_REPOSITORY_URL}/releases/tag/{release['tag']}" + ): + raise RuntimeError("artifact release identity has invalid release fields") + if ( + not isinstance(source["commit"], str) + or _HEX_40.fullmatch(source["commit"]) is None + or not isinstance(source["tree"], str) + or _HEX_40.fullmatch(source["tree"]) is None + or not isinstance(source["ci_run"], int) + or source["ci_run"] < 1 + or not isinstance(source["ci_url"], str) + or source["ci_url"] != f"{_RELEASE_REPOSITORY_URL}/actions/runs/{source['ci_run']}" + ): + raise RuntimeError("artifact release identity has invalid source fields") + return identity + + def safe_release_summary() -> dict[str, object]: """Expose no filesystem paths or unverifiable test-count claims.""" manifest = load_release_manifest() + identity = load_artifact_release_identity() + if identity is not None: + artifact_release = identity["release"] + artifact_source = identity["source"] + return { + "status": "released", + "version": artifact_release["version"], + "channel": artifact_release["channel"], + "source_commit": artifact_source["commit"], + "tag": artifact_release["tag"], + "ci_run": artifact_source["ci_run"], + "url": artifact_release["url"], + } release = manifest["release"] source = manifest["source"] return { @@ -165,3 +225,11 @@ def safe_release_summary() -> dict[str, object]: "channel": release["channel"], "source_commit": source["commit"], } + + +def effective_capabilities() -> dict[str, str]: + """Report the installer capability only when bundled release identity verifies it.""" + capabilities = dict(load_release_manifest()["capabilities"]) + if load_artifact_release_identity() is not None: + capabilities["public_installer"] = "available" + return capabilities diff --git a/desktop/scripts/verify_nsis_install.ps1 b/desktop/scripts/verify_nsis_install.ps1 index 11d6ab7..da8c9ee 100644 --- a/desktop/scripts/verify_nsis_install.ps1 +++ b/desktop/scripts/verify_nsis_install.ps1 @@ -1,6 +1,7 @@ param( [Parameter(Mandatory = $true)] - [string]$Installer + [string]$Installer, + [switch]$RequireReleaseIdentity ) $ErrorActionPreference = 'Stop' @@ -95,6 +96,16 @@ try { if ($workspaceStatus -ne 200 -or $status.release.version -ne '0.4.0') { throw 'installed Workspace returned an invalid product response' } + if ($RequireReleaseIdentity) { + $version = Invoke-RestMethod "$base/version" + if ( + $version.release.status -ne 'released' -or + $version.release.tag -ne 'v0.4.0' -or + $version.capabilities.public_installer -ne 'available' + ) { + throw 'installed runtime did not expose the verified public release identity' + } + } [void]$activeShell.CloseMainWindow() if (-not $activeShell.WaitForExit(15000)) { throw 'desktop shell did not exit after closing its main window' diff --git a/scripts/release_checksum.py b/scripts/release_checksum.py index 18d7ed9..9f891ba 100644 --- a/scripts/release_checksum.py +++ b/scripts/release_checksum.py @@ -24,6 +24,7 @@ def main() -> int: parser = argparse.ArgumentParser(description="Generate release artifact checksum manifest") parser.add_argument("--wheel", type=str, help="Path to the .whl file") parser.add_argument("--installer", type=str, help="Path to the NSIS installer .exe") + parser.add_argument("--artifact", action="append", default=[], help="Additional release payload path") parser.add_argument("--output", type=str, required=True, help="Output checksum file path") args = parser.parse_args() @@ -47,8 +48,15 @@ def main() -> int: return 1 artifacts.append(("installer", installer)) + for artifact_value in args.artifact: + artifact = Path(artifact_value) + if not artifact.is_file(): + print(f"ERROR: artifact not found: {artifact}", file=sys.stderr) + return 1 + artifacts.append(("artifact", artifact)) + if not artifacts: - print("ERROR: at least one of --wheel or --installer is required", file=sys.stderr) + print("ERROR: at least one release payload is required", file=sys.stderr) return 1 for label, path in artifacts: diff --git a/scripts/release_inject_identity.py b/scripts/release_inject_identity.py index ed22395..2c84629 100644 --- a/scripts/release_inject_identity.py +++ b/scripts/release_inject_identity.py @@ -31,7 +31,11 @@ def main() -> int: parser = argparse.ArgumentParser(description="Inject release identity") parser.add_argument("--commit", required=True, help="Exact Git commit SHA (40 hex chars)") parser.add_argument("--tree", required=True, help="Exact Git tree hash (40 hex chars)") - parser.add_argument("--branch", required=True, help="Git branch name") + + parser.add_argument("--tag", required=True, help="Release tag") + parser.add_argument("--version", required=True, help="Product version") + parser.add_argument("--url", required=True, help="Canonical GitHub Release URL") + parser.add_argument("--ci-url", required=True, help="Canonical GitHub Actions run URL") parser.add_argument("--output", required=True, help="Output identity manifest path") args = parser.parse_args() @@ -46,22 +50,30 @@ def main() -> int: return 1 ci_run = os.environ.get("GITHUB_RUN_ID") - if ci_run: - try: - ci_run = int(ci_run) - except ValueError: - print(f"WARNING: GITHUB_RUN_ID is not a valid integer: {ci_run}", file=sys.stderr) - ci_run = None + try: + ci_run = int(ci_run or "") + except ValueError: + print("ERROR: GITHUB_RUN_ID must be a positive integer", file=sys.stderr) + return 1 + if ci_run < 1 or args.tag != f"v{args.version}" or not args.url.startswith("https://") or not args.ci_url.startswith("https://"): + print("ERROR: invalid release identity arguments", file=sys.stderr) + return 1 identity = { "schema_version": "1.0.0", + "release": { + "tag": args.tag, + "version": args.version, + "channel": "stable", + "public": True, + "url": args.url, + }, "source": { "commit": commit, "tree": tree, - "branch": args.branch, "ci_run": ci_run, + "ci_url": args.ci_url, }, - "generated_at": "ISO-8601", # placeholder — CI may not have tz } output = Path(args.output) @@ -71,7 +83,7 @@ def main() -> int: print(f"Identity manifest written: {output}") print(f" commit: {commit}") print(f" tree: {tree}") - print(f" branch: {args.branch}") + print(f" ci_run: {ci_run}") return 0 diff --git a/tests/test_release_manifest.py b/tests/test_release_manifest.py index 36ae066..e668122 100644 --- a/tests/test_release_manifest.py +++ b/tests/test_release_manifest.py @@ -134,6 +134,64 @@ def test_release_manifest_marks_unimplemented_product_surfaces_truthfully() -> N assert capabilities["qdrant_runtime"] == "not_implemented" +def test_bundled_release_identity_exposes_a_verified_public_release_summary( + monkeypatch, tmp_path +) -> None: + from app import release + + identity_path = tmp_path / "release-identity.json" + identity_path.write_text( + json.dumps( + { + "schema_version": "1.0.0", + "release": { + "tag": "v0.4.0", + "version": "0.4.0", + "channel": "stable", + "public": True, + "url": "https://github.com/DTALEX66/Cognitive-Loop-OS/releases/tag/v0.4.0", + }, + "source": { + "commit": "34ca0fbd5ae636314a3403c473bde9247ef95907", + "tree": "d144559cdd81e1ca58223281ea8bdcbd27821716", + "ci_run": 30548553629, + "ci_url": "https://github.com/DTALEX66/Cognitive-Loop-OS/actions/runs/30548553629", + }, + } + ), + encoding="utf-8", + ) + monkeypatch.setattr(release, "_ARTIFACT_IDENTITY_PATH", identity_path) + release.load_artifact_release_identity.cache_clear() + + assert release.safe_release_summary() == { + "status": "released", + "version": "0.4.0", + "channel": "stable", + "source_commit": "34ca0fbd5ae636314a3403c473bde9247ef95907", + "tag": "v0.4.0", + "ci_run": 30548553629, + "url": "https://github.com/DTALEX66/Cognitive-Loop-OS/releases/tag/v0.4.0", + } + assert release.effective_capabilities()["public_installer"] == "available" + + +def test_bundled_release_identity_rejects_semantically_wrong_urls(monkeypatch, tmp_path) -> None: + from app import release + + identity = { + "schema_version": "1.0.0", + "release": {"tag": "v0.4.0", "version": "0.4.0", "channel": "stable", "public": True, "url": "https://github.com/foreign-owner/foreign-repo/releases/tag/v0.4.0"}, + "source": {"commit": "34ca0fbd5ae636314a3403c473bde9247ef95907", "tree": "d144559cdd81e1ca582231ea8bdcbd27821716", "ci_run": 30548553629, "ci_url": "https://github.com/foreign-owner/foreign-repo/actions/runs/30548553629"}, + } + path = tmp_path / "release-identity.json" + path.write_text(json.dumps(identity), encoding="utf-8") + monkeypatch.setattr(release, "_ARTIFACT_IDENTITY_PATH", path, raising=False) + release.load_artifact_release_identity.cache_clear() + with pytest.raises(RuntimeError, match="invalid release fields"): + release.load_artifact_release_identity() + + def test_truth_docs_do_not_overstate_startup_migration_or_delivery() -> None: root = Path(__file__).resolve().parents[1] readme = (root / "README.md").read_text(encoding="utf-8") @@ -228,12 +286,16 @@ def test_release_identity_injection_manifests_exact_commit_and_tree(tmp_path) -> [sys.executable, str(script), "--commit", commit, "--tree", tree, - "--branch", "feat/absorption-roadmap-r0", + + "--tag", "v0.4.0", + "--version", "0.4.0", + "--url", "https://github.com/DTALEX66/Cognitive-Loop-OS/releases/tag/v0.4.0", + "--ci-url", "https://github.com/DTALEX66/Cognitive-Loop-OS/actions/runs/30548553629", "--output", str(output)], capture_output=True, text=True, cwd=Path(__file__).resolve().parents[1], - env={key: value for key, value in os.environ.items() if key != "GITHUB_RUN_ID"}, + env={**os.environ, "GITHUB_RUN_ID": "30548553629"}, ) assert result.returncode == 0, f"script failed: {result.stderr}" @@ -241,8 +303,15 @@ def test_release_identity_injection_manifests_exact_commit_and_tree(tmp_path) -> assert identity["schema_version"] == "1.0.0" assert identity["source"]["commit"] == commit assert identity["source"]["tree"] == tree - assert identity["source"]["branch"] == "feat/absorption-roadmap-r0" - assert identity["source"]["ci_run"] is None # no GITHUB_RUN_ID in test + assert identity["release"] == { + "tag": "v0.4.0", + "version": "0.4.0", + "channel": "stable", + "public": True, + "url": "https://github.com/DTALEX66/Cognitive-Loop-OS/releases/tag/v0.4.0", + } + assert identity["source"]["ci_url"] == "https://github.com/DTALEX66/Cognitive-Loop-OS/actions/runs/30548553629" + assert identity["source"]["ci_run"] == 30548553629 def test_release_identity_injection_rejects_invalid_sha(tmp_path) -> None: From faec347d422e6ac03d505f474eb931fb7535f8f0 Mon Sep 17 00:00:00 2001 From: DTALEX66 <151195992+DTALEX66@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:32:03 +0800 Subject: [PATCH 2/2] test(workspace): wait for inspector transition endpoint --- scripts/a0_browser_smoke.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/a0_browser_smoke.py b/scripts/a0_browser_smoke.py index 719f600..b52df96 100644 --- a/scripts/a0_browser_smoke.py +++ b/scripts/a0_browser_smoke.py @@ -214,6 +214,9 @@ def observe_activity_jobs(route: Route) -> None: page.set_viewport_size({"width": 390, "height": 844}) expect(inspector).to_have_attribute("aria-hidden", "true") assert inspector.evaluate("element => element.hasAttribute('inert')") + page.wait_for_function( + """() => document.querySelector('#inspector').getBoundingClientRect().left >= window.innerWidth""" + ) inspector_geometry = page.evaluate( """() => { const rect = document.querySelector('#inspector').getBoundingClientRect(); return { left: rect.left, viewportWidth: window.innerWidth }; }""" )