Files
hermes-agent/scripts/bundles/desktop_inputs.py
ethernet b37acb8389 feat(release): dynamic R2-owned channels and preview retirement (rounds 1-2)
Checkpoint before round-3 reduction (two-tier retirement derived from
product identity). Includes:

- R2 channel protocol (release_channels.py, channel-protocol.ts): records,
  builds, manifests, retired channels with pinned destinationHead and
  receiverProtocol; fail-closed readers in both languages
- One shared native manifest/feed writer (scripts/bundles/channel_artifacts.py)
- Scoped/disposable R2 publication, fork isolation before credential
  access, canary bootstrap verification of its own promoted outputs
- Channel source CLI: typed SourceTarget, source-channel resolution,
  retirement downgrade refusal
- Desktop channel resolver/strategy, install-stamp/build-stamp receiver
  ownership (stable-owned S/T candidates), single-flight updater operation
- Cross-package retirement machinery (receiver/host/preservation/
  compatibility/connections/dialog/discovery, backup_migration strict
  snapshots with retained-link inventory, empty-dir preservation,
  connection-collision resolution, URL-credential rejection)
- Native install harness (tests/install/channel-retirement-*) and
  install-e2e retirement jobs
- checkout-source.test.ts transport shim now covers build_opener().open
  (was silently hitting the real network in CI)

Removed secondary certification protocol (channel_qualification.py) per
approved round-2 plan. All focused suites green at checkpoint; native
cross-package journeys unverified (to be deleted in round 3).
2026-09-14 10:26:36 -04:00

103 lines
4.4 KiB
Python

"""Build identity and process environment from an admitted preparation."""
from __future__ import annotations
from collections.abc import Mapping
from contextlib import contextmanager
import json
import re
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from scripts.bundles.desktop_prepare import BuildRequest, PreparedDesktop
@contextmanager
def build_lock(source: Path):
from hermes_cli.runtime_state import _lock
directory = source / ".build"
if directory.is_symlink():
raise ValueError("desktop build scratch must not be a symlink")
directory.mkdir(exist_ok=True)
with (directory / ".desktop-build.lock").open("a+b") as lock:
if not _lock(lock.fileno(), wait=False):
raise ValueError("another desktop build or preparation is using this source checkout")
yield
def build_environment(prepared: PreparedDesktop, variant: str, inherited: Mapping[str, str]) -> dict[str, str]:
from pm.build_operations import verified_tools
from pm.lock import Lockfile
from scripts.bundles.desktop_toolchain import bootstrap_environment
request = prepared.request
env = bootstrap_environment(request.source, request.work, request.cache, inherited)
selection = verified_tools(["python", "node", "npm"], source_store=request.cache / "tools",
target=request.target, lock=Lockfile(request.source / "pm/lock.json"))
if prepared.python != selection.entries["python"].binary or prepared.node != selection.entries["node"].binary:
raise ValueError("prepared Python/Node paths do not match selected tools; prepare again")
env = selection.environment(env)
env["HERMES_PYTHON"] = str(prepared.python)
return identity_environment(request, variant, env)
def identity_environment(request: BuildRequest, variant: str, inherited: Mapping[str, str]) -> dict[str, str]:
"""One build-only bridge shared by dependency workers, stamp and packagers."""
request.validate_channel()
if request.channel_request is not None and variant != "bundled":
raise ValueError("channel builds currently support only the bundled variant")
env = dict(inherited)
env.update(CI="true", PYTHONUTF8="1", GITHUB_SHA=request.commit,
HERMES_DESKTOP_VARIANT=variant,
HERMES_PAYLOAD_VERSION=request.version,
HERMES_BUNDLE_ENV_JSON=json.dumps(request.bundle_env, sort_keys=True))
env.pop("BUILD_NUMBER", None)
env.pop("GITHUB_HEAD_REF", None)
env.pop("_HERMES_CHANNEL_REQUEST_JSON", None)
if request.channel_request is not None:
env["_HERMES_CHANNEL_REQUEST_JSON"] = json.dumps(request.channel_request, sort_keys=True)
env["GITHUB_REPOSITORY"] = request.channel_request["repository"]
for key in ("HERMES_BUILD_COMMIT", "HERMES_PAYLOAD_TAG", "GITHUB_REF_NAME"):
env.pop(key, None)
elif request.tag is None:
env["HERMES_BUILD_COMMIT"] = request.commit
env.pop("HERMES_PAYLOAD_TAG", None)
env.pop("GITHUB_REF_NAME", None)
else:
env.pop("HERMES_BUILD_COMMIT", None)
env["HERMES_PAYLOAD_TAG"] = request.tag
env["GITHUB_REF_NAME"] = request.tag
return env
def validate_builder_identity(request: BuildRequest, args: list[str]) -> None:
if request.channel_request is not None and any(
re.match(r"(?:-c|--config)\.extraMetadata\.(?:version|shortVersion|shortVersionWindows)=", arg)
for arg in args
):
raise ValueError("channel package versions come only from the admitted request")
def packaging_environment(build: Mapping[str, str], inherited: Mapping[str, str],
target: str) -> dict[str, str]:
env = dict(build)
if target.startswith("darwin-"):
# Security.framework needs the login HOME for both key import and signing,
# even with an explicit keychain. Keep dependency preparation isolated.
env["HOME"] = inherited.get("HERMES_REAL_HOME") or inherited.get("HOME") or str(Path.home())
return env
def select_variant(prepared: PreparedDesktop, variant: str | None) -> str:
from scripts.termux.deb_version import channel_for_tag
request = prepared.request
variant = variant or request.variant
if variant != request.variant and not (
{variant, request.variant} == {"bundled", "store"} and request.tag
and channel_for_tag(request.tag) == "stable"
):
raise ValueError("requested variant was not prepared")
return variant