Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/always-on-guards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ jobs:
# GITHUB_ENV exporte par cette etape, lui coupant la parole).
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
# #17713 -- tete de PR (nom de branche), consomme par variation_tag_required
# pour exempter les sessions cloud du mainteneur (`claude/*` + `Hors flotte`).
HEAD_REF: ${{ github.event.pull_request.head.ref }}

steps:
# ------------------------------------------------------------------
Expand Down Expand Up @@ -774,11 +777,19 @@ jobs:

printf '%s' "${PR_BODY:-}" > /tmp/pr_body.txt

# #17713 -- exemption : une PR dont la branche commence par `claude/`
# ET dont le body porte "Hors flotte" est une session cloud du
# mainteneur, exempte du tag Grain: par `CLAUDE.md` ('A qui ce fichier
# s'adresse'). On passe HEAD_REF au script qui applique l'exemption
# (les DEUX conditions doivent etre reunies -- un renommage de
# branche seul ne suffit pas).
HEAD_REF="${HEAD_REF:-}"

# Capturer le RC via &&/|| (exempt d'errexit) -- jamais une
# commande suivie d'un RC=$? en ligne separee : sous bash -e elle
# meurt au block path meme, avant l'affectation.
python3 scripts/ci/variation_tag_required.py \
--body-file /tmp/pr_body.txt > /tmp/verdict.json 2>/tmp/verdict.err && RC=0 || RC=$?
--body-file /tmp/pr_body.txt --head-ref "$HEAD_REF" > /tmp/verdict.json 2>/tmp/verdict.err && RC=0 || RC=$?

if [ -s /tmp/verdict.err ]; then
echo "--- helper stderr ---"
Expand Down
36 changes: 34 additions & 2 deletions scripts/ci/variation_tag_required.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,18 @@
import grain_tag as gt # noqa: E402


def check(body: str | None) -> dict:
def check(body: str | None, head_ref: str | None = None) -> dict:
"""Return the blocking verdict for a PR body.

See module docstring §2. The signature is preserved as a pure function so
unit tests can pin each branch without going through the CLI.

`head_ref` is the PR head branch (e.g. ``claude/foo-bar-xyz``). When both
(a) it starts with the ``claude/`` prefix AND (b) the body declares
``Hors flotte``, the PR is exempt from the Grain-tag requirement -- cf.
#17713: a Claude Code session opened from the maintainer's cloud
environment writes no `Grain:` tag, and ``CLAUDE.md`` ('À qui ce fichier
s'adresse') explicitly exempts such sessions from the variation protocol.
"""
if body is None or body.strip() == "":
return {
Expand All @@ -108,6 +115,23 @@ def check(body: str | None) -> dict:
"lane": None,
}

# #17713 -- exemption : session cloud du mainteneur (branche `claude/*`).
# Les deux conditions doivent etre reunies : un simple renommage de
# branche ne suffit pas, et un marqueur `Hors flotte` sans prefixe
# `claude/` n'exempte pas non plus (cf acceptance #17713 point 1).
if (
head_ref
and head_ref.startswith("claude/")
and "Hors flotte" in body
):
return {
"required_pass": True,
"reason": "exempt by #17713 (claude/* + 'Hors flotte' in body)",
"tier": None,
"genre": None,
"lane": None,
}

parsed = gt.parse_grain_tag(body)
if parsed is None:
return {
Expand Down Expand Up @@ -146,6 +170,14 @@ def main(argv: list[str] | None = None) -> int:
action="store_true",
help="read the PR body from stdin (terminated by EOF)",
)
p.add_argument(
"--head-ref",
metavar="REF",
default=None,
help="PR head branch (e.g. 'claude/foo-bar-xyz'). When it starts with "
"'claude/' and the body declares 'Hors flotte', the Grain-tag "
"requirement is waived (#17713).",
)
args = p.parse_args(argv)

if args.body_file:
Expand All @@ -169,7 +201,7 @@ def main(argv: list[str] | None = None) -> int:
else:
body = sys.stdin.read()

verdict = check(body)
verdict = check(body, head_ref=args.head_ref)
print(json.dumps(verdict))
return 0 if verdict["required_pass"] else 1

Expand Down
81 changes: 81 additions & 0 deletions scripts/tests/test_variation_tag_required.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,84 @@ def test_13232_metadata_dependent_guard_has_no_paths_filter(wf, why):
"ne poste aucun check-run. Reduire le fan-out par `types:` ou "
"`concurrency`, jamais par `paths:`. Cf #13232."
)


# --- #17713 : exemption `claude/*` + `Hors flotte` -------------------------
#
# Une session Claude Code cloud du mainteneur ouvre une PR sous le prefixe
# `claude/` et declare `Hors flotte` dans le body. `CLAUDE.md` ('A qui ce
# fichier s'adresse') exempte explicitement ces sessions du protocole de
# variation -- le garde Grain-tag doit donc laisser passer ces PR. Les DEUX
# conditions sont requises (un simple renommage de branche ne suffit pas,
# et un marqueur `Hors flotte` sans prefixe `claude/` n'exempte pas non plus
# -- une lane de la flotte ne peut pas sortir du protocole en usurpant le
# marqueur).


def test_17713_claude_head_with_hors_flote_is_exempt():
"""Branch `claude/*` + body `Hors flotte` -> required_pass True."""
body = "Fix mineur.\n\n**Hors flotte** -- session cloud du mainteneur.\n"
v = vtr.check(body, head_ref="claude/curious-river-1f")
assert v["required_pass"] is True
assert "17713" in v["reason"]


def test_17713_claude_head_without_hors_flote_still_blocked():
"""Branch `claude/*` sans marqueur body -> toujours bloque."""
body = "Fix mineur. Pas de marqueur."
v = vtr.check(body, head_ref="claude/curious-river-1f")
assert v["required_pass"] is False


def test_17713_fleet_branch_with_hors_flote_still_blocked():
"""Branche de la flotte + body `Hors flotte` -> toujours bloque.

Une lane de la flotte ne peut pas sortir du protocole de variation en
usurpant le marqueur : seule la conjonction `claude/*` + `Hors flotte`
exempte.
"""
body = "Fix.\n\n**Hors flotte** -- tentative d'exemption par lane flotte."
v = vtr.check(body, head_ref="fix/17713-fausse-exemption")
assert v["required_pass"] is False


def test_17713_no_head_ref_blocks_even_with_hors_flote():
"""Sans head_ref (CLI legacy / appel depuis un autre job), le marqueur
body seul n'exempte pas -- la garde reste fail-closed."""
body = "Fix.\n\n**Hors flotte** -- mais sans head_ref."
v = vtr.check(body) # pas de head_ref
assert v["required_pass"] is False


def test_17713_cli_head_ref_arg_propagates():
"""End-to-end CLI : --head-ref propage et l'exemption tient."""
fixture = Path(__file__).resolve().parent / "_vtr_fixture_17713.md"
fixture.write_text(
"Fix.\n\n**Hors flotte** -- session cloud du mainteneur.\n",
encoding="utf-8",
)
try:
proc = subprocess.run(
[
sys.executable,
"scripts/ci/variation_tag_required.py",
"--body-file",
str(fixture),
"--head-ref",
"claude/curious-river-1f",
],
capture_output=True,
text=True,
check=False,
encoding="utf-8",
errors="replace",
)
assert proc.returncode == 0, (
f"expected exit 0 (exempt), got {proc.returncode}; "
f"stderr={proc.stderr!r}"
)
verdict = json.loads(proc.stdout)
assert verdict["required_pass"] is True
assert "17713" in verdict["reason"]
finally:
fixture.unlink(missing_ok=True)
Loading