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
77 changes: 77 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}


Expand Down
68 changes: 68 additions & 0 deletions app/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand All @@ -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
13 changes: 12 additions & 1 deletion desktop/scripts/verify_nsis_install.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
param(
[Parameter(Mandatory = $true)]
[string]$Installer
[string]$Installer,
[switch]$RequireReleaseIdentity
)

$ErrorActionPreference = 'Stop'
Expand Down Expand Up @@ -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'
Expand Down
3 changes: 3 additions & 0 deletions scripts/a0_browser_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 }; }"""
)
Expand Down
10 changes: 9 additions & 1 deletion scripts/release_checksum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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:
Expand Down
32 changes: 22 additions & 10 deletions scripts/release_inject_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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)
Expand All @@ -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

Expand Down
Loading
Loading