From bcc8eef4b8d1eabeb43639bf0daf1bcaafd0731e Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Sat, 6 Jun 2026 22:03:41 -0500 Subject: [PATCH 1/6] ci(dev-lead): guard the caller<->reusable permission contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reusable workflow job can only use token permissions the caller grants at jobs..permissions; if a job requests more, every consumer fails at startup (startup_failure) with no runtime error. #435 added statuses:read to the reusable but not the caller template, breaking every @main consumer (.github 15/15) — petry-projects/.github#402. Add test_caller_permissions.py: parse the union of all reusable job permissions and assert the published caller template (petry-projects/.github standards/workflows/dev-lead.yml) grants >= each scope. Wire it as a new test-dev-lead job. This fails the build at the PR that edits the reusable — forcing the template grant to land first (the safe ordering). Verified: catches a tampered reusable requesting packages:write; the negative message names the exact scope and file to fix. NOTE: this job stays red until the template gains statuses:read (petry-projects/.github#403) — that's the contract gap it correctly detects. Refs petry-projects/.github#402 Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/test-dev-lead.yml | 14 ++ .../integration/test_caller_permissions.py | 122 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 tests/dev-lead/integration/test_caller_permissions.py diff --git a/.github/workflows/test-dev-lead.yml b/.github/workflows/test-dev-lead.yml index 9406e45a2..cea2ce020 100644 --- a/.github/workflows/test-dev-lead.yml +++ b/.github/workflows/test-dev-lead.yml @@ -80,3 +80,17 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Validate concurrency config (per-issue/per-PR lanes) run: bash tests/dev-lead/integration/test_concurrency_config.sh + + caller-permissions: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install PyYAML + run: python3 -m pip install --quiet pyyaml + - name: Validate caller<->reusable permission contract + # Reads the public petry-projects/.github caller template; github.token suffices. + env: + GH_TOKEN: ${{ github.token }} + run: python3 tests/dev-lead/integration/test_caller_permissions.py diff --git a/tests/dev-lead/integration/test_caller_permissions.py b/tests/dev-lead/integration/test_caller_permissions.py new file mode 100644 index 000000000..8d937aee1 --- /dev/null +++ b/tests/dev-lead/integration/test_caller_permissions.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Validate the dev-lead caller<->reusable GITHUB_TOKEN permission contract. + +A reusable workflow's jobs can only use token permissions that the CALLER grants +at `jobs..permissions`. If any reusable job requests MORE than the caller +template grants, every consumer pinned to that reusable fails at startup +("startup_failure") — there is no runtime error, the run is simply invalid. + +This is exactly what happened in petry-projects/.github#402: #435 added +`statuses: read` to the reusable's job permissions but the caller template was +never updated, so every @main consumer broke (.github was 15/15 startup_failure). + +The contract: for every permission scope, the caller template must grant a level +>= the maximum any reusable job requests. This guard fails the build the moment +the reusable asks for something the template doesn't grant — at the PR that edits +the reusable, in the reusable's own repo, where the breaking change is made. + +Reads: + - reusable: .github/workflows/dev-lead-reusable.yml (this repo, local) + - template: standards/workflows/dev-lead.yml (petry-projects/.github, fetched) +""" +from __future__ import annotations + +import base64 +import subprocess +import sys + +try: + import yaml +except ImportError: # pragma: no cover - CI installs it; clear message if not + print("FAIL: PyYAML is required (pip install pyyaml)", file=sys.stderr) + sys.exit(2) + +LEVELS = {"none": 0, "read": 1, "write": 2} +INV = {v: k for k, v in LEVELS.items()} + +TEMPLATE_REPO = "petry-projects/.github" +TEMPLATE_PATH = "standards/workflows/dev-lead.yml" +REUSABLE_PATH = ".github/workflows/dev-lead-reusable.yml" + + +def lvl(value) -> int: + return LEVELS.get(str(value).strip().lower(), 0) + + +def load_local(path: str) -> dict: + with open(path, "r", encoding="utf-8") as fh: + return yaml.safe_load(fh) + + +def fetch_template() -> dict: + """Fetch the caller template from the public petry-projects/.github repo.""" + res = subprocess.run( + ["gh", "api", f"repos/{TEMPLATE_REPO}/contents/{TEMPLATE_PATH}", "--jq", ".content"], + capture_output=True, text=True, + ) + if res.returncode != 0: + print(f"FAIL: could not fetch {TEMPLATE_REPO}:{TEMPLATE_PATH}\n{res.stderr.strip()}", + file=sys.stderr) + sys.exit(2) + return yaml.safe_load(base64.b64decode(res.stdout)) + + +def reusable_required(reusable: dict) -> dict[str, int]: + """Max level each permission scope is requested across all reusable jobs.""" + required: dict[str, int] = {} + for job in (reusable.get("jobs") or {}).values(): + perms = job.get("permissions") if isinstance(job, dict) else None + if not isinstance(perms, dict): + continue # no per-job block => inherits top-level `permissions: {}` (none) + for scope, level in perms.items(): + required[scope] = max(required.get(scope, 0), lvl(level)) + return required + + +def caller_granted(template: dict) -> dict[str, int] | None: + """Permissions the template grants on the job that calls the reusable.""" + for job in (template.get("jobs") or {}).values(): + if not isinstance(job, dict): + continue + if "dev-lead-reusable" in str(job.get("uses", "")): + perms = job.get("permissions") or {} + return {scope: lvl(level) for scope, level in perms.items()} + return None + + +def main() -> int: + reusable = load_local(REUSABLE_PATH) + template = fetch_template() + + required = reusable_required(reusable) + granted = caller_granted(template) + + if granted is None: + print(f"FAIL: no job in {TEMPLATE_REPO}:{TEMPLATE_PATH} calls dev-lead-reusable") + return 1 + + failures = [] + for scope, need in sorted(required.items()): + if need == 0: + continue + have = granted.get(scope, 0) + status = "OK" if have >= need else "MISSING" + print(f" [{status}] {scope}: reusable needs {INV[need]}, template grants {INV.get(have, 'none')}") + if have < need: + failures.append((scope, need, have)) + + print() + if failures: + print("FAIL: caller template under-grants permissions the reusable requests.") + for scope, need, have in failures: + print(f" - add `{scope}: {INV[need]}` to {TEMPLATE_REPO}:{TEMPLATE_PATH} " + f"jobs.dev-lead.permissions (currently {INV.get(have, 'none')})") + print("\nWithout it, every @main consumer fails at startup (startup_failure).") + return 1 + + print("PASS: caller template grants >= every permission the reusable jobs request.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From a3364b7801c42b790cc519fe75cafe34d1c64254 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sun, 7 Jun 2026 03:07:21 +0000 Subject: [PATCH 2/6] chore: apply manual instructions [skip ci-relay] --- .../test_caller_permissions.cpython-312.pyc | Bin 0 -> 7019 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc diff --git a/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc b/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b75d8e8a325193463c960dbd30ae57104d9c61a GIT binary patch literal 7019 zcmb_hZ%iCVmam@fo~#?5pC1M@H4 zJqBiHvJ+`_3%f{QWwoB19fe4imRzDcPL~h6+DPkNopd?vhZ{UZW;;k1-R4Wau_4jk zS@+>y^-On<3CZ4vYk8`Rs_Lp&zk2oF?>+y)>0}T*|NI}d6DM~e^e6m>_Lz$mVk<@< zbPw@p81V#eiV#uLu!+ERGC~fM>YEy-)Hgj$+loErVKcN?cr$N(Kn~jl`@}wNV&nZm zsd@n|pamjChEd3L%ZkuzCWJC5L!^l`ddoD=P$)!ZhzuHb^0wg$o*8!W_Tfstns@w| z8m{8qyc6EL_}zR3ysLQ+?}E3R-@{kJdpC@$g11N5!|$4GKuFwcLIOSEHssK1(C*P> z`T%x~9|FygGh_`CEVW4V5(vgvc?!MhA!ky-b!0~K|i$nzRrRKAukd(NQh`?U#8tlA! zE;!hGDbT|v1Th+xq;NdOhT<_<Lqp}J|e+0^h{we#<@g7z~Y24QnEPHoDky^LP(Zcd}CpGJULR+ zdiw)*)^egvLrcTRtL{|L^~KMQ#>LW<#>ui$ zD4r0Ccx@Ldnv~??*|8iO0qG*l*_Tyhqug{jnv8z8Likt}?< z8W(sk^CdP@UP7^Ru~;0m6AXsblWOj0@9OrmeKR-OFLz_j$M-fYg0eRx!U=!~M2JM# z#LNsAjdQo{gLr#^+_YmGEZ2vHsh`Ng$bL{rq#4Qp3v<70P>o8 zL=20UR?OXjp+NV5LXHWtLQ1lz&`^XE3Jqph0H>nRU~s@Z;!Utm+hlAgbv0=T$7BKQ zB0ixO+!ZX|zMye6C0}AjAtQGpVl~Xr2tUaSMHX$?s}}ro{-1|(N8g%w;;CEp)D=7p zPdvU=kFVf4zH+!=Z_QJ!TQC?s10i<)Duft3fKZ1wt&)Hb$q!0Uwt<_Lbyz3BYi)nh zga&KK!~>1skp>ch22c$;fh1xIoZk&_n&50p?$As1D1?}zlk#ZuX+X#-#6oc%?j6=J zutPD0!YZiPByLm)M&carH7QhrlgAa)=(vQDXIT;ZXobPOFa%%|4?=?ve$ojj03Vxk zmR~%-RKI3x+(ZO@WO-!6RlV@W+#9)(g6r^KNND;CZvP4wPbJ_cXkoXH{0I7!B?NW- z!AhaxH4jLY<26wt1o}v*05LN0q^x6gAMuo2qFV^vNAFNP&6~$)pih<$ts2r9ltvR+ zfNc+Nn>+=i=7wz7I(X)px$Wh1^A4EBp^Y@&6E35!)ikvRiOh1P*1FTzA3BXQm8^EAq8tmeLRyW?-+}azip)1wVfo+~@ytn(UQ86A> zI}*u}NI3Kz?p(FWKE*UPu8>?JoHApdAho2J7R<1~IYFIjZk~8OW%ZR>20-y z3KEPbuw(m1p5Phy=8y)wGVbGj3~g8_^JpBoe;rvC}v6Kf$E6(fWi9AND3uP zhh-7ND`I37^9UyiFSaVSkr!KeLH!ng1Y*O*BSlzRHB80ZRH1s1pyrjtlQ6>z@RROC zkwsrH?gihRZyhdHGG~5}$;Fq4KX$JCDDS!Wi6?*MX5RYR`kv>OYQLsQyE%J)(~7M2 zpS_LkH|(y3vvX%3)IHhXxVpb_`GtSH4b4eggb19&rGPGeM(!@@yrzxJ&B$S=n z{?oK3lhy};8TAr$&$m!$ewspQ0&W_e>bT5J7wI1>aB$@e4^ ztXQ?laji&Z6cQGI`vIYn1il#aTJXh~wo3TgVFIN&k1Fl!p!TuY4ntmrpY#hTvgq^Y z4`(~qEBD@W+;!xJ3YE2wD;t+hPh2NgT_+2!)~^svcM!AWMq}&Bz{j;~jjt?{_bhiU zIptiKrT?F z90Ry0HOD|^s5Duo=#;iLWF4y2tOJ!Hab!M#C^Ta(#puumF}fvnzE6*}ahweC!Lwm3 zgw<*@j(^pt1G}H%NKLDJVk^j*s`^$;pinqmRFK#P{XO2I;)^fBA0l{rB%a1a1uo9u z0`op#npRB$@nzh478kGJ;#DXld`@Mm4gLvd5`t>XE%0Ioba&%%ccB2i{$7>t!YU1# zTk}9(bJb$awI6y|vlRN}*n4BkqbnnYx-*3%{>OIzx_i&Pi+3;PE*IR5kKMlC< zr@gy-Qk6v(2q`ISAt3!A)&Tmcy=sb2(ldT~oT<#he=<2zcB3brk z%5;lORpInfDJd8e1YTlOyC7k+%_@nQ_@n|Q3aZ)wV@xszWU;xJhAZ>m&sr@tD}aMk zU&@Bl9gyT=Z>LOtHbtwctyEpnk!x11JU_@bgp#5Nq#4p_PYG`-z+8iz8E{QDENgkc zZF#KiSw6O5TawS~OI_+~9~khLlB33UN*jS>R57DyPzu#D>u6!Zq3z1&b*gcq;_MGx z9cVw-9SCA>tukHJ`Ky*v#9_8rkBbHuG(2@ec!<)_un13>s~%HYlamT$;FPv)47v5A$Kib+xAiDSGE5X zUb&fnwSR43=*htF>cDXRW-!l<6$Zxh{o#DY#F}*yPhb19AHehry>q>}t4kM_Yx4(M zK6M=5bb$5otJp3(A&fuB}nm*X<6KWGU8Q5FbS_2oWCyhB*3CNQk?;GtuIC6d@>@O75@eXb1e+_z3Svgd!&`DW+b;LnCWrQDk&qW1sS=9CFpt3k7f%0J>` zKl6zESLS!jr_?2V{Opy#y|hWgfdAQIkflMwj`SDQIrtwn=eZN+-=kyaeB|$&2`K*; D%)!i0 literal 0 HcmV?d00001 From 705679f62ff0d0ae9502d21b9cc9478e9af7eadc Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sun, 7 Jun 2026 03:11:48 +0000 Subject: [PATCH 3/6] chore: apply manual instructions [skip ci-relay] --- .../test_caller_permissions.cpython-312.pyc | Bin 7019 -> 8443 bytes .../integration/test_caller_permissions.py | 71 +++++++++++++----- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc b/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc index 6b75d8e8a325193463c960dbd30ae57104d9c61a..f9ebd858c7ff585bf8a87b4596fc3b104ef37a98 100644 GIT binary patch delta 3989 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 t9=W~?{s)R>SKR;r diff --git a/tests/dev-lead/integration/test_caller_permissions.py b/tests/dev-lead/integration/test_caller_permissions.py index 8d937aee1..8096d5f0a 100644 --- a/tests/dev-lead/integration/test_caller_permissions.py +++ b/tests/dev-lead/integration/test_caller_permissions.py @@ -40,7 +40,10 @@ def lvl(value) -> int: - return LEVELS.get(str(value).strip().lower(), 0) + v = str(value).strip().lower() + if v not in LEVELS: + raise ValueError(f"Unknown permission level: {value!r}") + return LEVELS[v] def load_local(path: str) -> dict: @@ -64,24 +67,49 @@ def fetch_template() -> dict: def reusable_required(reusable: dict) -> dict[str, int]: """Max level each permission scope is requested across all reusable jobs.""" required: dict[str, int] = {} + if not isinstance(reusable, dict): + return required + # Jobs without a per-job permissions block inherit the workflow-level permissions. + workflow_perms = reusable.get("permissions") or {} for job in (reusable.get("jobs") or {}).values(): perms = job.get("permissions") if isinstance(job, dict) else None if not isinstance(perms, dict): - continue # no per-job block => inherits top-level `permissions: {}` (none) + perms = workflow_perms if isinstance(workflow_perms, dict) else None + if not isinstance(perms, dict): + continue for scope, level in perms.items(): required[scope] = max(required.get(scope, 0), lvl(level)) return required -def caller_granted(template: dict) -> dict[str, int] | None: - """Permissions the template grants on the job that calls the reusable.""" - for job in (template.get("jobs") or {}).values(): +def caller_grants(template: dict) -> list[tuple[str, dict[str, int]]]: + """(job_key, grants) pairs for every job in the template that calls the reusable. + + Handles string shorthand permissions (write-all / read-all) by mapping them to + a wildcard key ``"*"`` so the caller check in main() can fall back to it for + any scope not explicitly listed. When a calling job omits its own permissions + block, the workflow-level permissions are used as the default. + """ + if not isinstance(template, dict): + return [] + workflow_perms = template.get("permissions") or {} + results = [] + for job_key, job in (template.get("jobs") or {}).items(): if not isinstance(job, dict): continue if "dev-lead-reusable" in str(job.get("uses", "")): - perms = job.get("permissions") or {} - return {scope: lvl(level) for scope, level in perms.items()} - return None + perms = job.get("permissions") + if perms is None: + # Fall back to workflow-level permissions when the job omits its own block. + perms = workflow_perms + if isinstance(perms, str): + val = lvl("write" if perms == "write-all" else "read" if perms == "read-all" else "none") + results.append((job_key, {"*": val})) + elif isinstance(perms, dict): + results.append((job_key, {scope: lvl(level) for scope, level in perms.items()})) + else: + results.append((job_key, {})) + return results def main() -> int: @@ -89,28 +117,31 @@ def main() -> int: template = fetch_template() required = reusable_required(reusable) - granted = caller_granted(template) + all_grants = caller_grants(template) - if granted is None: + if not all_grants: print(f"FAIL: no job in {TEMPLATE_REPO}:{TEMPLATE_PATH} calls dev-lead-reusable") return 1 failures = [] - for scope, need in sorted(required.items()): - if need == 0: - continue - have = granted.get(scope, 0) - status = "OK" if have >= need else "MISSING" - print(f" [{status}] {scope}: reusable needs {INV[need]}, template grants {INV.get(have, 'none')}") - if have < need: - failures.append((scope, need, have)) + for job_key, granted in all_grants: + if len(all_grants) > 1: + print(f"Caller job '{job_key}':") + for scope, need in sorted(required.items()): + if need == 0: + continue + have = granted.get(scope, granted.get("*", 0)) + status = "OK" if have >= need else "MISSING" + print(f" [{status}] {scope}: reusable needs {INV[need]}, template grants {INV.get(have, 'none')}") + if have < need: + failures.append((scope, need, have, job_key)) print() if failures: print("FAIL: caller template under-grants permissions the reusable requests.") - for scope, need, have in failures: + for scope, need, have, job_key in failures: print(f" - add `{scope}: {INV[need]}` to {TEMPLATE_REPO}:{TEMPLATE_PATH} " - f"jobs.dev-lead.permissions (currently {INV.get(have, 'none')})") + f"jobs.{job_key}.permissions (currently {INV.get(have, 'none')})") print("\nWithout it, every @main consumer fails at startup (startup_failure).") return 1 From 0f373d779388fb70123f99ce9daffd20eba69f3f Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sun, 7 Jun 2026 03:17:47 +0000 Subject: [PATCH 4/6] fix(reviews): address review comments [skip ci-relay] --- ...ller_permissions.cpython-312-pytest-9.0.3.pyc | Bin 0 -> 8588 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312-pytest-9.0.3.pyc diff --git a/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312-pytest-9.0.3.pyc b/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..896b04c15d57d09a65542883f27f885463c8f62b GIT binary patch literal 8588 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

hpeP#D$r z8^UE667m1vD}fF|)19WQs~*xC*Ve4-nRLgOE@rMO>uP`=_qN&V zS$BQ&u5&kE?F+*TqTcO?sP|POWik!HXefFDa5?W zWt(#{@z&wlv-fS=mhJB8&bi7t>-?t7rp6_E)ADAVsd(S>W^4CkH}8e4$a?6;Dac`3 za<07AdLJdTMU zoi$BgpB3hI%~fXV_e0*peeC(|=FGAFrSk(1&%d^K{atjV+Wb)t9rMrS3(cEZR@hpkeK+lCS-MHoc&9d^C00bDQkv4 zzeK&7v)PrTO}6;`_&oi7D3U=f4Hv9r5=vDbO29Qe6+roM3nMF!K`68-Ro4{6g?kfD zoNZqO7>qv`Ntc0yqKAbE3fjB~pCUuNI&JL>moIy^$!5Qwiw69DnfCjK;!4fVFF^Q5 zvIT$kk|4L2?JvhB{Q9qIe{2Gm@4c)j@&Yc|dj(zrjj~nm!Z1(mqv2&d-Oma_Gu~Jo zzCj5DDP=g>b}}3T15>^vsP@;0$GrH43$v=-_2ZUqm_iT!i9ZA=M!nvOv9VxDN7yj0 zNSYWEFt$-j(*wtgp!Els|hxkK62!=4c5PuIhI8uW6nkprz@9Gh;<3A936aB>T z1#*9Z=oN}0O1_|6D>N!Cn>{=4UbH@&Hsz|ey=#Ble%txbw0nk{CZ?ZHU!C0khLkn! zPLq#H>eG%~$sT;$pLVR8D58ATX&`D>X_|O(wZ=u1t?t}Rl&&7EB^)cS5Qwr*Chn8f zIm(_s`qPd#I&Sp7)&GDhTOkou^WQ6R5?M+?jfs*!VAa}xkNju)Q~CjQN}oS@_ODK@ m7-7QyuF}X{FJeUUE9!^?eTp1M4w-)Ec=pHv@^?N0?*9jX1a3zF literal 0 HcmV?d00001 From 7a9f6c7f9b2bc167a874bd512d5c17ec0a920918 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sun, 7 Jun 2026 03:23:07 +0000 Subject: [PATCH 5/6] chore: apply manual instructions [skip ci-relay] --- .../test_caller_permissions.cpython-312.pyc | Bin 8443 -> 9162 bytes .../integration/test_caller_permissions.py | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc b/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc index f9ebd858c7ff585bf8a87b4596fc3b104ef37a98..724e074d83bdb13f7a6d9dd7cab3cbf54f9bec76 100644 GIT binary patch delta 1356 zcmbtSO>7%Q6yCLW{Tu(_+Ez+KJ&BVLr%jqtpiMyyY190mv_PUjIkfTAJB^)ecB7e1 z4AfOBA*3Sp;A$?Z)I*Ap8mUO6%B9>mk^)i`=TMg12ofh0(F0Ps@Mi7AaN)v8{``LS zz3+W9@2~zRL*eg3p@0PE$6s2fA0K)Y9tpW5H~%nnPbtfnyZE1>4~BdqFU~_(^`G`y z!Kx)mB}G$aIwXm@G}Z8d=c##KL)Lr`0SS#cl|&5b&kU$>hC3GI>NsWx2c{RUT5yqfTuq8KeT)I^wouw zJdTiKid2?w_b5_H9+xsf{0b=ICnJN4fdGOtMsgMp#f>p*u5h$+EZ7Z&bOG+Ut5qy=NjX_q&PkcS>Rp_5Y-8y15aO6DkG z1RUTn8L-sbn})+%EkUfHk#47x9h@U{w!m1yG?=Y`m>zcVxxY4xu2Zpk&wYENP(Qd- zytgfPQn73qA76^38fk+_#6`kAi$Ahe&@#&)`6P0@(Y=6mTV*+Oj@YVmDNM9LJQPLo zVX)4@k7eNjZc4jh3AOOP=o$AjDaLO`Q;WewncN%S3O4V=63dB|Bb&{y-I@A!;=Ae1 z^OwGPedCH&iIH39wwqGRz6TBW8&<|wn=9zm)m)|N^sV9Y_~#d%HpI6h2bbEujBiES zcfjiYD!moy-bo%@zOs6Kv+eBNmp8_;8=A2>W~_TQtWPS*>t**gN-qyT7`>kyT|Kah zE3Ky1Cfe0^!6a*k zlIx1xyY}mgMR`$NBo$$LL_mjt41n#{iD7#ftQyg1p9l+L+dXpeeL4zOCP?2B(F+3l z0qg*5R&B3r-!)v&=V3DXj!1c(b%RD?7F#h0(QGA;7YP5ksa2hX;HPWKOTWo!45lM9 zG_oRv^oB=P1;(Y~lLbzhN=wSgT1k~kDay!)>$U_h|NMb+m X7&1I5IsR-w=AWTX*M!0!p|}4AiArDV delta 658 zcmYL{O=uHA6vt<>JK3gTv+X8_sMJl;THMn5K@NUYTl+yYr39oJks8XjStW7v;mo$t zu*KX2>1{4jdh{UlB)N)r&8>*Y9;Dv%=Eq3{bzT;6V1N66|Mz(FW_R9R{WTZssp=pS zEB9%vI-GnN%d4RenBP|SLZw~)Q@zDsX*y2uCt9AYdU7?wJBbl>3ZXk8gtp0c*p2c( znlVet^1g6jxoYr6$P4e|K>y2m*dv#OwR2iVJ9{$n@Cq_XJIB|Da>x8=)26iwW0gjo z`Es?f&I%NonhJkEY{Y$1aqSxOrJD7SCIVm8&+3G2G@7D8?^_hZI%0BM1*uez{eA&LeD{X9&iCL&6~;0=~lKsO%b}C1ekyo0Q?qu9FPSB2LM&#FEXbF?+OW5g7x@ercN}R R_%X(Bj823$CEgib`U|jWo>u?> diff --git a/tests/dev-lead/integration/test_caller_permissions.py b/tests/dev-lead/integration/test_caller_permissions.py index 8096d5f0a..9c8fa50f9 100644 --- a/tests/dev-lead/integration/test_caller_permissions.py +++ b/tests/dev-lead/integration/test_caller_permissions.py @@ -65,7 +65,12 @@ def fetch_template() -> dict: def reusable_required(reusable: dict) -> dict[str, int]: - """Max level each permission scope is requested across all reusable jobs.""" + """Max level each permission scope is requested across all reusable jobs. + + Records ``"*"`` as a wildcard key when a job (or inherited workflow-level + block) uses the scalar shorthands ``write-all`` / ``read-all``, mirroring + how ``caller_grants`` represents those shorthands on the caller side. + """ required: dict[str, int] = {} if not isinstance(reusable, dict): return required @@ -73,7 +78,18 @@ def reusable_required(reusable: dict) -> dict[str, int]: workflow_perms = reusable.get("permissions") or {} for job in (reusable.get("jobs") or {}).values(): perms = job.get("permissions") if isinstance(job, dict) else None + if isinstance(perms, str): + # Per-job scalar shorthand (write-all / read-all): record as wildcard. + val = lvl("write" if perms == "write-all" else "read" if perms == "read-all" else "none") + required["*"] = max(required.get("*", 0), val) + continue if not isinstance(perms, dict): + # No per-job block; inherit workflow-level permissions. + if isinstance(workflow_perms, str): + # Workflow-level scalar shorthand: record as wildcard. + val = lvl("write" if workflow_perms == "write-all" else "read" if workflow_perms == "read-all" else "none") + required["*"] = max(required.get("*", 0), val) + continue perms = workflow_perms if isinstance(workflow_perms, dict) else None if not isinstance(perms, dict): continue From 984f98eeac5bbe95be50f7caddc60ef380d4c983 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sun, 7 Jun 2026 03:27:40 +0000 Subject: [PATCH 6/6] chore: apply manual instructions [skip ci-relay] --- tests/dev-lead/integration/.gitignore | 3 +++ ...ller_permissions.cpython-312-pytest-9.0.3.pyc | Bin 8588 -> 0 bytes .../test_caller_permissions.cpython-312.pyc | Bin 9162 -> 0 bytes 3 files changed, 3 insertions(+) create mode 100644 tests/dev-lead/integration/.gitignore delete mode 100644 tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312-pytest-9.0.3.pyc delete mode 100644 tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc diff --git a/tests/dev-lead/integration/.gitignore b/tests/dev-lead/integration/.gitignore new file mode 100644 index 000000000..3bbe7b6f9 --- /dev/null +++ b/tests/dev-lead/integration/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +*.pyo diff --git a/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312-pytest-9.0.3.pyc b/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312-pytest-9.0.3.pyc deleted file mode 100644 index 896b04c15d57d09a65542883f27f885463c8f62b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8588 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

hpeP#D$r z8^UE667m1vD}fF|)19WQs~*xC*Ve4-nRLgOE@rMO>uP`=_qN&V zS$BQ&u5&kE?F+*TqTcO?sP|POWik!HXefFDa5?W zWt(#{@z&wlv-fS=mhJB8&bi7t>-?t7rp6_E)ADAVsd(S>W^4CkH}8e4$a?6;Dac`3 za<07AdLJdTMU zoi$BgpB3hI%~fXV_e0*peeC(|=FGAFrSk(1&%d^K{atjV+Wb)t9rMrS3(cEZR@hpkeK+lCS-MHoc&9d^C00bDQkv4 zzeK&7v)PrTO}6;`_&oi7D3U=f4Hv9r5=vDbO29Qe6+roM3nMF!K`68-Ro4{6g?kfD zoNZqO7>qv`Ntc0yqKAbE3fjB~pCUuNI&JL>moIy^$!5Qwiw69DnfCjK;!4fVFF^Q5 zvIT$kk|4L2?JvhB{Q9qIe{2Gm@4c)j@&Yc|dj(zrjj~nm!Z1(mqv2&d-Oma_Gu~Jo zzCj5DDP=g>b}}3T15>^vsP@;0$GrH43$v=-_2ZUqm_iT!i9ZA=M!nvOv9VxDN7yj0 zNSYWEFt$-j(*wtgp!Els|hxkK62!=4c5PuIhI8uW6nkprz@9Gh;<3A936aB>T z1#*9Z=oN}0O1_|6D>N!Cn>{=4UbH@&Hsz|ey=#Ble%txbw0nk{CZ?ZHU!C0khLkn! zPLq#H>eG%~$sT;$pLVR8D58ATX&`D>X_|O(wZ=u1t?t}Rl&&7EB^)cS5Qwr*Chn8f zIm(_s`qPd#I&Sp7)&GDhTOkou^WQ6R5?M+?jfs*!VAa}xkNju)Q~CjQN}oS@_ODK@ m7-7QyuF}X{FJeUUE9!^?eTp1M4w-)Ec=pHv@^?N0?*9jX1a3zF diff --git a/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc b/tests/dev-lead/integration/__pycache__/test_caller_permissions.cpython-312.pyc deleted file mode 100644 index 724e074d83bdb13f7a6d9dd7cab3cbf54f9bec76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9162 zcmcIpYit`=cD}h$Pa478A+7*^32e( z7&5DD)2_TxYYDrkmD)jMz(BNc&NO!{zG1#6~XgQ-)tHDWF11^;2+v!QY*xAh(PEz z;!ro@2+j~7f`)Dbf$L;|>?V~r)lDgHW4FCH9DSGUw(+*X3XO2~{UB34 zif*A>gpc$ipWzCP&}9Qc36vn>L>&FnFm9!gk4z8=)Sbs!x*eRg+sWCw^SL6<{tne$ zz!h_O@Gj&^I0w9oxKhpu?_#cu%ZGOfj4Ob5DPP7F4%H$glp7Fl94yZAq1B+hRO9IZ ztQtQA8X-r>YQ&qjAi+f-7(;Uud|#;c=awHC9{}h0QO2z~2WQGszKF(uIgD zxAV-&=F?1s7lM9K^oK)?FC3Bt)+f2G)?W=`}mY-l7qBaDL| z*P^^AiOi`p?ai2INFk~&#wzi_NC2-?rC!hQ*Lh(CFF=e2d4Y-eLm{36?o@X4we+$6 zfXDzZqQnYPG-9o-%GA8LD=P3+F6LiHe}(4~c*ZX>p)eyvLy|wpGrS;#g?fbzXu_SM zAJ`aS{Gsbu@*uX(0ceFk{sxE~7q#Ta%V1BbTyLUe^~7(-#&pB(poq zh`w-ySLL;usbEx;)V*U^CIH+8tOtIgNH)mc@CT#8mn(#uQH0zdWd)8=_Y2=VN!%nX}}-qSBC0B;=;Tb+NQCD6(FO;ZS61+>OAKMCC;T; zkGC_o0rn(39D+F`;dLp&X8ICkhntA}YDxp?SQE7p8`Zv6r%du_xY0TBTU zfdCU38DWC~*RKhg>hlNqZ?V<;78^_#DN~_vh?gm>=dy8F@JoEGbZu^8Kf0jUw*RI0 zd>Lzv7lpudA`)!y@}EKFHj?zdIMQtIAoQ<(G|N9|T-}3^)`u}k${-^`*9pNPS+vm{ zpw%72`vz@}pXH2<6BzzFkjrp7R@oIA3SrgCIa|t^@h~o$=WFweqUXV?# z&F7k1J7g;WLX>aDX3J%ejbNmryi8mdvGf_{`ECgtSY5pu{UM15dy03q5qEjjQ|rx6 zE4m{iG8woY5HJ|l!M}J2icz#k+s2yTwLYQC=jiejUHOD&=4d8GSB)M@o9SPf-u~&L zqdd88_bfkqF?b1|n$1mSB-7TOvVa<(7b(VEBl9klB#k z0Q-`@IfHmBS3Pk;6R^*~fm_0%Ea-NiYP181#4v#AMF3#{?+m=UsM9hb#Eem?ui;ft zdzugV!W^7qtg&F%LNbL_TDFL6AMXu>Sq?-@MObM-HuMc-L@X2mFR~T)!VnOrun`(| zz`uwKkeelMjGH(zy=}o#w}c2|)lBcAv*^}aH{Y7-O*uFJfrO?%;Pz*@crJpkgKm|$ z$-gx2HT$5hKUk?0d5wee=E`d@<6!6`p@JpIk&-T}JBXvQN_q#qPjN=h)NcfL%={6p zNjibzXb>~7>ftOyd%;P%DA}|Q&borOEtfWD2P$@LWcEGi%+|Ge&0*n1I5O>WkdgfOtKMw%!DyUY>ZhOvBw5r2X(5UVendvc4t~T zK^q-DHX?yb;|)ipNK}$3iN7HUSRZ1gs@e91qX3#<&Q&wdJSRNQ39En=L%=7{<&>>r zv=`hJA1{hB32u*^*V%lkt@Ti6v$wsu?M%+kwnLpQvQ^+=N_w;=pwh z@_-;%s}WJA_#1vnz(N%uh(NNbmlgRvO|qqTPZP&0-{OyeZ8&(O08?X&%&4cyp&*i1 zLpfm=bnStE@eUND=&7~%miwkV4F@YaWxAV~3eR+ZmN)y;Wa;rorOC6GlJwy#iLY6*dl^CFRWG@;35gOO^vYl74bE-4+dTq6448H2{axt=tVl1ZxJ= z3@;g}1CBOw&>I8ACMuGG%_5ffB8qu!K>SA*8{FN!CCghSL2Zt9K9&> zD>?m!AK}=rf@8z~6USe!;OGU81w-7hhOA#0ClcfT5vP90?jA*W=at~vlraC8oa3gf zoO5>3tnHM(2SnVYNWPH*-5TFcA*|Id8@m&f{}g*ejRzQ>1)G_5J=ADI^Dx0X0T+|? z31Jb!IdCvF*H{Tr!MDafZ9A9}@O_z{o~pX4o*o9m8I~D_Fu{jIhanz^XAr!y_#37c zA{c)Nyms(EIYx87A%e!S38vNy06yfxK`)MRdEkc)umU3vgasU(D)T7bYXk5Dvo*rJ z*r!#0)-ypr_QSw|P^b-rhoL{?V5vbYd;@kJtQ|J!Fhv-`>m2G3pW|sW2qOA%2!Z*M zZCN3KX~i-2>E{-m69Fe|p3@p90(KMxeC}kTPQd~fEnEGf;;4sw;D>X5pCpt0JQ!}p zVH5?7){4U?lMtQBMvzcYgwKGenoz(s)$5lXn&cF(5g-Hj0U&@J1Jr>Yc&a!m1Wy$U zR`F->BJiV{_X3TmSEDJk!2CzxU+e(|8AVGD#8i#8EZRyY2JTY}wykMr*_iX|k`1>n zPhOsGNR@1#FWEDDc=U8Szx=j+(mr)AmA_>^zi!6x#JO|MxijT#T1JJ&W5gJ_Sl2Y$ z@!6Jzx`PwsZS$mgDsLgb=3(cU5fkU{b}ZO7KP@boE}3~{e)I0h{)b(U2IreEe7Yxj zl}i=!V@DR(Z;2*dXU$Wnxj(92 zaJrwCR!m==eRF>6;rF&B+k28+Xudu4r70=JQl)QBkc&+9bjydW_gZI*X4%x1S3jvv zF$X5-siMg|Sj}S1_SubJ6fe{qn>_vK=vZ@h0+=nme)9(>-#dBd6u63(W5mtYvF3^0 ziyJpj*M3-kuYM*9^r~L{tojrFZ|YJTn}Nx$Uz~Xc2zHEEs>ae-szLVR(K9c?6yAOS zC}Q~?Fd@0FWgNW-1EQfHqL!Bc&x;U6^#7m1)RlkBn5xwvvzFm21NT>t!@vlx1*qTz zYbNVK29~cws4%|!CFAwgC5&<7C_ptpQx?vRYd}Y1j+q39TZcC~Zsh39FAJD*t2Tea z6gLd&a0mO6dEz8zi<|sJpAPK=8o#+>_7|ie9DEtAxbcS_f?MWWbSC!-uC@XX<^&x# z=d4DDXUN7DDcM*$VP3&q`en2G(TZ{Z4)9>;fIP2~?F1^2?X(S zaRno<8CDn5JHiAZ(}%wULk(_ySh3HmAV&iu$id~9b4aoHhHyd&C**2fN{+P;<618Z z?U2UwgNq&(RDzHyRg9?Oxbrt65IFd8vN7PtsOV&Aupb^Pcp+aUQy4X6BtV>q z6hIPQ#Wfv^gnjVSWfnZR^)*~z2SGT9i!Az>lsqcuvdFZyox))lBAWp;ffG?SXH-JK za3LJUMKdmr;o>+HBAycSt2`>=C&+Yu*=@(9W7<4ZJtNLkCO0)b&fmRQux_gP ze$zrht=^q%>w405X|C;3s_n`X*VWJM4~-9_Gh06{n(g{c9I*R8J95R81JmvCF7*HE^CB1LeldLkl`>$4vj1LMZRx*keL^9J`}(Z z6(_;p{XApPj0saVj}RxdvEbi>Rte6WS-{u>J=6R87|1K&w2$(Dk<)VwT3a?bs8NM^ z|0u_h(wtc_{=~RFZW=7hey7#q=C#{7_)>?gjKNP>;+8l)bQ!oda0-|ttU27<;#T0^ z9yddoCzb0n&^Y-4ZH{c)nw(fTr#2&GX>_wsAZ1qI0RS@$fQjS$RS&F!hEc{oN|OVC z)*n-7{O>4~!t6@o+Ukma zKtqa^eJ#m;_2cX3O5e}Ray9-&?mD=#Y|jXl!(Dk!yV+H7I+m~IX5iupTW-JvVGArJ zR-Qqv7p$v*Pe{yu1k62L^5Ac_#SGg#igT|RR@rdoq-;KQqNC%)>EkhyVJ^iCSD07< zzFW%N2!?nb0+?7KfWXR|HAaaQV$GR+kvfMyCK`gwPJiFy(5}?Q$sTTsg13xvS&Q z;nrqv8>GOLgp?AqC{QB20=pyB;=%<5WX~aWY!V@8C7#qD4fAvW~CVjVSs~+6xc+#XfK#(oi3ZU%oHXI>lf?|i|cTJ;=bojRqafz+w}~YEeCF%gwUla?JSzO_KOh+ zd(uUfw_lrlZ7Mq5|KZTRq1o!$YZxnwJYz@G8#YgO{>A0V&Y6+1W0?4nDbvJ_DPel+ zbXl@?AA~#Hr>;-dC6At4IN$x`{MEVhSCd{}k{?Q)46Fbqllb3|pja`RM0vdZ(>uD{*;>E4G; zk4jUowIw}glV{tPNTR5d_#P2?UBnWJiGOE87Dv)?Zh<}z5`IcqlGgJJ)P=OouEc6G z?e*gQ^m-xX3z=b@ew9f`yt*i1J#4S)K(V-nJu8<%$kr)U*A$Nnx5gYe`o9F;FTUNB zt^f%|53>UlC-V~AoCkPy*vb#CTyd4lX0MkE`@CM+>h<sNgGPU7{!= z|0~M5WJN{AQ)g!i<}9y_n$qPPKd`@NzvFmf+BQi|5EDB_uZ?YfTS}R>jgnvI*N#Hi zz7rSwM)Q_U6j8G5Fc4MCRx9!9a^)tXc=;%?jwo8*Uq$3Cy+I(#J{FrNE7FvG^vKU! z-fp?s`R=*LRPhptsLKCb>Y@-`1PVvw{|+m`zK7)BTR*oxrcUbP$IkxA$t5EU`0r&a fGS`auR^!hp@R5I$cX)^Cx9F9_Zt}Mc1eE^+-sSIe