diff --git a/cli/administration/deploy/ci/runs.py b/cli/administration/deploy/ci/runs.py index 2f66417aa1..a3212fe13d 100644 --- a/cli/administration/deploy/ci/runs.py +++ b/cli/administration/deploy/ci/runs.py @@ -393,12 +393,34 @@ def config_from_title(title: str) -> dict[str, str]: return {name: recorded[name] for name in CONFIG_INPUTS if name in recorded} +SUITE_JOB_IDS = {"workspace": "test-workspace", "instructions": "test-instructions"} + + +def suite_passed(jobs: list[dict], suite: str) -> bool: + """Whether ``suite`` ('workspace' | 'instructions') ran to success in this + run. A suite that was skipped, cancelled or never dispatched has not + passed, so a retrigger keeps asking for it.""" + job_id = SUITE_JOB_IDS[suite] + outcomes = [ + _effective(job) + for job in jobs + if job_id in str(job.get("name", "")).split(" / ")[1:] + ] + return bool(outcomes) and all(outcome == "success" for outcome in outcomes) + + def config_from_run(title: str, jobs: list[dict]) -> dict[str, str]: """The source run's configuration, with the distros the title does not - record recovered from its jobs (:func:`distros_from_jobs`).""" + record recovered from its jobs (:func:`distros_from_jobs`). + + A suite the source run already passed is turned off rather than carried + over, so a retrigger spends its runners on what actually failed.""" config = config_from_title(title) if not config.get("distros"): config = {**config, "distros": distros_from_jobs(jobs)} + for suite in SUITE_JOB_IDS: + if config.get(suite) == "true" and suite_passed(jobs, suite): + config = {**config, suite: "false"} return {name: value for name, value in config.items() if value} diff --git a/tests/unit/cli/administration/deploy/ci/test_runs.py b/tests/unit/cli/administration/deploy/ci/test_runs.py index 8c59bc1599..909bf4e346 100644 --- a/tests/unit/cli/administration/deploy/ci/test_runs.py +++ b/tests/unit/cli/administration/deploy/ci/test_runs.py @@ -340,6 +340,40 @@ class TestConfigFromTitle(unittest.TestCase): jobs = [{"name": "๐ŸŽฒ Pick distro(s)"}, {"name": "๐Ÿงน Lint"}] self.assertNotIn("distros", runs.config_from_run(title, jobs)) + def _suite_job(self, suite: str, conclusion: str) -> dict: + return { + "name": f"๐ŸŽ›๏ธ Orchestrate CI (manual) / {runs.SUITE_JOB_IDS[suite]} / ๐Ÿ“– X", + "status": "completed", + "conclusion": conclusion, + } + + def test_a_suite_the_source_run_passed_is_not_asked_for_again(self) -> None: + for suite in runs.SUITE_JOB_IDS: + with self.subTest(suite): + title = render({suite: "true"}) + jobs = [self._suite_job(suite, "success")] + self.assertEqual(runs.config_from_run(title, jobs)[suite], "false") + + def test_a_suite_that_failed_is_asked_for_again(self) -> None: + for suite in runs.SUITE_JOB_IDS: + with self.subTest(suite): + title = render({suite: "true"}) + jobs = [self._suite_job(suite, "failure")] + self.assertEqual(runs.config_from_run(title, jobs)[suite], "true") + + def test_a_suite_that_never_ran_is_asked_for_again(self) -> None: + title = render({"workspace": "true"}) + jobs = [{"name": "๐Ÿงน Lint", "status": "completed", "conclusion": "success"}] + self.assertEqual(runs.config_from_run(title, jobs)["workspace"], "true") + + def test_one_failing_shard_keeps_the_whole_suite(self) -> None: + title = render({"workspace": "true"}) + jobs = [ + self._suite_job("workspace", "success"), + self._suite_job("workspace", "failure"), + ] + self.assertEqual(runs.config_from_run(title, jobs)["workspace"], "true") + if __name__ == "__main__": unittest.main()