Loading tbc_check/checker.py +25 −5 Original line number Diff line number Diff line Loading @@ -468,12 +468,15 @@ def _check_job(job_name: str, tpl_body, job_prefix) -> int: def _check_inheritance(tpl_body: dict[str, Any]) -> int: err_count = 0 # Gather actual jobs by filtering non-job items in template job_bodies = { name: body for name, body in tpl_body.items() if name not in ["stages", "workflow", "variables", "rules"] and isinstance(body, dict) } # count non-hidden jobs # Count non-hidden jobs to infer if it's a single job template or not is_single_job_tpl = sum(1 for name in job_bodies if not name.startswith(".")) == 1 Loading @@ -484,23 +487,40 @@ def _check_inheritance(tpl_body: dict[str, Any]) -> int: # - all jobs should inherit from a hidden base job # for name, body in iter(job_bodies.items()): job_err_count = 0 # skip job with only partial overrides if not any(key in body for key in ["stage", "extends", "image"]): print( f" {AnsiColors.HGRAY}✕ job '{name}' does not defines an actual job: skip{AnsiColors.RESET}" ) continue base_job = body.get("extends", None) if is_single_job_tpl and base_job is not None: job_err_count += 1 print( f" {AnsiColors.RED}✕ single job template: job '{name}' inherits another job{AnsiColors.RESET}" ) if not is_single_job_tpl and not name.startswith(".") and base_job is None: job_err_count += 1 print( f" {AnsiColors.RED}✕ multiple job template: job '{name}' should inherit a base job{AnsiColors.RESET}" f" {AnsiColors.RED}✕ multiple job template: non-hidden job '{name}' should inherit a base job{AnsiColors.RESET}" ) if base_job is not None and not base_job.startswith("."): job_err_count += 1 print( f" {AnsiColors.RED}✕ multiple job template: job '{name}' inherits from non-hidden job '{base_job}'{AnsiColors.RESET}" f" {AnsiColors.RED}✕ job '{name}' inherits from non-hidden job '{base_job}'{AnsiColors.RESET}" ) if base_job is not None and base_job not in job_bodies: print( f" {AnsiColors.YELLOW}⚠ multiple job template: job '{name}' inherits from '{base_job}' which is not defined in current template{AnsiColors.RESET}" f" {AnsiColors.YELLOW}⚠ job '{name}' inherits from '{base_job}' which is not defined in current template{AnsiColors.RESET}" ) if job_err_count == 0: print( f" {AnsiColors.GREEN}✓{AnsiColors.RESET} job '{name}' inheritance: OK" ) err_count += job_err_count return err_count def _check_tpl( kicker: dict[str, any], Loading Loading @@ -529,7 +549,7 @@ def _check_tpl( # check inheritance # ----------------- _check_inheritance(tpl_body=tpl_body) err_count += _check_inheritance(tpl_body=tpl_body) # check jobs # ---------- Loading tests/test_check_inheritance.py +61 −56 Original line number Diff line number Diff line from copy import deepcopy import pytest from tbc_check import checker def test_check_inheritance_multiple_job_inheritance_from_undefined(capfd: pytest.CaptureFixture[str]): tpl_body = { TPL_MULTIPLE_JOB = { "workflow": None, "variables": None, "stages": None, ".base-job": {}, ".base-job": { "image": "base-image" }, "prefix-job1": { "extends": ".base-job" "stage": "build", "extends": ".base-job", }, "prefix-job2": { "extends": ".other-base-job" "stage": "test", "extends": ".base-job" }, } res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;33m⚠ multiple job template: job 'prefix-job2' inherits from '.other-base-job' which is not defined in current template\x1b[0m\n" ) def test_check_inheritance_multiple_job_inheritance_bad(capfd: pytest.CaptureFixture[str]): tpl_body = { TPL_SINGLE_JOB = { "workflow": None, "variables": None, "stages": None, ".base-job": {}, "prefix-job1": { "extends": ".base-job" "stage": "build", }, "prefix-job2": {}, } def test_check_inheritance_multiple_job_inheritance_from_undefined(capfd: pytest.CaptureFixture[str]): tpl_body = deepcopy(TPL_MULTIPLE_JOB) tpl_body["prefix-job2"]["extends"] = ".undefined" res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;31m✕ multiple job template: job 'prefix-job2' should inherit a base job\x1b[0m\n" " \x1b[0;32m✓\x1b[0m job '.base-job' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" " \x1b[0;33m⚠ job 'prefix-job2' inherits from '.undefined' which is not defined in current template\x1b[0m\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job2' inheritance: OK\n" ) def test_check_inheritance_multiple_job_inheritance_bad(capfd: pytest.CaptureFixture[str]): tpl_body = deepcopy(TPL_MULTIPLE_JOB) tpl_body["prefix-job2"].pop("extends") res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;32m✓\x1b[0m job '.base-job' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" " \x1b[0;31m✕ multiple job template: non-hidden job 'prefix-job2' should inherit a base job\x1b[0m\n" ) def test_check_inheritance_multiple_job_inheritance_good(capfd: pytest.CaptureFixture[str]): tpl_body = { "workflow": None, "variables": None, "stages": None, ".hidden": None, ".base-job": {}, "prefix-job1": { "extends": ".base-job" }, "prefix-job2": { "extends": ".base-job" }, } tpl_body = deepcopy(TPL_MULTIPLE_JOB) res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( "" " \x1b[0;32m✓\x1b[0m job '.base-job' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job2' inheritance: OK\n" ) def test_check_inheritance_multiple_job_inheritance_from_non_hidden(capfd: pytest.CaptureFixture[str]): tpl_body = { "workflow": None, "variables": None, "stages": None, ".hidden": None, ".base-job": {}, "prefix-job1": { "extends": ".base-job" }, "prefix-job2": { "extends": "prefix-job1" }, } tpl_body = deepcopy(TPL_MULTIPLE_JOB) tpl_body["prefix-job2"]["extends"] = "prefix-job1" res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;31m✕ multiple job template: job 'prefix-job2' inherits from non-hidden job 'prefix-job1'\x1b[0m\n" " \x1b[0;32m✓\x1b[0m job '.base-job' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" " \x1b[0;31m✕ job 'prefix-job2' inherits from non-hidden job 'prefix-job1'\x1b[0m\n" ) def test_check_inheritance_single_job_without_inheritance(capfd: pytest.CaptureFixture[str]): tpl_body = deepcopy(TPL_SINGLE_JOB) res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" ) def test_check_inheritance_single_job_with_inheritance(capfd: pytest.CaptureFixture[str]): tpl_body = { "workflow": None, "variables": None, "stages": None, ".hidden": None, ".prefix-base": {}, "prefix-job1": { "extends": ".prefix-base" }, } tpl_body = deepcopy(TPL_SINGLE_JOB) tpl_body["prefix-job1"]["extends"] = ".base-job" res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;31m✕ single job template: job 'prefix-job1' inherits another job\x1b[0m\n" " \x1b[0;33m⚠ job 'prefix-job1' inherits from '.base-job' which is not defined in current template\x1b[0m\n" ) def test_check_inheritance_single_job_override_only(capfd: pytest.CaptureFixture[str]): tpl_body = deepcopy(TPL_SINGLE_JOB) tpl_body["prefix-job1"] = { "rules": {} } res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[90m✕ job 'prefix-job1' does not defines an actual job: skip\x1b[0m\n" ) Loading
tbc_check/checker.py +25 −5 Original line number Diff line number Diff line Loading @@ -468,12 +468,15 @@ def _check_job(job_name: str, tpl_body, job_prefix) -> int: def _check_inheritance(tpl_body: dict[str, Any]) -> int: err_count = 0 # Gather actual jobs by filtering non-job items in template job_bodies = { name: body for name, body in tpl_body.items() if name not in ["stages", "workflow", "variables", "rules"] and isinstance(body, dict) } # count non-hidden jobs # Count non-hidden jobs to infer if it's a single job template or not is_single_job_tpl = sum(1 for name in job_bodies if not name.startswith(".")) == 1 Loading @@ -484,23 +487,40 @@ def _check_inheritance(tpl_body: dict[str, Any]) -> int: # - all jobs should inherit from a hidden base job # for name, body in iter(job_bodies.items()): job_err_count = 0 # skip job with only partial overrides if not any(key in body for key in ["stage", "extends", "image"]): print( f" {AnsiColors.HGRAY}✕ job '{name}' does not defines an actual job: skip{AnsiColors.RESET}" ) continue base_job = body.get("extends", None) if is_single_job_tpl and base_job is not None: job_err_count += 1 print( f" {AnsiColors.RED}✕ single job template: job '{name}' inherits another job{AnsiColors.RESET}" ) if not is_single_job_tpl and not name.startswith(".") and base_job is None: job_err_count += 1 print( f" {AnsiColors.RED}✕ multiple job template: job '{name}' should inherit a base job{AnsiColors.RESET}" f" {AnsiColors.RED}✕ multiple job template: non-hidden job '{name}' should inherit a base job{AnsiColors.RESET}" ) if base_job is not None and not base_job.startswith("."): job_err_count += 1 print( f" {AnsiColors.RED}✕ multiple job template: job '{name}' inherits from non-hidden job '{base_job}'{AnsiColors.RESET}" f" {AnsiColors.RED}✕ job '{name}' inherits from non-hidden job '{base_job}'{AnsiColors.RESET}" ) if base_job is not None and base_job not in job_bodies: print( f" {AnsiColors.YELLOW}⚠ multiple job template: job '{name}' inherits from '{base_job}' which is not defined in current template{AnsiColors.RESET}" f" {AnsiColors.YELLOW}⚠ job '{name}' inherits from '{base_job}' which is not defined in current template{AnsiColors.RESET}" ) if job_err_count == 0: print( f" {AnsiColors.GREEN}✓{AnsiColors.RESET} job '{name}' inheritance: OK" ) err_count += job_err_count return err_count def _check_tpl( kicker: dict[str, any], Loading Loading @@ -529,7 +549,7 @@ def _check_tpl( # check inheritance # ----------------- _check_inheritance(tpl_body=tpl_body) err_count += _check_inheritance(tpl_body=tpl_body) # check jobs # ---------- Loading
tests/test_check_inheritance.py +61 −56 Original line number Diff line number Diff line from copy import deepcopy import pytest from tbc_check import checker def test_check_inheritance_multiple_job_inheritance_from_undefined(capfd: pytest.CaptureFixture[str]): tpl_body = { TPL_MULTIPLE_JOB = { "workflow": None, "variables": None, "stages": None, ".base-job": {}, ".base-job": { "image": "base-image" }, "prefix-job1": { "extends": ".base-job" "stage": "build", "extends": ".base-job", }, "prefix-job2": { "extends": ".other-base-job" "stage": "test", "extends": ".base-job" }, } res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;33m⚠ multiple job template: job 'prefix-job2' inherits from '.other-base-job' which is not defined in current template\x1b[0m\n" ) def test_check_inheritance_multiple_job_inheritance_bad(capfd: pytest.CaptureFixture[str]): tpl_body = { TPL_SINGLE_JOB = { "workflow": None, "variables": None, "stages": None, ".base-job": {}, "prefix-job1": { "extends": ".base-job" "stage": "build", }, "prefix-job2": {}, } def test_check_inheritance_multiple_job_inheritance_from_undefined(capfd: pytest.CaptureFixture[str]): tpl_body = deepcopy(TPL_MULTIPLE_JOB) tpl_body["prefix-job2"]["extends"] = ".undefined" res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;31m✕ multiple job template: job 'prefix-job2' should inherit a base job\x1b[0m\n" " \x1b[0;32m✓\x1b[0m job '.base-job' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" " \x1b[0;33m⚠ job 'prefix-job2' inherits from '.undefined' which is not defined in current template\x1b[0m\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job2' inheritance: OK\n" ) def test_check_inheritance_multiple_job_inheritance_bad(capfd: pytest.CaptureFixture[str]): tpl_body = deepcopy(TPL_MULTIPLE_JOB) tpl_body["prefix-job2"].pop("extends") res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;32m✓\x1b[0m job '.base-job' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" " \x1b[0;31m✕ multiple job template: non-hidden job 'prefix-job2' should inherit a base job\x1b[0m\n" ) def test_check_inheritance_multiple_job_inheritance_good(capfd: pytest.CaptureFixture[str]): tpl_body = { "workflow": None, "variables": None, "stages": None, ".hidden": None, ".base-job": {}, "prefix-job1": { "extends": ".base-job" }, "prefix-job2": { "extends": ".base-job" }, } tpl_body = deepcopy(TPL_MULTIPLE_JOB) res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( "" " \x1b[0;32m✓\x1b[0m job '.base-job' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job2' inheritance: OK\n" ) def test_check_inheritance_multiple_job_inheritance_from_non_hidden(capfd: pytest.CaptureFixture[str]): tpl_body = { "workflow": None, "variables": None, "stages": None, ".hidden": None, ".base-job": {}, "prefix-job1": { "extends": ".base-job" }, "prefix-job2": { "extends": "prefix-job1" }, } tpl_body = deepcopy(TPL_MULTIPLE_JOB) tpl_body["prefix-job2"]["extends"] = "prefix-job1" res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;31m✕ multiple job template: job 'prefix-job2' inherits from non-hidden job 'prefix-job1'\x1b[0m\n" " \x1b[0;32m✓\x1b[0m job '.base-job' inheritance: OK\n" " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" " \x1b[0;31m✕ job 'prefix-job2' inherits from non-hidden job 'prefix-job1'\x1b[0m\n" ) def test_check_inheritance_single_job_without_inheritance(capfd: pytest.CaptureFixture[str]): tpl_body = deepcopy(TPL_SINGLE_JOB) res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;32m✓\x1b[0m job 'prefix-job1' inheritance: OK\n" ) def test_check_inheritance_single_job_with_inheritance(capfd: pytest.CaptureFixture[str]): tpl_body = { "workflow": None, "variables": None, "stages": None, ".hidden": None, ".prefix-base": {}, "prefix-job1": { "extends": ".prefix-base" }, } tpl_body = deepcopy(TPL_SINGLE_JOB) tpl_body["prefix-job1"]["extends"] = ".base-job" res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[0;31m✕ single job template: job 'prefix-job1' inherits another job\x1b[0m\n" " \x1b[0;33m⚠ job 'prefix-job1' inherits from '.base-job' which is not defined in current template\x1b[0m\n" ) def test_check_inheritance_single_job_override_only(capfd: pytest.CaptureFixture[str]): tpl_body = deepcopy(TPL_SINGLE_JOB) tpl_body["prefix-job1"] = { "rules": {} } res = checker._check_inheritance(tpl_body=tpl_body) out, err = capfd.readouterr() assert out == ( " \x1b[90m✕ job 'prefix-job1' does not defines an actual job: skip\x1b[0m\n" )