test(ci): evaluate stable-release and build-cache gates instead of spelling them
test_stable_release_graph and test_desktop_build_cache asserted gate text: `== "always()"`, `== "false"`, `"conclusion != 'success'" in`, `"!cancelled()" in`. Both files now evaluate the gate with the shared evaluator, which also resolves `steps.*` now. - The stable gates (acceptance, publication, complete) must run when every ancestor failed. - Deferred desktop e2e never runs. - The publication reconciler runs after a failed, dispatched Stable Release of this repository and on manual dispatch. It refuses a successful run, a push-triggered run, and a fork's run. - No desktop-build-cache step runs during cancellation. The service-failure report runs when restore or save failed and stays quiet otherwise.
This commit is contained in:
@@ -187,7 +187,8 @@ def admitted(names, *, channel=False):
|
||||
return {name: {"result": "success", "outputs": dict(outputs)} for name in names}
|
||||
|
||||
|
||||
def evaluate(expression, inputs, needs, *, cancelled=False, failed=False, job_if=True, github=None, env=None):
|
||||
def evaluate(expression, inputs, needs, *, cancelled=False, failed=False, job_if=True, github=None, env=None,
|
||||
steps=None):
|
||||
"""Evaluate the workflow expression subset, including Actions' implicit success.
|
||||
|
||||
This is not a scheduler simulation; native Actions still owns cancellation
|
||||
@@ -205,12 +206,12 @@ def evaluate(expression, inputs, needs, *, cancelled=False, failed=False, job_if
|
||||
|
||||
def value(match):
|
||||
bits = match[0].split('.')
|
||||
result = {'inputs': inputs, 'needs': needs, 'github': github or {}, 'env': env or {}}
|
||||
result = {'inputs': inputs, 'needs': needs, 'github': github or {}, 'env': env or {}, 'steps': steps or {}}
|
||||
for bit in bits:
|
||||
result = result.get(bit, '') if isinstance(result, dict) else ''
|
||||
return repr(result)
|
||||
|
||||
expression = re.sub(r'\b(?:inputs|needs|github|env)(?:\.[\w-]+)+', value, expression)
|
||||
expression = re.sub(r'\b(?:inputs|needs|github|env|steps)(?:\.[\w-]+)+', value, expression)
|
||||
expression = expression.replace('&&', ' and ').replace('||', ' or ')
|
||||
expression = re.sub(r'!(?!=)', ' not ', expression)
|
||||
expression = re.sub(r'\btrue\b', 'True', expression)
|
||||
|
||||
@@ -5,6 +5,8 @@ from pathlib import Path
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from tests.ci.desktop_release_roles import gate
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
@@ -78,8 +80,10 @@ def test_release_reuses_whole_ci_and_docker_before_publication():
|
||||
assert required <= ancestors(jobs, "publish-bundles")
|
||||
assert {"publish-docker", "publish-bundles", "publication"} <= ancestors(jobs, "complete")
|
||||
assert "promote-docker" not in jobs and "promote-bundles" not in jobs
|
||||
# The gates must run to judge a failed or skipped prerequisite, not skip with it.
|
||||
for name in ("acceptance", "publication", "complete"):
|
||||
assert jobs[name]["if"] == "always()"
|
||||
failed = {need: {"result": "failure"} for need in ancestors(jobs, name)}
|
||||
assert gate(jobs[name]["if"], {}, failed), name
|
||||
|
||||
|
||||
def test_claim_flags_remove_exactly_the_jobs_the_gate_expects_skipped():
|
||||
@@ -126,7 +130,7 @@ def test_all_applicable_ci_jobs_are_aggregated_and_desktop_e2e_stays_deferred():
|
||||
jobs = workflow("ci.yaml")["jobs"]
|
||||
checks = {name for name, job in jobs.items() if "uses" in job}
|
||||
assert checks <= set(jobs["all-checks-pass"]["needs"])
|
||||
assert jobs["e2e-desktop"]["if"] == "false"
|
||||
assert not gate(jobs["e2e-desktop"]["if"], {}, {})
|
||||
assert "workflow_call" in workflow("ci.yaml")["on"]
|
||||
|
||||
|
||||
@@ -258,7 +262,17 @@ def test_publication_reconciler_has_every_recovery_trigger_and_shared_lock():
|
||||
reconcile = publication["jobs"]["reconcile"]
|
||||
assert reconcile["environment"] == "release-signing"
|
||||
assert publication["permissions"] == {"contents": "write", "actions": "write"}
|
||||
assert "conclusion != 'success'" in reconcile["if"]
|
||||
# Reconcile after a dispatched run of this repository that did not succeed;
|
||||
# a manual dispatch of the reconciler always runs.
|
||||
def run_of(**overrides):
|
||||
workflow_run = {"conclusion": "failure", "event": "workflow_dispatch",
|
||||
"head_repository": {"full_name": "o/r"}, **overrides}
|
||||
return {"event_name": "workflow_run", "repository": "o/r", "event": {"workflow_run": workflow_run}}
|
||||
|
||||
assert gate(reconcile["if"], {}, {}, github=run_of())
|
||||
assert gate(reconcile["if"], {}, {}, github={"event_name": "workflow_dispatch"})
|
||||
for refused in ({"conclusion": "success"}, {"event": "push"}, {"head_repository": {"full_name": "fork/r"}}):
|
||||
assert not gate(reconcile["if"], {}, {}, github=run_of(**refused)), refused
|
||||
checkout = reconcile["steps"][0]
|
||||
assert checkout["with"]["ref"] == "${{ github.event.repository.default_branch }}"
|
||||
assert checkout["with"]["persist-credentials"] == "false"
|
||||
|
||||
@@ -10,6 +10,7 @@ import sys
|
||||
import pytest
|
||||
|
||||
from scripts.ci.setup_toolchain import current_target
|
||||
from tests.ci.desktop_release_roles import gate
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -187,8 +188,13 @@ def test_composite_transports_only_and_uses_the_restore_key_for_save(tmp_path):
|
||||
assert restore["continue-on-error"] is True
|
||||
assert save["continue-on-error"] is True
|
||||
for step in steps:
|
||||
assert "!cancelled()" in step["if"], "failure salvage must not run during cancellation"
|
||||
assert any("steps.save.outcome == 'failure'" in step["if"] and "::warning::" in step.get("run", "") for step in steps)
|
||||
assert not gate(step["if"], {}, {}, job_if=False, cancelled=True), \
|
||||
"failure salvage must not run during cancellation"
|
||||
# A cache service failure is reported instead of failing preparation.
|
||||
(report,) = [step for step in steps if "::warning::" in step.get("run", "")]
|
||||
for failed in ("restore", "save"):
|
||||
assert gate(report["if"], {}, {}, job_if=False, failed=True, steps={failed: {"outcome": "failure"}})
|
||||
assert not gate(report["if"], {}, {}, job_if=False, steps={"restore": {"outcome": "success"}})
|
||||
|
||||
|
||||
def test_action_path_join_is_an_actual_newline(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user