Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f4f8cc5
refactor(release): restore single macOS artifact
Git-on-my-level Jul 21, 2026
5a3b646
refactor(release): unify macOS manifest contract
Git-on-my-level Jul 21, 2026
329a2bb
refactor(release): type canonical manifest adapter
Git-on-my-level Jul 21, 2026
6e435e4
fix(release): preserve canonical manifest bytes
Git-on-my-level Jul 21, 2026
c1a04b6
fix(release): mount exact install smoke artifact
Git-on-my-level Jul 21, 2026
0cf9f93
fix(release): require canonical qualification dmg
Git-on-my-level Jul 21, 2026
55a9ac5
fix(release): parse installer download contract
Git-on-my-level Jul 21, 2026
50f15db
fix(release): harden installer download contract
Git-on-my-level Jul 21, 2026
6600151
fix(release): lock installer run block bytes
Git-on-my-level Jul 21, 2026
78fc2b6
fix(release): bind installer contract to download step
Git-on-my-level Jul 21, 2026
818729c
fix(release): lock installer mount block bytes
Git-on-my-level Jul 21, 2026
1911bfd
fix(release): bind installer contract to test job
Git-on-my-level Jul 21, 2026
7c73171
fix(release): lock installer test job bytes
Git-on-my-level Jul 21, 2026
ead60bd
fix(release): parse workflow guard keys semantically
Git-on-my-level Jul 21, 2026
4833361
fix(release): close installer workflow semantic bypasses
Git-on-my-level Jul 21, 2026
bc9ba0f
fix(ci): install actionlint for desktop swift test suites
Git-on-my-level Jul 21, 2026
6d4b344
ci: trigger workflow run for actionlint fix
Git-on-my-level Jul 21, 2026
1f247f9
chore: nudge CI trigger
Git-on-my-level Jul 21, 2026
0a8e359
ci: re-trigger workflows after branch restore
Git-on-my-level Jul 21, 2026
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
30 changes: 11 additions & 19 deletions .github/schemas/desktop-release-manifest-v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@
"dmg_url",
"dmg_sha256",
"ed_signature",
"beta_zip_url",
"beta_zip_sha256",
"beta_dmg_url",
"beta_dmg_sha256",
"beta_ed_signature",
"qualification_evidence_asset",
"qualification_evidence_sha256",
"qualification_tier",
Expand Down Expand Up @@ -51,11 +46,6 @@
"dmg_url": { "$ref": "#/$defs/dmgUrl" },
"dmg_sha256": { "$ref": "#/$defs/sha256" },
"ed_signature": { "type": "string", "pattern": ".*\\S.*" },
"beta_zip_url": { "$ref": "#/$defs/betaZipUrl" },
"beta_zip_sha256": { "$ref": "#/$defs/sha256" },
"beta_dmg_url": { "$ref": "#/$defs/betaDmgUrl" },
"beta_dmg_sha256": { "$ref": "#/$defs/sha256" },
"beta_ed_signature": { "type": "string", "pattern": ".*\\S.*" },
"qualification_evidence_asset": {
"type": "string",
"pattern": "^qualification-evidence-[^/]+\\.json$"
Expand Down Expand Up @@ -125,7 +115,17 @@
"type": "string",
"format": "date-time",
"pattern": "Z$"
}
},
"published_at": {
"type": "string",
"format": "date-time",
"pattern": "Z$"
},
"changelog": {
"type": "array",
"items": { "type": "string", "pattern": ".*\\S.*" }
},
"mandatory": { "type": "boolean" }
},
"allOf": [
{
Expand Down Expand Up @@ -167,14 +167,6 @@
"dmgUrl": {
"type": "string",
"pattern": "^https://github\\.com/BasedHardware/omi/releases/download/v[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:%2[Bb]|\\+)[1-9][0-9]*-macos/omi\\.dmg$"
},
"betaZipUrl": {
"type": "string",
"pattern": "^https://github\\.com/BasedHardware/omi/releases/download/v[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:%2[Bb]|\\+)[1-9][0-9]*-macos/Omi\\.Beta\\.zip$"
},
"betaDmgUrl": {
"type": "string",
"pattern": "^https://github\\.com/BasedHardware/omi/releases/download/v[0-9]+\\.[0-9]+(?:\\.[0-9]+)?(?:%2[Bb]|\\+)[1-9][0-9]*-macos/omi-beta\\.dmg$"
}
}
}
35 changes: 6 additions & 29 deletions .github/scripts/check-desktop-auto-beta-candidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

TAG_RE = re.compile(r"^v(?P<version>\d+\.\d+\.\d+)\+(?P<build>\d+)-macos$")
EXPECTED_BUNDLE_ID = "com.omi.computer-macos"
EXPECTED_BETA_BUNDLE_ID = "com.omi.computer-macos.beta"
EXPECTED_TEAM_ID = "9536L8KLMP"
REQUIRED_SMOKE_CHECKS = {
"Launch + identity metadata is aligned",
Expand Down Expand Up @@ -145,7 +144,7 @@ def validate(args: argparse.Namespace) -> dict:
callback_canary = smoke.get("notification_callback_canary")

zip_release = asset_by_name(release, {"Omi.zip"})
dmg_release = asset_by_name(release, {"Omi.dmg", "omi.dmg"})
dmg_release = asset_by_name(release, {"omi.dmg"})
zip_smoke = smoke_artifact(smoke, "sparkle_zip")
dmg_smoke = smoke_artifact(smoke, "dmg")
artifact_digests = {
Expand All @@ -157,32 +156,10 @@ def validate(args: argparse.Namespace) -> dict:
if dmg_smoke.get("sha256") != artifact_digests[dmg_release["name"]]:
fail("published DMG digest does not match the signed artifact smoke")

# Releases that ship the side-by-side Omi Beta identity carry a second smoke
# result; when those assets exist the beta artifact must satisfy the same
# contract as stable (older releases without beta assets stay valid).
beta_assets = {a.get("name") for a in release.get("assets", [])}
if "Omi.Beta.zip" in beta_assets:
if not getattr(args, "beta_smoke_result", "") or not Path(args.beta_smoke_result).exists():
fail("release ships Omi Beta assets but no beta smoke result was provided")
beta_smoke = load_json(args.beta_smoke_result)
_validate_smoke_contract(
beta_smoke,
bundle_id=EXPECTED_BETA_BUNDLE_ID,
release_tag=args.release_tag,
expected_version=expected_version,
expected_build=expected_build,
label="beta",
)
beta_zip_release = asset_by_name(release, {"Omi.Beta.zip"})
beta_dmg_release = asset_by_name(release, {"omi-beta.dmg"})
beta_zip_smoke = smoke_artifact(beta_smoke, "sparkle_zip")
beta_dmg_smoke = smoke_artifact(beta_smoke, "dmg")
artifact_digests["Omi.Beta.zip"] = normalized_digest(beta_zip_release)
artifact_digests["omi-beta.dmg"] = normalized_digest(beta_dmg_release)
if beta_zip_smoke.get("sha256") != artifact_digests["Omi.Beta.zip"]:
fail("published Omi.Beta.zip digest does not match the beta artifact smoke")
if beta_dmg_smoke.get("sha256") != artifact_digests["omi-beta.dmg"]:
fail("published omi-beta.dmg digest does not match the beta artifact smoke")
asset_names = {a.get("name") for a in release.get("assets", [])}
unexpected = sorted(asset_names & {"Omi.Beta.zip", "omi-beta.dmg"})
if unexpected:
fail(f"unexpected dual-identity desktop assets: {', '.join(unexpected)}")

return {
"passed": True,
Expand All @@ -200,7 +177,7 @@ def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--release-json", required=True)
parser.add_argument("--smoke-result", required=True)
parser.add_argument("--beta-smoke-result", default="")

parser.add_argument("--release-tag", required=True)
parser.add_argument("--latest-tag", required=True)
parser.add_argument("--tag-sha", required=True)
Expand Down
3 changes: 1 addition & 2 deletions .github/scripts/check-desktop-prod-promotion-policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
"Authorization: Bearer $ACCESS_TOKEN",
"appcast.xml?identity=stable",
"verify_stable_appcast.py",
".event",
".path",
"desktop_qualification_admission.py",
"--if-generation-match=0",
'operation\": os.environ[\"OPERATION\"]',
)
Expand Down
24 changes: 23 additions & 1 deletion .github/scripts/check-release-process-guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def main() -> int:
errors.extend(check_desktop_codemagic_release())
errors.extend(check_desktop_preview_publishing())
errors.extend(check_desktop_qualification_runner())
errors.extend(check_desktop_update_docs())
errors.extend(check_no_unprovisioned_beta_backend_hosts())
errors.extend(check_mobile_codemagic_release_triggers())
errors.extend(check_docs_workflow_scripts())
Expand Down Expand Up @@ -294,7 +295,7 @@ def check_desktop_qualification_runner() -> list[str]:
"self-hosted",
"macos",
"omi-desktop-qualification",
"ref: main",
"ref: ${{ inputs.release_tag }}",
"docker info",
"check-desktop-auto-beta-candidate.py",
"--automatic",
Expand Down Expand Up @@ -322,6 +323,27 @@ def check_desktop_qualification_runner() -> list[str]:
return errors


def check_desktop_update_docs() -> list[str]:
"""Keep operator docs aligned with the single retained artifact identity."""
path = ROOT / "docs/doc/developer/desktop-updates.mdx"
text = path.read_text(encoding="utf-8") if path.exists() else ""
errors: list[str] = []
required = ("Omi.app", "com.omi.computer-macos", "Omi.zip", "`omi.dmg`", "independent pointers")
forbidden = (
"separately installable",
"own bundle identity",
"all four artifacts",
"Stable/Beta URLs",
)
for fragment in required:
if fragment not in text:
errors.append(f"desktop update docs are missing single-artifact contract: {fragment}")
for fragment in forbidden:
if fragment in text:
errors.append(f"desktop update docs retain forbidden dual-identity claim: {fragment}")
return errors


def check_no_unprovisioned_beta_backend_hosts() -> list[str]:
"""Production-family clients must share the established production backend.

Expand Down
51 changes: 51 additions & 0 deletions .github/scripts/desktop_qualification_admission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Admit only tag-bound trusted qualification evidence for desktop promotion."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any


def validate_qualification_run(run: object, repository: str, release_tag: str, candidate_sha: str) -> None:
"""Require a successful same-repository workflow dispatched on the candidate tag."""
if not isinstance(run, dict):
raise ValueError("qualification run must be an object")
required = {
"conclusion": "success",
"event": "workflow_dispatch",
"path": ".github/workflows/desktop_qualify_beta.yml",
"head_branch": release_tag,
"head_sha": candidate_sha,
"name": "Qualify Desktop Beta Candidate",
}
for key, expected in required.items():
if run.get(key) != expected:
if key == "head_branch":
raise ValueError("qualification run must execute the candidate tag controls")
if key == "head_sha":
raise ValueError("qualification run must execute the candidate source SHA")
raise ValueError(f"qualification run {key} must equal {expected!r}")
for key in ("repository", "head_repository"):
value = run.get(key)
if not isinstance(value, dict) or value.get("full_name") != repository:
raise ValueError(f"qualification run {key} must be the trusted repository")


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--run-json", type=Path, required=True)
parser.add_argument("--repository", required=True)
parser.add_argument("--release-tag", required=True)
parser.add_argument("--candidate-sha", required=True)
args = parser.parse_args()
validate_qualification_run(
json.loads(args.run_json.read_text(encoding="utf-8")), args.repository, args.release_tag, args.candidate_sha
)
return 0


if __name__ == "__main__":
raise SystemExit(main())
17 changes: 9 additions & 8 deletions .github/scripts/desktop_qualification_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import re
from typing import Any

ARTIFACTS = ("Omi.zip", "Omi.dmg", "omi.dmg", "Omi.Beta.zip", "omi-beta.dmg")
ZIP_SIGNATURES = {"Omi.zip": "edSignature", "Omi.Beta.zip": "betaEdSignature"}
ARTIFACTS = ("Omi.zip", "omi.dmg")
ZIP_SIGNATURES = {"Omi.zip": "edSignature"}
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")


Expand Down Expand Up @@ -67,12 +67,9 @@ def build_evidence(
_fail("was not created after the passing candidate gate")
metadata = _metadata(str(release.get("body") or ""))
artifacts: dict[str, dict[str, str]] = {}
stable_dmg = [name for name in ("Omi.dmg", "omi.dmg") if name in files]
if len(stable_dmg) != 1:
_fail("requires exactly one stable DMG file")
required = {"Omi.zip", stable_dmg[0], "Omi.Beta.zip", "omi-beta.dmg"}
required = {"Omi.zip", "omi.dmg"}
if set(files) != required:
_fail("does not contain all four qualified artifacts")
_fail("does not contain the exact qualified Omi.zip and omi.dmg")
for name, path in files.items():
if not path.is_file():
_fail(f"is missing downloaded {name}")
Expand Down Expand Up @@ -119,7 +116,11 @@ def verify_evidence(
_fail("release ID or source SHA does not match the trusted run")
source_qualification = evidence.get("source_qualification")
signed_artifacts = evidence.get("signed_artifact_verification")
if not isinstance(source_qualification, dict) or source_qualification.get("passed") is not True or source_qualification.get("tier") != "T2":
if (
not isinstance(source_qualification, dict)
or source_qualification.get("passed") is not True
or source_qualification.get("tier") != "T2"
):
_fail("does not prove source-built named-bundle T2 qualification")
if not isinstance(signed_artifacts, dict) or signed_artifacts.get("passed") is not True:
_fail("does not prove exact signed artifact verification")
Expand Down
5 changes: 3 additions & 2 deletions .github/scripts/desktop_release_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,11 @@ def collect_snapshot(
"platform",
"version",
"build_number",
"source_sha",
"app_source_sha",
"zip_sha256",
"dmg_sha256",
"qualification",
"qualification_evidence_asset",
"qualification_evidence_sha256",
),
),
"pointers": {
Expand Down
9 changes: 4 additions & 5 deletions .github/scripts/desktop_release_doctor_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,13 @@ def _github_surfaces(snapshot: dict[str, object], release_id: str, phase: str) -

def _manifest_surface(snapshot: dict[str, object], release_id: str, tag_sha: str, phase: str, metadata: dict[str, object]) -> dict[str, object]:
manifest = snapshot.get("manifest", _unavailable("collector did not provide the canonical manifest"))
expected = {"release_id": release_id, "source_sha": tag_sha}
expected = {"release_id": release_id, "app_source_sha": tag_sha}
if _is_unavailable(manifest):
return _unavailable_surface("canonical_manifest", expected, manifest)

manifest_id = _optional_string(manifest.get("release_id")) if isinstance(manifest, dict) else ""
manifest_sha = _optional_string(manifest.get("source_sha")) if isinstance(manifest, dict) else ""
qualification = manifest.get("qualification") if isinstance(manifest, dict) else None
evidence = _optional_string(qualification.get("evidence_asset")) if isinstance(qualification, dict) else ""
manifest_sha = _optional_string(manifest.get("app_source_sha")) if isinstance(manifest, dict) else ""
evidence = _optional_string(manifest.get("qualification_evidence_asset")) if isinstance(manifest, dict) else ""
metadata_evidence = _optional_string(metadata.get("qualifiedBetaEvidence"))
required = phase in {"beta", "stable"}
valid = manifest_id == release_id and manifest_sha == tag_sha and (not required or bool(evidence))
Expand All @@ -216,7 +215,7 @@ def _manifest_surface(snapshot: dict[str, object], release_id: str, tag_sha: str
"PASS" if valid else "FAIL",
"aligned" if valid else "reversible_drift",
{**expected, "qualification_evidence": metadata_evidence or None},
{"release_id": manifest_id or None, "source_sha": manifest_sha or None, "qualification_evidence": evidence or None},
{"release_id": manifest_id or None, "app_source_sha": manifest_sha or None, "qualification_evidence": evidence or None},
message,
repair,
)
Expand Down
29 changes: 19 additions & 10 deletions .github/scripts/desktop_release_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@
"dmg_url",
"dmg_sha256",
"ed_signature",
"beta_zip_url",
"beta_zip_sha256",
"beta_dmg_url",
"beta_dmg_sha256",
"beta_ed_signature",
"qualification_evidence_asset",
"qualification_evidence_sha256",
"qualification_tier",
Expand All @@ -54,6 +49,9 @@
"compatibility_contract",
"environment_contract_version",
"created_at",
"published_at",
"changelog",
"mandatory",
}
)
REQUIRED_FIELDS = frozenset(
Expand All @@ -62,6 +60,9 @@
"desktop_backend_source_sha",
"desktop_backend_oci_index_digest",
"desktop_backend_platform_digest",
"published_at",
"changelog",
"mandatory",
}
)
BACKEND_FIELDS = frozenset(
Expand Down Expand Up @@ -173,6 +174,13 @@ def _require_timestamp(data: dict[str, Any], key: str) -> str:
return value


def _require_changelog(data: dict[str, Any]) -> list[str]:
value = data.get("changelog")
if not isinstance(value, list) or any(not isinstance(item, str) or not item.strip() for item in value):
_fail("changelog must be a list of non-empty strings")
return value


def _validate_compatibility(manifest: dict[str, Any]) -> None:
raw = manifest.get("compatibility_contract")
if not isinstance(raw, dict):
Expand Down Expand Up @@ -227,11 +235,6 @@ def validate_manifest(value: object) -> dict[str, Any]:
_require_release_asset_url(manifest, "dmg_url", release_id=release_id, asset_name="omi.dmg")
_require_sha256(manifest, "dmg_sha256")
_require_string(manifest, "ed_signature")
_require_release_asset_url(manifest, "beta_zip_url", release_id=release_id, asset_name="Omi.Beta.zip")
_require_sha256(manifest, "beta_zip_sha256")
_require_release_asset_url(manifest, "beta_dmg_url", release_id=release_id, asset_name="omi-beta.dmg")
_require_sha256(manifest, "beta_dmg_sha256")
_require_string(manifest, "beta_ed_signature")

evidence_asset = _require_string(manifest, "qualification_evidence_asset")
if not EVIDENCE_ASSET_RE.fullmatch(evidence_asset):
Expand Down Expand Up @@ -262,6 +265,12 @@ def validate_manifest(value: object) -> dict[str, Any]:
if not ENVIRONMENT_CONTRACT_RE.fullmatch(environment_contract):
_fail("environment_contract_version must use desktop-backend-env-vN form")
_require_timestamp(manifest, "created_at")
if "published_at" in manifest:
_require_timestamp(manifest, "published_at")
if "changelog" in manifest:
_require_changelog(manifest)
if "mandatory" in manifest and not isinstance(manifest.get("mandatory"), bool):
_fail("mandatory must be a boolean")
_validate_compatibility(manifest)
return manifest

Expand Down
6 changes: 3 additions & 3 deletions .github/scripts/desktop_repair_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

RELEASE_ID_RE = re.compile(r"^v\d+\.\d+(?:\.\d+)?\+\d+-macos$")
SHA40_RE = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE)
SHA256_RE = re.compile(r"^[0-9a-f]{64}$", re.IGNORECASE)
SHA256_RE = re.compile(r"^sha256:[0-9a-f]{64}$", re.IGNORECASE)


def _required_string(data: dict[str, Any], key: str) -> str:
Expand Down Expand Up @@ -45,9 +45,9 @@ def build_repair_bundle(manifest: dict[str, Any], bucket: str) -> dict[str, Any]
if not isinstance(build_number, int) or isinstance(build_number, bool) or build_number <= 0:
raise ValueError("build_number must be a positive integer")

source_sha = _required_string(manifest, "source_sha")
source_sha = _required_string(manifest, "app_source_sha")
if not SHA40_RE.fullmatch(source_sha):
raise ValueError("source_sha has an invalid digest")
raise ValueError("app_source_sha has an invalid digest")
dmg_sha256 = _required_string(manifest, "dmg_sha256")
if not SHA256_RE.fullmatch(dmg_sha256):
raise ValueError("dmg_sha256 has an invalid digest")
Expand Down
Loading