test(ci): resolve commit and channel staging jobs by role
test_commit_build_staging and test_channel_build_publication ran staging scripts from jobs named by id and step name, and checked literal `if:` substrings. The per-arch build split broke them for that reason alone. - Staging scripts come from the native leg for a target and trust branch, the universal assembler, or the termux builder. Within the job, the step is found by the receipt it stages (--commit-build or --channel-request). The summary and channel publisher are found by the table they render. - Summary needs come from the summary job's own needs, not a copied list. - "Selection env mentions needs.<id>-commit.result" becomes "no native leg is fail-fast". That is what the commit summary relies on. The selection logic itself is evaluated in test_desktop_bundle_smoke. - Channel exclusion is evaluated: on every native leg the tag/commit stage stands down for a pinned channel build and the pinned-request stage runs. The commit-only status page is evaluated to run for a commit build and to stand down for channel and disposable-channel dispatches.
This commit is contained in:
@@ -18,6 +18,10 @@ import pytest
|
||||
from hermes_cli.release_channels import canonical_json
|
||||
from scripts.releases import channel_publish, handoff, r2
|
||||
from scripts.releases.channels import preview_identity
|
||||
from tests.ci.desktop_release_roles import (
|
||||
DOWNLOADABLE_DISPATCHES, admitted, channel_publisher, commit_summary, gate, native_builds, needs_of, stage_step,
|
||||
universal_assembler,
|
||||
)
|
||||
from tests.ci.test_desktop_release_tag_admission import _seed_repo, _git, _workflow
|
||||
from tests.scripts.test_release_r2 import r2_server # noqa: F401
|
||||
|
||||
@@ -244,7 +248,8 @@ def test_real_workflow_admission_and_public_smoke_fetch(tmp_path, r2_server, sta
|
||||
@pytest.mark.platforms("posix")
|
||||
def test_workflow_promotion_missing_native_gate_does_not_write(tmp_path, r2_server, staged_channel):
|
||||
request, _ = staged_channel
|
||||
script = workflow_step("desktop-bundled-release.yml", "publish-channel", "Publish immutable feeds and manifest, then CAS channel head")
|
||||
script = workflow_step("desktop-bundled-release.yml", channel_publisher(_workflow()["jobs"]),
|
||||
"Publish immutable feeds and manifest, then CAS channel head")
|
||||
env = {"CHANNEL_BUILD": request["buildId"], "CHANNEL_REQUEST_SHA256": hashlib.sha256(canonical_json(request)).hexdigest(),
|
||||
"CLOUDFLARE_R2_PUBLIC_URL": request["publicBase"], "GITHUB_REPOSITORY": request["repository"],
|
||||
"RUNNER_TEMP": str(tmp_path), "RELEASE_NEEDS": "{}"}
|
||||
@@ -265,7 +270,8 @@ def test_real_publication_cas_and_manifest_summary(tmp_path, r2_server, staged_c
|
||||
"policy": "preview", "state": "active", "revision": 1, "nextSequence": 8,
|
||||
"identity": request["identity"], "head": None}
|
||||
r2_server.store[channel_key] = (canonical_json(record), '"record"')
|
||||
script = workflow_step("desktop-bundled-release.yml", "publish-channel", "Publish immutable feeds and manifest, then CAS channel head")
|
||||
script = workflow_step("desktop-bundled-release.yml", channel_publisher(_workflow()["jobs"]),
|
||||
"Publish immutable feeds and manifest, then CAS channel head")
|
||||
env = {"CHANNEL_BUILD": request["buildId"], "CHANNEL_REQUEST_SHA256": hashlib.sha256(canonical_json(request)).hexdigest(),
|
||||
"CLOUDFLARE_R2_PUBLIC_URL": request["publicBase"], "GITHUB_REPOSITORY": request["repository"],
|
||||
"RUNNER_TEMP": str(tmp_path), "GITHUB_STEP_SUMMARY": str(tmp_path / "summary.md"),
|
||||
@@ -350,7 +356,8 @@ def test_channel_windows_record_stage_and_assembly_handoff_shell(tmp_path, r2_se
|
||||
prefix = handoff.channel_prefix(request)
|
||||
for key in ("metadata-windows-x64.json", "handoff-win32-x64.json"):
|
||||
r2_server.store.pop(prefix + key)
|
||||
script = workflow_step("desktop-bundled-release.yml", "build-win32-x64-commit", "Record and stage channel windows packages")
|
||||
jobs = _workflow()["jobs"]
|
||||
script = stage_step(jobs[native_builds(jobs)[("win32-x64", "commit")]], channel=True)["run"]
|
||||
result = run_shell(tmp_path, r2_server, script, env)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
# One-dispatch: the env-default commit equals the request's own, which is
|
||||
@@ -368,7 +375,7 @@ def test_channel_windows_record_stage_and_assembly_handoff_shell(tmp_path, r2_se
|
||||
metadata = json.loads((release / "metadata-windows-x64.json").read_text(encoding="utf-8"))
|
||||
assert metadata["identity"] == request["identity"]["msixAppIdWithOrg"]
|
||||
assert metadata["applicationId"] == request["identity"]["appNamePascal"]
|
||||
fetch = workflow_step("desktop-bundled-release.yml", "assemble-win32-bundle", "Retrieve Windows packages from R2")
|
||||
fetch = workflow_step("desktop-bundled-release.yml", universal_assembler(jobs), "Retrieve Windows packages from R2")
|
||||
result = run_shell(tmp_path, r2_server, fetch, env)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert sorted(p.name for p in release.glob("*.msix")) == sorted(p.name for p in build.glob("*.msix"))
|
||||
@@ -493,35 +500,40 @@ def test_tag_and_commit_staging_never_runs_for_a_pinned_channel_build():
|
||||
alone, so every other handoff staging step must be gated off whenever a
|
||||
channel build is pinned.
|
||||
"""
|
||||
workflow = _workflow()
|
||||
seen = set()
|
||||
for job in ("build-win32-x64-commit", "build-win32-arm64-commit",
|
||||
"build-darwin-arm64-commit", "build-darwin-x64-commit"):
|
||||
for step in workflow["jobs"][job]["steps"]:
|
||||
run = step.get("run", "") if isinstance(step, dict) else ""
|
||||
if "scripts.releases.handoff stage" not in run:
|
||||
continue
|
||||
if "--channel-request" in run:
|
||||
continue
|
||||
seen.add(step["name"])
|
||||
assert "needs.validate.outputs.channel-build == ''" in (step.get("if") or ""), (
|
||||
f"{step['name']!r} stages tag/commit receipts without excluding channel builds")
|
||||
assert seen, "walk broken: no non-channel handoff staging steps found"
|
||||
jobs = _workflow()["jobs"]
|
||||
channel = admitted(["validate"], channel=True)
|
||||
for name in native_builds(jobs).values():
|
||||
step = stage_step(jobs[name])
|
||||
# Every trust branch stages tag/commit receipts through this step, so
|
||||
# it must stand down for a pinned channel build and run otherwise.
|
||||
assert not gate(step.get("if", "true"), DOWNLOADABLE_DISPATCHES["channel"], channel, job_if=False), (
|
||||
f"{name}: {step['name']!r} stages tag/commit receipts for a channel build")
|
||||
for dispatch in ("tag", "candidate", "commit"):
|
||||
assert gate(step.get("if", "true"), DOWNLOADABLE_DISPATCHES[dispatch], admitted(["validate"]),
|
||||
job_if=False), (name, dispatch)
|
||||
# The pinned-request stage is the one that runs instead.
|
||||
pinned = stage_step(jobs[name], channel=True)
|
||||
assert gate(pinned["if"], DOWNLOADABLE_DISPATCHES["channel"], channel, job_if=False), name
|
||||
|
||||
|
||||
def test_commit_only_status_page_is_not_published_for_a_channel_build():
|
||||
"""Two renderers exist; only the channel one may run for a channel build.
|
||||
|
||||
``publish-channel`` renders the channel matrix from the pinned request.
|
||||
The channel publisher renders the channel matrix from the pinned request.
|
||||
Letting the commit-only summary run too would fetch commit-namespace
|
||||
receipts that a channel build never wrote and publish a page claiming the
|
||||
commit's binaries were not built.
|
||||
"""
|
||||
workflow = _workflow()
|
||||
channel = next(step for step in workflow["jobs"]["publish-channel"]["steps"]
|
||||
if "render-builds-table.py" in step.get("run", ""))
|
||||
assert "--channel-build" in channel["run"]
|
||||
summary = workflow["jobs"]["commit-builds-summary"]
|
||||
assert "inputs.channel == ''" in summary["if"], (
|
||||
jobs = _workflow()["jobs"]
|
||||
channel_publisher(jobs) # the channel renderer exists; it fails loudly otherwise
|
||||
summary = jobs[commit_summary(jobs)]
|
||||
|
||||
def runs(inputs, *, channel=False):
|
||||
needs = admitted(needs_of(summary), channel=channel)
|
||||
needs["validate"]["outputs"]["all-jobs"] = "true"
|
||||
return gate(summary["if"], inputs, needs)
|
||||
|
||||
assert runs(DOWNLOADABLE_DISPATCHES["commit"])
|
||||
assert not runs(DOWNLOADABLE_DISPATCHES["channel"], channel=True), (
|
||||
"the commit-only status page must not run for a channel dispatch")
|
||||
assert "inputs.disposable_channel == ''" in summary["if"]
|
||||
assert not runs({**DOWNLOADABLE_DISPATCHES["commit"], "disposable_channel": "native-preview"})
|
||||
|
||||
@@ -12,6 +12,9 @@ from urllib.parse import quote, unquote
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.ci.desktop_release_roles import (
|
||||
commit_summary, native_builds, needs_of, selection_gates, stage_step, termux_builder, universal_assembler,
|
||||
)
|
||||
from tests.ci.test_desktop_release_tag_admission import _BASH, _child_env, _workflow
|
||||
from tests.scripts.test_release_r2 import r2_server # noqa: F401
|
||||
|
||||
@@ -66,6 +69,7 @@ def test_failed_commit_summary_publishes_downloads_or_run_links(tmp_path, r2_ser
|
||||
base = f'http://127.0.0.1:{r2_server.server_port}/hermes-releases'
|
||||
summary = tmp_path / 'summary.md'
|
||||
jobs = _workflow()['jobs']
|
||||
summary_job = commit_summary(jobs)
|
||||
bundle_env = {'HERMES_HOME': None, 'HERMES_SKIP_INTRO': '',
|
||||
'HERMES_SHARED_AUTH_DIR': '<script>\n"café" & value</script>'}
|
||||
env = dict(HERMES_BUILD_COMMIT=sha, HERMES_PAYLOAD_TAG='', RELEASE_COMMIT=sha,
|
||||
@@ -76,14 +80,15 @@ def test_failed_commit_summary_publishes_downloads_or_run_links(tmp_path, r2_ser
|
||||
CLOUDFLARE_R2_ACCOUNT_ID='loopback', CLOUDFLARE_R2_ACCESS_KEY_ID='test-inert',
|
||||
CLOUDFLARE_R2_SECRET_ACCESS_KEY='test-inert', CLOUDFLARE_R2_BUCKET='hermes-releases',
|
||||
RELEASE_NEEDS=json.dumps({name: {'result': 'success' if name == 'validate' else 'failure'}
|
||||
for name in jobs['commit-builds-summary']['needs']}))
|
||||
for name in needs_of(jobs[summary_job])}))
|
||||
if has_download:
|
||||
artifact = tmp_path / 'apps/desktop/release/HermesBundled-0.33.0-win-x64.msix'
|
||||
artifact.parent.mkdir(parents=True)
|
||||
artifact.write_bytes(b'inert downloadable fixture')
|
||||
staged = shell_step(tmp_path, r2_server, 'build-win32-x64-commit', 'Stage Windows packages to R2', env)
|
||||
producer = jobs[native_builds(jobs)[('win32-x64', 'commit')]]
|
||||
staged = shell_step(tmp_path, r2_server, '', '', env, script=stage_step(producer)['run'])
|
||||
assert staged.returncode == 0, staged.stdout + staged.stderr
|
||||
result = shell_step(tmp_path, r2_server, 'commit-builds-summary',
|
||||
result = shell_step(tmp_path, r2_server, summary_job,
|
||||
'Render the full expected-binary matrix', env)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
text = summary.read_text(encoding='utf-8-sig')
|
||||
@@ -109,13 +114,11 @@ def test_failed_commit_summary_publishes_downloads_or_run_links(tmp_path, r2_ser
|
||||
elif line.startswith('| Linux'):
|
||||
assert 'Disabled' in line and '](' not in line
|
||||
assert all(key.startswith(f'releases/commit/{sha}/') for key in r2_server.store)
|
||||
for name in ('build-win32-x64', 'build-win32-arm64', 'build-darwin-arm64', 'build-darwin-x64'):
|
||||
# The commit/release split collapsed into one leg per arch; the
|
||||
# result job's env still encodes exactly-which-trust-branch-succeeded.
|
||||
result_job = jobs[name]
|
||||
assert f"needs.{name}-commit.result" in result_job['env']['SELECTED_BUILD_SUCCEEDED']
|
||||
assert jobs[f'{name}-commit']['strategy']['fail-fast'] is False
|
||||
step = next(step for step in jobs['commit-builds-summary']['steps'] if 'run' in step)
|
||||
# One failed arch must not cancel its siblings: the summary above only
|
||||
# has downloads to offer for the legs that were allowed to finish.
|
||||
for name in native_builds(jobs).values():
|
||||
assert jobs[name]['strategy']['fail-fast'] is False, name
|
||||
step = next(step for step in jobs[summary_job]['steps'] if 'run' in step)
|
||||
assert step['env']['RUN_URL'] == '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||
assert step['env']['HERMES_BUNDLE_ENV_JSON'] == '${{ inputs.bundle_env }}'
|
||||
|
||||
@@ -130,27 +133,29 @@ def test_commit_staging_and_summary_bind_every_produced_file_without_channels(tm
|
||||
CLOUDFLARE_R2_SECRET_ACCESS_KEY='test-inert', CLOUDFLARE_R2_BUCKET='hermes-releases')
|
||||
release = tmp_path / 'apps/desktop/release'
|
||||
release.mkdir(parents=True)
|
||||
jobs = _workflow()['jobs']
|
||||
legs = native_builds(jobs)
|
||||
gates = selection_gates(jobs)
|
||||
summary_job = commit_summary(jobs)
|
||||
producers = [
|
||||
('build-win32-x64-commit', 'Stage Windows packages to R2', 'win32-x64', [
|
||||
'HermesBundled-0.33.0-win-x64.msix']),
|
||||
('build-win32-arm64-commit', 'Stage Windows packages to R2', 'win32-arm64', [
|
||||
'HermesBundled-0.33.0-win-arm64.msix']),
|
||||
('build-darwin-arm64-commit', 'Stage macOS packages and feed inputs to R2', 'darwin-arm64', [
|
||||
(legs[('win32-x64', 'commit')], 'win32-x64', ['HermesBundled-0.33.0-win-x64.msix']),
|
||||
(legs[('win32-arm64', 'commit')], 'win32-arm64', ['HermesBundled-0.33.0-win-arm64.msix']),
|
||||
(legs[('darwin-arm64', 'commit')], 'darwin-arm64', [
|
||||
'HermesBundled-0.33.0-mac-arm64.dmg', 'HermesBundled-0.33.0-mac-arm64.zip',
|
||||
'HermesBundled-0.33.0-mac-arm64.zip.blockmap']),
|
||||
('build-darwin-x64-commit', 'Stage macOS packages and feed inputs to R2', 'darwin-x64', [
|
||||
(legs[('darwin-x64', 'commit')], 'darwin-x64', [
|
||||
'HermesBundled-0.33.0-mac-x64.dmg', 'HermesBundled-0.33.0-mac-x64.zip',
|
||||
'HermesBundled-0.33.0-mac-x64.zip.blockmap']),
|
||||
('assemble-win32-bundle', 'Stage universal bundles to R2', 'windows-universal', [
|
||||
'HermesBundled-0.33.0.0-win.msixbundle']),
|
||||
(universal_assembler(jobs), 'windows-universal', ['HermesBundled-0.33.0.0-win.msixbundle']),
|
||||
]
|
||||
artifact_keys = set()
|
||||
for job, name, target, names in producers:
|
||||
for job, target, names in producers:
|
||||
for file in release.iterdir():
|
||||
file.unlink()
|
||||
for filename in names:
|
||||
(release / filename).write_bytes(f'transport fixture: {filename}'.encode())
|
||||
result = shell_step(tmp_path, r2_server, job, name, {**env, 'TARGET': target})
|
||||
result = shell_step(tmp_path, r2_server, '', '', {**env, 'TARGET': target},
|
||||
script=stage_step(jobs[job])['run'])
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
receipt_key = f'releases/commit/{sha}/handoff-{target}.json'
|
||||
receipt = json.loads(r2_server.store[receipt_key][0])
|
||||
@@ -160,15 +165,15 @@ def test_commit_staging_and_summary_bind_every_produced_file_without_channels(tm
|
||||
puts = [path for method, path, _ in r2_server.requests if method == 'PUT']
|
||||
assert puts[-1].endswith(receipt_key)
|
||||
|
||||
termux_name = 'Stage the commit-build deb to R2'
|
||||
termux_stage = stage_step(jobs[termux_builder(jobs)])['run']
|
||||
before = dict(r2_server.store)
|
||||
missing = shell_step(tmp_path, r2_server, 'termux-deb', termux_name, env)
|
||||
missing = shell_step(tmp_path, r2_server, '', '', env, script=termux_stage)
|
||||
assert missing.returncode != 0
|
||||
assert r2_server.store == before
|
||||
deb = tmp_path / 'termux-build/deb/hermes agent_0.33.0~commit.aaaaaaaaaaaa_aarch64.deb'
|
||||
deb.parent.mkdir(parents=True)
|
||||
deb.write_bytes(b'transport fixture, not a native Debian package')
|
||||
staged = shell_step(tmp_path, r2_server, 'termux-deb', termux_name, env)
|
||||
staged = shell_step(tmp_path, r2_server, '', '', env, script=termux_stage)
|
||||
assert staged.returncode == 0, staged.stdout + staged.stderr
|
||||
artifact_keys.add(f'releases/commit/{sha}/deb/{deb.name}')
|
||||
receipt = json.loads(r2_server.store[f'releases/commit/{sha}/handoff-termux.json'][0])
|
||||
@@ -176,14 +181,8 @@ def test_commit_staging_and_summary_bind_every_produced_file_without_channels(tm
|
||||
|
||||
summary = tmp_path / 'summary.md'
|
||||
summary_env = {**env, 'GITHUB_STEP_SUMMARY': str(summary), 'RELEASE_NEEDS': json.dumps({
|
||||
'validate': {'result': 'success'}, 'build-win32-x64': {'result': 'success'},
|
||||
'build-win32-arm64': {'result': 'success'}, 'build-darwin-arm64': {'result': 'success'},
|
||||
'build-darwin-x64': {'result': 'success'},
|
||||
'assemble-win32-bundle': {'result': 'success'},
|
||||
'termux-deb': {'result': 'success'}, 'build-linux-x64': {'result': 'success'},
|
||||
'build-linux-arm64': {'result': 'success'},
|
||||
})}
|
||||
result = shell_step(tmp_path, r2_server, 'commit-builds-summary',
|
||||
name: {'result': 'success'} for name in needs_of(jobs[summary_job])})}
|
||||
result = shell_step(tmp_path, r2_server, summary_job,
|
||||
'Render the full expected-binary matrix', summary_env)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
text = summary.read_text(encoding='utf-8-sig')
|
||||
@@ -207,7 +206,7 @@ def test_commit_staging_and_summary_bind_every_produced_file_without_channels(tm
|
||||
receipt_key = f'releases/commit/{sha}/handoff-win32-x64.json'
|
||||
original = r2_server.store[receipt_key]
|
||||
r2_server.store[receipt_key] = (b'not-json', '"invalid"')
|
||||
failed = shell_step(tmp_path, r2_server, 'commit-builds-summary',
|
||||
failed = shell_step(tmp_path, r2_server, summary_job,
|
||||
'Render the full expected-binary matrix', summary_env)
|
||||
assert failed.returncode != 0
|
||||
assert summary.read_text(encoding='utf-8-sig') == text
|
||||
@@ -218,8 +217,8 @@ def test_commit_staging_and_summary_bind_every_produced_file_without_channels(tm
|
||||
r2_server.store.pop(f'releases/commit/{sha}/handoff-darwin-x64.json')
|
||||
summary.unlink()
|
||||
summary_env['RELEASE_NEEDS'] = json.dumps({'validate': {'result': 'success'},
|
||||
'build-darwin-x64': {'result': 'failure'}})
|
||||
incomplete = shell_step(tmp_path, r2_server, 'commit-builds-summary',
|
||||
gates['darwin-x64']: {'result': 'failure'}})
|
||||
incomplete = shell_step(tmp_path, r2_server, summary_job,
|
||||
'Render the full expected-binary matrix', summary_env)
|
||||
assert incomplete.returncode == 0, incomplete.stdout + incomplete.stderr
|
||||
assert 'failed: build-darwin-x64' in summary.read_text(encoding='utf-8-sig')
|
||||
assert f"failed: {gates['darwin-x64']}" in summary.read_text(encoding='utf-8-sig')
|
||||
|
||||
Reference in New Issue
Block a user