From 392a5d68cc9904c92ebba208c110efeb01f09ce3 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:23:56 +0200 Subject: [PATCH] fix: strict sync fails a profile it cannot check profile_sync skipped a profile whose repository no longer answers, which carries no pin, or whose declared death the forge contradicts, with empty counts. --strict counted refs to review only, so the CI job meant to stop an unchecked new profile passed a mistyped repository. Such skips now fail a strict run. An unsupported host, a declared takedown and a profile without refs stay accepted. --- scripts/profile_sync.py | 22 ++++++++++++++++++++-- tests/test_profile_sync.py | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/scripts/profile_sync.py b/scripts/profile_sync.py index 301ffffc..739194b0 100644 --- a/scripts/profile_sync.py +++ b/scripts/profile_sync.py @@ -1115,6 +1115,21 @@ class ProfileReport: # tracks: history was rewritten, or the pin lies on another branch. orphaned: list[str] = field(default_factory=list) + # Skips that say the profile cannot be checked as written: a repository + # that no longer answers, no pin to read, a death the forge contradicts. + # --strict passed them, and the CI job meant to stop an unchecked profile + # let a mistyped repository through. + UNCHECKABLE = ( + "no declared repository answers", + "no source_commit and no resolvable profiled_date", + "upstream_gone is declared but the forge answers", + "upstream gone:", + ) + + def uncheckable(self) -> bool: + """Whether the skip is a defect of the profile rather than its nature.""" + return bool(self.skipped) and self.skipped.startswith(self.UNCHECKABLE) + def needs_review(self) -> int: counts = self.counts or {} return sum(counts.get(status, 0) for status in REVIEW_STATUSES) + len( @@ -3255,7 +3270,8 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument( "--strict", action="store_true", - help="exit 1 when a profile has refs to review, as for a new profile in CI", + help="exit 1 when a profile has refs to review or cannot be checked as " + "written (no repository answers, no pin), as for a new profile in CI", ) parser.add_argument( "--json", action="store_true", dest="as_json", @@ -3810,7 +3826,9 @@ def main() -> None: if not args.changed_only or report.needs_review() or report.name in failed ] _emit(args, selected, reports, shown) - if failed or (args.strict and any(report.needs_review() for report in reports)): + if failed or (args.strict and any( + report.needs_review() or report.uncheckable() for report in reports + )): raise SystemExit(1) diff --git a/tests/test_profile_sync.py b/tests/test_profile_sync.py index b84b9d66..5a1379f4 100644 --- a/tests/test_profile_sync.py +++ b/tests/test_profile_sync.py @@ -4234,5 +4234,24 @@ class TheUnknownProfileHintCountsTheListing(unittest.TestCase): self.assertIn(f"lists all {count}.", hint.stderr + hint.stdout) +class StrictRefusesAnUncheckableProfile(unittest.TestCase): + """A new profile whose repository answered 404, or with no pin at all, + was skipped with empty counts and --strict passed it in CI.""" + + def test_defects_are_told_from_the_profiles_nature(self): + from profile_sync import ProfileReport + + def report(reason): + return ProfileReport(name="p", entries=[], counts={}, skipped=reason) + + self.assertTrue(report("no declared repository answers (moved)").uncheckable()) + self.assertTrue(report( + "no source_commit and no resolvable profiled_date on any declared repository" + ).uncheckable()) + self.assertFalse(report("unsupported host: https://example.org").uncheckable()) + self.assertFalse(report("upstream gone (declared): taken down").uncheckable()) + self.assertFalse(report("no source_ref").uncheckable()) + + if __name__ == "__main__": unittest.main()