Files
hermes-agent/tests/scripts/test_render_builds_table.py
ethernet a9793b3ea6 refactor(release): rename the nightly release channel to canary
The fast-moving desktop prerelease channel is now "canary" everywhere:
the tag shape (vX.Y.Z-canary.<ts>), the electron-updater/R2 feed dirs
(canary.yml / releases/<os>/canary/), the update-channel consts and CLI
choices, the MSIX build-number derivation, the App Installer channel
paths, and the Windows Store flight var (MS_STORE_CANARY_FLIGHT_ID).

Also renames the scheduled workflow to canary-release.yml and the
release test file to test_release_canary.py, and flips the CLI flags
(--canary / --prune-canaries / prune-canaries subcommand).

Unrelated "nightly" mentions are untouched: Brave's own browser channel
(browser_connect), cron scheduling prose (README, i18n, cron/browser/
kanban docs, zh-Hans), upstream skill docs (comfyui/unsloth/torchtitan),
evals fixtures, Node's node-nightly prereleases, and cron job names in
gateway tests.

Note: MS_STORE_NIGHTLY_FLIGHT_ID was renamed to MS_STORE_CANARY_FLIGHT_ID
in the workflow — the matching repo/org variable on GitHub must be
renamed in repo settings for the Store flight ring to keep working.
2026-09-01 22:15:17 -04:00

144 lines
6.9 KiB
Python

"""render-builds-table.py: tables from REAL bucket object names, spliced idempotently.
The contract: table rows exist only for objects that are actually in the
R2 bucket for the tag's exact version (missing artifact = missing row,
never a dead link), links point at the R2 public URL, msixbundle / zip /
feed manifests stay out, and re-rendering replaces the previous block
instead of stacking a second one.
Adapted from the restack suite for this branch's artifact shapes: the
Windows per-arch artifact here is .msix (the msixbundle folds both arches
and stays out of the tables), not the NSIS .exe.
"""
import importlib.util
from pathlib import Path
_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "render-builds-table.py"
_SPEC = importlib.util.spec_from_file_location("render_builds_table", _SCRIPT)
assert _SPEC and _SPEC.loader
rbt = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(rbt)
ASSETS = [
"releases/tag/v0.28.0/HermesBundled-0.28.0-mac-arm64.dmg",
"releases/tag/v0.28.0/HermesBundled-0.28.0-mac-arm64.zip", # updater delta target — no row
"releases/tag/v0.28.0/HermesBundled-0.28.0-win-x64.msix",
"releases/tag/v0.28.0/HermesBundled-0.28.0-win-arm64.msix",
"releases/tag/v0.28.0/HermesBundled-0.28.0-win.msixbundle", # store/sideload channel — no row
"releases/tag/v0.28.0/HermesBundled-0.28.0-linux-x64.AppImage",
"releases/tag/v0.28.0/HermesLight-0.28.0-win-x64.msix",
"latest.yml", # feed manifest — no row
"light.yml",
"releases/tag/v0.28.0/HermesBundled-0.28.0-win-x64.msix.blockmap", # no row
]
BASE_URL = "https://releases.example.com"
class TestParseAssets:
def test_only_table_shaped_assets_parse(self):
parsed = rbt.parse_assets(ASSETS)
assert ("mac", "arm64") in parsed["HermesBundled"]
assert ("win", "x64") in parsed["HermesBundled"]
assert ("win", "arm64") in parsed["HermesBundled"]
assert ("linux", "x64") in parsed["HermesBundled"]
assert len(parsed["HermesBundled"]) == 4 # zip/msixbundle/blockmap/yml excluded
assert parsed["HermesLight"] == {("win", "x64"): ("releases/tag/v0.28.0/HermesLight-0.28.0-win-x64.msix", "msix")}
def test_canary_versions_parse(self):
parsed = rbt.parse_assets(["releases/tag/v0.28.0-canary.20260818/HermesBundled-0.28.0-canary.20260818-win-x64.msix"])
assert ("win", "x64") in parsed["HermesBundled"]
def test_flat_names_still_parse(self):
# Names without the releases/tag/ prefix (e.g. from a plain list) are
# handled too — the shape match runs on the basename either way.
parsed = rbt.parse_assets(["HermesBundled-0.28.0-win-x64.msix"])
assert parsed["HermesBundled"][("win", "x64")] == ("HermesBundled-0.28.0-win-x64.msix", "msix")
class TestFilterNamesForVersion:
def test_stable_tag_does_not_match_canary_objects(self):
# '0.28.0' is a prefix of '0.28.0-canary...' — the filter must be
# exact, or a stable table would list canary binaries.
names = [
"releases/tag/v0.28.0/HermesBundled-0.28.0-mac-arm64.dmg",
"releases/tag/v0.28.0-canary.20260818/HermesBundled-0.28.0-canary.20260818-win-x64.msix",
"releases/tag/v0.29.0/HermesBundled-0.29.0-win-x64.msix",
"latest.yml",
]
assert rbt.filter_names_for_version(names, "0.28.0") == [
"releases/tag/v0.28.0/HermesBundled-0.28.0-mac-arm64.dmg",
]
def test_canary_tag_matches_its_objects(self):
names = [
"releases/tag/v0.28.0-canary.20260818/HermesBundled-0.28.0-canary.20260818-win-x64.msix",
"releases/tag/v0.28.0-canary.20260818/HermesBundled-0.28.0-canary.20260818-win-x64.msix.blockmap",
"releases/tag/v0.28.0-canary.20260817/HermesBundled-0.28.0-canary.20260817-win-arm64.msix",
"releases/tag/v0.28.0/HermesBundled-0.28.0-mac-arm64.dmg",
]
assert rbt.filter_names_for_version(names, "0.28.0-canary.20260818") == [
"releases/tag/v0.28.0-canary.20260818/HermesBundled-0.28.0-canary.20260818-win-x64.msix",
]
class TestRenderAndSplice:
def test_rows_only_for_present_assets(self):
block = rbt.render_tables(rbt.parse_assets(ASSETS), BASE_URL)
assert f"{BASE_URL}/releases/tag/v0.28.0/HermesBundled-0.28.0-mac-arm64.dmg" in block
assert "HermesBundled-0.28.0-win.msixbundle" not in block
assert ".zip" not in block
assert ".blockmap" not in block
# A leg that never uploaded leaves no row at all.
assert "linux-arm64" not in block
def test_links_point_at_the_r2_base_url(self):
block = rbt.render_tables(rbt.parse_assets(ASSETS), BASE_URL)
assert "github.com" not in block
assert f"{BASE_URL}/releases/tag/v0.28.0/HermesLight-0.28.0-win-x64.msix" in block
def test_base_url_trailing_slash_is_stripped(self):
block = rbt.render_tables(rbt.parse_assets(ASSETS), f"{BASE_URL}/")
assert f"{BASE_URL}/releases/tag/v0.28.0/HermesBundled-0.28.0-mac-arm64.dmg" in block
assert f"{BASE_URL}//releases" not in block
def test_splice_replaces_marker_and_is_idempotent(self):
body = f"# Notes\n\n{rbt.MARKER}\n\n## Changes"
block = rbt.render_tables(rbt.parse_assets(ASSETS), BASE_URL)
once = rbt.splice(body, block)
assert "## Downloads" in once
assert once.count(rbt.MARKER) == 1
# Second render (e.g. a re-run with more assets) replaces, not stacks.
twice = rbt.splice(once, block)
assert twice.count("## Downloads") == 1
assert twice.count(rbt.END_MARKER) == 1
def test_no_marker_leaves_body_alone(self):
block = rbt.render_tables(rbt.parse_assets(ASSETS), BASE_URL)
assert rbt.splice("no marker here", block) == "no marker here"
class TestPendingPlaceholder:
def test_final_render_replaces_the_pending_link(self):
# The lifecycle: marker → pending link (builds-pending job) →
# tables (builds-table job). The link must not survive step 3.
body = f"# Notes\n\n{rbt.MARKER}\n\n## Changes"
pending = rbt.render_pending("https://github.com/o/r/actions/runs/123")
with_pending = rbt.splice(body, pending)
assert "actions/runs/123" in with_pending
assert with_pending.count(rbt.MARKER) == 1 # wrapper survives for step 3
tables = rbt.render_tables(rbt.parse_assets(ASSETS), BASE_URL)
final = rbt.splice(with_pending, tables)
assert "actions/runs/123" not in final
assert "## Downloads" in final
assert final.count(rbt.END_MARKER) == 1
def test_pending_rerender_replaces_not_stacks(self):
once = rbt.splice(rbt.MARKER, rbt.render_pending("https://x/runs/1"))
twice = rbt.splice(once, rbt.render_pending("https://x/runs/2"))
assert "https://x/runs/1" not in twice
assert twice.count("https://x/runs/2") == 1
assert twice.count(rbt.END_MARKER) == 1