A preview channel always minted its own side-by-side identity (Hermes NAME, ai.hermes.channel.h<token>), and a plain --build-commit carries Hermes Agent <sha7> with a red icon, so there was no way to ship an exact commit under the official name, icon and package ID. --branding stable (with --channel) makes channel creation copy the identity from the published stable channel record instead of reserving a new token. Branding is fixed at creation like the rest of the identity: re-dispatching with the other branding is refused, as is stable branding before a stable is published or in a disposable namespace. --branding without --channel is refused rather than silently falling back to a sha-branded one-off build. The workflow gains a `branding` input, forwarded to the allocation step and the dispatch log so the printed release.py replay matches what was sent.
118 lines
5.9 KiB
Python
118 lines
5.9 KiB
Python
"""Channel dispatch names an exact pushed Git commit; CI allocates and builds."""
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
|
|
def git(repo, *args):
|
|
return subprocess.check_output(["git", *args], cwd=repo, text=True, encoding="utf-8").strip()
|
|
|
|
|
|
@pytest.fixture
|
|
def source(tmp_path):
|
|
origin = tmp_path / "origin.git"
|
|
repo = tmp_path / "source"
|
|
origin.mkdir()
|
|
repo.mkdir()
|
|
git(origin, "init", "--bare", "--quiet")
|
|
git(repo, "init", "--initial-branch=main", "--quiet")
|
|
git(repo, "config", "user.name", "Test")
|
|
git(repo, "config", "user.email", "test@example.test")
|
|
(repo / "pyproject.toml").write_text('[project]\nname="fixture"\nversion="1.2.3"\n', encoding="utf-8")
|
|
git(repo, "add", "pyproject.toml")
|
|
git(repo, "commit", "--quiet", "-m", "Initial")
|
|
git(repo, "remote", "add", "origin", str(origin))
|
|
git(repo, "push", "--quiet", "-u", "origin", "main")
|
|
return repo
|
|
|
|
|
|
def test_dispatch_names_channel_and_binds_to_pushed_source(source):
|
|
from scripts.releases.channel_build import prepare_build
|
|
calls = []
|
|
options = dict(name="custom-branch", revision="main", remote="origin", repo=source,
|
|
repository="example/hermes-agent", default_branch="main",
|
|
dispatch=calls.append, bundle_env={"HERMES_GUEST_ONBOARDING": "1"})
|
|
dry = prepare_build(**options)
|
|
assert dry["commit"] == git(source, "rev-parse", "HEAD")
|
|
assert dry["sourceVersion"] == "1.2.3"
|
|
assert not calls
|
|
command = dry["command"]
|
|
assert "desktop-bundled-release.yml" in command
|
|
assert "channel=custom-branch" in command
|
|
assert "build_commit=" + dry["commit"] in command
|
|
assert any(value.startswith("bundle_env=") for value in command)
|
|
assert "--ref" in command and command[command.index("--ref") + 1] == "main"
|
|
assert not any(value.startswith("branding=") for value in command)
|
|
stable = prepare_build(**options, branding="stable")
|
|
assert "branding=stable" in stable["command"] and stable["branding"] == "stable"
|
|
admitted = prepare_build(**options, publish=True)
|
|
assert calls == [command]
|
|
assert admitted["commit"] == dry["commit"]
|
|
# An unpublished commit is never dispatched.
|
|
git(source, "commit", "--allow-empty", "--quiet", "-m", "Unpublished")
|
|
with pytest.raises(ValueError, match="pushed"):
|
|
prepare_build(**options, publish=True)
|
|
assert len(calls) == 1
|
|
|
|
|
|
def test_missing_default_branch_is_rejected_without_dispatch(source):
|
|
from scripts.releases.channel_build import prepare_build
|
|
from hermes_cli.release_channels import ChannelError
|
|
with pytest.raises(ChannelError, match="default branch"):
|
|
prepare_build(name="invalid-controller", revision="main", remote="origin", repo=source,
|
|
repository="example/hermes-agent", default_branch="",
|
|
dispatch=lambda command: pytest.fail("must not dispatch"), publish=True)
|
|
|
|
|
|
def test_disposable_scope_is_opt_in_and_lease_bound(monkeypatch):
|
|
from scripts.releases import channel_build, r2
|
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
|
monkeypatch.delenv("GITHUB_REPOSITORY", raising=False)
|
|
monkeypatch.delenv("R2_DISPOSABLE_RUN", raising=False)
|
|
monkeypatch.setenv("CLOUDFLARE_R2_PUBLIC_URL", "https://archive.example")
|
|
# No lease and no repository identity check: scoping is opt-in, the
|
|
# unscoped publisher talks production keys only when the caller holds them.
|
|
monkeypatch.setattr(r2, "credentials", lambda: ({"access_key_id": "inert", "secret_key": "inert"}, "https://r2.example", "bucket"))
|
|
unscoped = channel_build.configured_publisher("ethernet8023/hermes-agent")
|
|
assert unscoped.store.scope.key("releases/channels/preview.json") == "releases/channels/preview.json"
|
|
monkeypatch.setenv("R2_DISPOSABLE_RUN", "123")
|
|
monkeypatch.setenv("GITHUB_REPOSITORY_ID", "456")
|
|
monkeypatch.setattr(channel_build.commit_build, "output", lambda args: "789")
|
|
with pytest.raises(ValueError, match="another repository"):
|
|
channel_build.configured_publisher("ethernet8023/hermes-agent")
|
|
monkeypatch.setattr(channel_build.commit_build, "output", lambda args: "456")
|
|
publisher = channel_build.configured_publisher("ethernet8023/hermes-agent")
|
|
assert publisher.store.scope.key("releases/channels/preview.json") == "ci-disposable/456/123/releases/channels/preview.json"
|
|
assert publisher.reader.base_url == "https://archive.example/ci-disposable/456/123"
|
|
|
|
|
|
def test_release_parser_preserves_plain_oneoff_dispatch(monkeypatch):
|
|
import sys
|
|
from scripts import release
|
|
from scripts.releases import commit_build
|
|
calls = []
|
|
monkeypatch.setattr(commit_build, "cmd_build_commit", lambda args: calls.append(("oneoff", args.build_commit)))
|
|
monkeypatch.setattr(sys, "argv", ["release.py", "--build-commit", "main"])
|
|
release.main()
|
|
assert calls == [("oneoff", "main")]
|
|
monkeypatch.setattr(sys, "argv", ["release.py", "--channel", "bad/name", "--build-commit", "main"])
|
|
with pytest.raises(SystemExit) as rejected:
|
|
release.main()
|
|
assert rejected.value.code == 2
|
|
from scripts.releases import channel_build
|
|
monkeypatch.setattr(channel_build, "cmd_channel", lambda args: calls.append(("channel", args.channel)))
|
|
monkeypatch.setattr(sys, "argv", ["release.py", "--channel", "unknown-name", "--build-commit", "main"])
|
|
release.main()
|
|
assert calls[-1] == ("channel", "unknown-name")
|
|
monkeypatch.setattr(sys, "argv", ["release.py", "--channels", "--bundle-env", "X=y"])
|
|
with pytest.raises(SystemExit) as incompatible:
|
|
release.main()
|
|
assert incompatible.value.code == 2
|
|
monkeypatch.setattr(sys, "argv", ["release.py", "--branding", "stable", "--build-commit", "main"])
|
|
with pytest.raises(SystemExit) as orphan:
|
|
release.main()
|
|
assert orphan.value.code == 2
|
|
monkeypatch.setattr(sys, "argv", ["release.py", "--channel", "unknown-name", "--branding", "stable", "--build-commit", "main"])
|
|
release.main()
|
|
assert calls[-1] == ("channel", "unknown-name")
|