From 5e501c0689ecc5271acab72fed137ec45875db50 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sat, 7 Nov 2026 19:47:41 +0800 Subject: [PATCH] fix(governance): relay fork review events and scan non-UTF-8 history --- .github/workflows/pr-governance-tests.yml | 3 ++- pr-governance/README.md | 10 ++++--- .../__pycache__/secret_scan.cpython-313.pyc | Bin 0 -> 15612 bytes pr-governance/scripts/runner.cjs | 16 +++++++++++ pr-governance/scripts/secret_scan.py | 11 +++++--- pr-governance/scripts/test-runner.cjs | 19 +++++++++++++ pr-governance/scripts/test_secret_scan.py | 7 +++++ pr-governance/templates/codex-nudge.yml | 2 +- pr-governance/templates/gitleaks.yml | 2 +- pr-governance/templates/pr-review-signal.yml | 25 ++++++++++++++++++ pr-governance/templates/risk-gate.yml | 16 ++++++----- .../templates/weekly-secret-scan.yml | 2 +- 12 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 pr-governance/scripts/__pycache__/secret_scan.cpython-313.pyc create mode 100644 pr-governance/templates/pr-review-signal.yml diff --git a/.github/workflows/pr-governance-tests.yml b/.github/workflows/pr-governance-tests.yml index 57e8704..09b1e33 100644 --- a/.github/workflows/pr-governance-tests.yml +++ b/.github/workflows/pr-governance-tests.yml @@ -20,9 +20,10 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: Test the gate, label binding and review request + - name: Test the gate, labels, reviewer evidence, runner and review request run: >- node --test pr-governance/scripts/test-gate.cjs + pr-governance/scripts/test-labels.cjs pr-governance/scripts/test-codex.cjs pr-governance/scripts/test-runner.cjs pr-governance/scripts/test-nudge.cjs - name: Install the pinned scanner run: bash pr-governance/scripts/install-gitleaks.sh diff --git a/pr-governance/README.md b/pr-governance/README.md index 1b82be8..34cae99 100644 --- a/pr-governance/README.md +++ b/pr-governance/README.md @@ -65,7 +65,11 @@ organization community-health files. 2. Copy `templates/risk-gate.yml`, `templates/codex-nudge.yml`, `templates/gitleaks.yml` and `templates/weekly-secret-scan.yml` into `.github/workflows/`, replacing `__GOVERNANCE_REPO__` with the - `owner/repository` of this governance repository. + `owner/repository` of this governance repository and `__GOVERNANCE_SHA__` + with the full commit SHA at the head of `v1`. Copy + `templates/pr-review-signal.yml` unchanged as well: review events on fork + and Dependabot pull requests get a read-only token, so the gate is started + by that workflow's completion instead. 3. Add the required contexts to branch protection on the default branch: `pr-template`, `ai-review`, `risk-gate`, `Gitleaks`, and the repository's own checks. Keep `strict` (require branches to be up to date) and @@ -79,8 +83,8 @@ file, which is read from the default branch head. ## Trust model - Workflows never check out or execute pull request code. -- The action is pinned by the `v1` branch of this repository, not by the - repository under judgement. +- The action is pinned to a full commit SHA of this repository, not by the + repository under judgement. Update the pins after `v1` moves. - The policy is read from the default branch head, so a pull request cannot change the rules that judge it. - Reviewer evidence is a marker comment written by the configured reviewer diff --git a/pr-governance/scripts/__pycache__/secret_scan.cpython-313.pyc b/pr-governance/scripts/__pycache__/secret_scan.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..856784bc80088c910147c01b7f549aae76333ddf GIT binary patch literal 15612 zcmbt*eRLC7mS>edOW*PrvW-6qumJ(GO)wAx#=+PI8!(`<2tfq3u*$X)S#rOU0V|yx z(mk{7By$F5lWg4EnTaQxvoRg^H0fkcnAyw>-O0>k&Y5$jgq+B!c)GJ^a?YOqV|NMA zn{@u#dta$avSaAovk(00)qAhr_r3Rb?|r)Ka@iROf9?KXPbB>e^KbZ~1(TX!@BWZw zm{%BqA&kHZh9m5Nff#s98;=-?5uT)r|dVGyhWx7`r1G7RyabwNp)RuXJhYm~-23opTqrMYTa zu$du2D$iC5_J9LYRcCF%ZQrf8o)H{cAH*j(YZ+24xFD_(+z{6a9*EZpUZJdo70PRw zCk&)csDQkBp%UT-p-!lRT$VHnYv7$1>IENtUnevO)ikwUXcTH_YJ*SH_<_*3m|PY)M% zo$d;rJtM=gcXvVJ9Gt$eUK(QBS%HNW)eSldiC365CX7{Rs9;X9 zlA(=}46wFPFI9G$u583`%%-lYWJt2Tu!-=`&|>Iio1iWae@S+P@f!yfF4)_1;NVbi z&s75fYGnxQ1{_jSiw^^x2*IIBcmQ}_osM;aq@ECm(}ai2IxKC6=rVKLz&dyTWoype z{fpN5s+rLjleaug@3dy!-M1MiSzN=<@3~`y6ug>u|NfS{n=a?`?sb-BGlsFNGp>YQ z&l%xJLWUZG`a#rk7-l`3tsow#6=5Z5C)qHrB@H1q%xUSJg*G=AGGee0VO$Iv1h&W; zLM*KF2MjZA3M!;n!c54*LzJr@}!21^(&({e&aASDBml3@HU328k?m-lSaS{B)G zxzTdJ{=^uj|8_sV?vY= zh!1iUK9P#XBAS?3g`=R9l&~GjQEoGahwRF({`t`Kp_$(224|o6)VcZAuKogRsj3E% z26Zz#x(kAbAD;wyGn`7Mqwyr4N+za22o;2r##eaCDkXwvIp` zFjp?fPGQMYmG?AeJ&kkLc`nOu&wC!qdLCLTuaoQhbL9iFb709+GrMVSYqqxKJ!9U} zp7pffu-!+?pli9nf)>nL&^m*WADabxt3!GjVUCNJ+H#H1(sqlWO>*M4T3@YN@U?Hok?ls zz~Tx?jm1Sf;d?yxGz2Ne*4_$vI;$?`L)$vDF6Ig>B35k#dJp#<;V2^*Yk`rJ^zSXU zrlr<1rvbNjpop$#m`PZzi494Uz=n)_p-K$K4rvojBl81kyZU;lAjxkY%y@Kg7avWg zh?r>>Qj=s@a>C=n?^PKX339iZ&8jf3q1Kn5F!gpKG-WC;$QvO0jGW)Vq{ zf|!ETw~a*OiO9uh;z?yQ5mEj}B-(96?N%zXKE386(4vI9c$ryrS4=zT0Yc21O49cI zHiCPvn3B(V2*<|@)NhST5uAx)NJ(Tn*0LtVOTZ%}#WDet1LSMM(I8$1U_2_O z$-`KSjVO*-oWS@}BrZw_BVjot><6e`!!T8=PJ#46Eo1@GPa(R@0F}DSXEwfY_Hy5% z)%D}2o_$JQvnglYwB)Xp*LLUJ2W9Ty7f|rbv(MzMzO2=^mxUNKWXhR{qT;NS>MCbh3vZuD>)pUNKe9SaYHfS6R7}JoxS@%A@K^+ z@CN}D!o%XJ3+Xh_Y77}pvLRNd1vnxlU<}XBig4UhQm)fwvC@M}1*2dR%z{O*c37L3 zbxc^NXDij(c%}%v1Wuz=fe{TOOsBDt0g-JRRE(heWDJ3WLmBgAdL;0$VkP24B04Pg z`7Me?O2<-@pf$l0&Y)tVf`}87!zt7<6pMQB74z_zIQ-;Y>JUgCP99sq5Tv6CwdiYPw%P0(9xrc^|;WB zltuQzn)r>R8y@5^M2Zd0@Z>lgYpDP-RJtPrSOX)hB@u`KX>%6y2^M6+m1mxRW_EHe zcJ1JG%iBlfhMpUKh<^QW*PCbFio6l|=uE!%(QNOdOU+Kc~W5ZdipJ^l>dC2kYRsfVWUpCX$%(1On1n>S7?B0?qY;m>435 zV5LY9BEJDnBU0zLTp$3tbbMqg<2u}Xpy%9EDpq_Nh{u>njb_Y&Ks1r~)*T2S_zDz= zBU2j)#Ns0(fpHOdvD7&dClcC3kWqhY2u#3V0N6bqWW^5Vib;YLCsO`TVCNad4CE4@ zKw_0r2^axV2mx~qL$*LvpcTbLQmHg~404u{iQ-nr94QQkh)^ShwcdvaD8^!@6jMI4 zYj&@^x#K24sKe8yMTh4~*Oji#a4ehxNyK)V?^9^0uhOS)0fsckh z-uU6!eD7n~-p6vi;ao$Q^5es~@_yObf7{43o@MWH?3x%mZTk|i-v!yRe%5x&u|Dqz z!2hfxP+I?v6>ENT&&;?gUp^x{_&-c`?cJN0Uu~*CXfh6?N}~lkBq;Wpres&bSxajb zYKPcPor}ld;GqRkJILCBjBZ%Q-B)YH@P}KWO6RJGuBdbtB@EA;E^kL{)Cxpo4C~|# zjDyO8kP*~J%MQcNLW`>*JZ=0quVWianTTN3=BO%<%i8Azn_w5rAr^XYgbbjL8U^Q0 zWM&W^)DL9{#Tl0LG0ON|g9?*rMya8i2tj7V;iQiP!KGKo3`%a~EcBM~1_EL-7D$Z* zQWMlNn6c?}x1zQCNg(n#G(^r}6oKelLrV+!9whu0(t??=z>G>$NE)hGiDJ2l3L(XW zDwARw1M#MCz%oEJ0}G`IY7|kEvp{5YY9s+=G5C}I1|qa&mE|34vyQd1J+BYUb>|(M zvW`vHF63LgvaMY=P5)^B*pP2Ml5IUwik_BCK#>ck+Qq7xmyS+zs5@Pld1`JzZtGqw zubrs{V)bmg3q~XVZ~Qy1)_x7C_Ag6t)K^xfY8{9#&N01fW~W@cWxhv#sAth#K7DHD zp_$msDcMms8-oV8rr$KR{KdfUAl3S134FV4h747)DQrDV28|X4;`$>vRDh4cf~Ko0 zMd_xiD?Ag{$t6^hdzvkgIC}mvuw^bGDUf*3k}mhO!&`(!0$0+WZh2~TCawAiLCFYN zSCf;1Eo3R_OJxG~08CWt2dpcm^PuCw_p8WI!BNt;ZkuY=X9Xj7;?iMQO}zp;omFrO zF2UVlOmZPEsLM0;8;k|dURd2&6^y|c1pQ=X(B!yI@~E6aZ8Zp9Z6;vh((PG6G`Ow^ zFcxEQ?TSoz)!NtEN!r4&o@xl$!f3^z;X5|6^)QZgT4_kPh?V56I$y0H9cL8hqPt)~ z6}ExRt9+2sEQ)6^293l=GrmATRlNe5u^$C<>S6|}s3xL}BM?ZY0_VZyAQF`R0EAN{ z9RU9X7)?ks3^?P`bJ22wUx1KDHY9ascq-~4zMo{rHZ@ZVb$|@@fo0rXP)Z^Iv?nIh zbwih&2-H`$&q$&Qq$b)qq>_uWMvstgL;s)@_pe+ly zYElAZ4gdozV$x(H?JuKZpK5U=1ihi1Xt1yUkYb5WOaOxgbbkrz$B2MK8Nw)vQ71+e zZ{U&;kRc4&gb|)I0{9M8)bpuS!f#hB=i|vJnNm#Sh+ev}$fB+)d5FHKo1<{BiV<8z zP|?8F2A(3Uhoe+D&7&cq2uJR!2Gt;CLWc$U0hD|n{v;RB18C1y$Mocno?hbC%v@OD z8kS5ovl|yo>lQs#uL*N&ej1+N^4{=`jrlzTANA#S9a;2jp5JnPe9^WMXM)kUAxG6uu!h{ zU)wQ%>FrM0*RjCuS#tTVoO}MWVn$b4qYqw;i;fD-2!&@!3&^X0`?7ZpuvTcxKs>uL1aQe{LliKBQ@ zP2%7*LLRM}RdGiO)|G-`{33~`#fUTsr=P6mYI^~`kgx&jX{+q|#n}Ar+0&m^ZT!WU zY7PHkSp_=UoJsK_n1WOL5_br|eEK$0o-oKtWH4NyQY452GajSfUtH%rM$<3G@s!Hj$_BYs)bDW|u4Eifh@t z$cs?%$M7emAVNihG24E8trlB+UX+4as?uCgoV>H@+saX&u( z?D3a-ULE-N1MeBHAAZ-hw6*m;%k>NI|KOb;y!%Y9tuMFr&{sy5?`IcJ1|d;3#4esX zgNZQvJ5bios%D^J9WTHDbXpjI4PIuDHI>MI_yW$nPD9$MdXN__X)5B9AO}6I2;k6X zi{_f3js%L;g8Hd1<`oUTg0aJ3V3L*+ieLm&$W)?N>;5ajjE33}o_{kSAESl_qEX0Y zA4ErbP58b9H#ScN;;Lbl)r?YrVr<6=pBE0c8KC=Wvxj1A=+ z4IzDON$2TnNcMtr&&y!-fH|DL)@kT8&^%WFRz)&O1{90OnqZwBK}@a)#nyP{*lu;E zVU(XVoZhP8COCatwOqj+MixdxNWT;UN|~o5Uq@7s@7I-niYYiI}~-O^BOMLXG& zL4a#D0#oc?_k?pJ%sOTpqn!+NWQ-VirV;iCZzUAWie+F&(NUJ-2!;j+dxMdo-eV`O z8o(Y+#F6+V#fWTY0EUop7R(SW==i$Il8czx78FM&!QnN^r&EAfs7Vr$4CT+5t=Snh zL`D7>J|q7QqiKki@o(gAlz*eqyM%wk@dA$oOhOGsp;O@IBSr9yae25G~zX}H&dxAO%%7-A8A|Pmb z3by|#vLCBh#pH!J0q>&`Jp>4=5Cozdftr=b^N>R&19<^cJVv;-)Dx(fhEo$$>hd5O zqs(40(|b4MPcWkmBMBnEi=^S9xU|iSP)DS-ApSs; zVtz82x|l>C={UH1;ZTqtVO`^s;*^ATLKP_%;=*-|!VZz2LHP{)NoQdDQ6Klrm|k_h zX39D0Raa%svFUQplC3OnYr17?nm_VE@bAw2)tSFL_gCjWK9=h|bITT%BV$VyzFEth zc`kiz=(_*y$c<;Rfn)Niv)PKrr@3D{$`(C!dC!KdXG7l8ob@y>t>NW$LT=5F>>XNk z*5sW{S!dImLvNjV<4pGCW4X37w@yBGbM!sm4d18R4?s?~?TkDe%QlPhSR&h;Sa6On zd3{;msSoOJKE2Qp%K1*^yr-tkiAtJy@oFUwnQ?SewI$ z-e>@Oohf82;X68uR)zKH18B!9SlO%{mZW7RLw?kvvEV4&!735XTo^=8p%)zjKx?)g zx&ee|jyp=euxo4|)&cKx(Gb?v{gB>@Ru6qm_Y(nw#|k3AF-Ts5mIw$(hP;9iW%YJy z45N&mj6p6ax=7yS5=0qiklqt4aJsq@E(iK6AB}-}TeylLWm<}L`H83`iLpJX78e-f zck27ys_p!z*coy|YL6qYVd~E?LiR`g9HZAULe@v#z-R^IvQoxH;#l+qMmmck{~2GO zf`~eCmvJ3++VplExdt_`s&pNq`!gPsccyZ7Xu-7p|CI&(AR9Qc*w}o1;|;@&_8S}J zwgUiyD-W{M+}B2=%Kr^ItrrR03EFV=PamH>E^j>~yAOZD^?%8^WcU6Bt_v)Sf0CX# z{K7LiM}ur?_}ZhBGt$8?n;Q>0j2}Ae2Wza$jO;q;{w<*8Wu(ilrcr8m3(%nPlu0sf zSMlI+P^xx32wiJOpPI1o_ya;$!)jhIYP~9DFon8W@2XZ?fj2G@iJD5x-n!i`Zq$(I zv6BTvYD}7vI!PtX4@UFALX1V<)M~c8)o{3=_b-?Wc)v=UX-H=oFzzH=m4FqWxKu*? z$UwM~x#(mZvI0A>?l4fTz#wp-6#yPywx(NPO?6fv=xCB>LN@3PuxQd=ED!4#KUk-Y zuK0x2w+nV$et6OwZi2H#w}NUc1@JGRT15KbYFU9jM_rh|GPl@2AYxqyD6R_3(NoIu zM!k}O2TYE;_8<&97|=275Zt{0XfZrVq&S%JSFp2JVM-aZ%E=JhW%>>j?lv!Tbr%eC zdw4)J^d4d1dRKx}*75WMm2E644KoG_xQ7#GA1298eD%uWIqX@E(0^7ug z3fREYuU;9K$oo+KF8oP6AOql>&hq(7`PS}iYxhEH&pn2%JjUK??U_6DFRp)b{fpP||W`6h2JFj-W-E}ke$+{!6=6gn_ss?T-ANg$Ek*^@lXIp#j(2v2n=o(}% z_kQ_@K;PP?xzML;w?21dx_8O9cD8S>cfS3-%IooW8*hp?AN{!ZW>myfFANyp$di`gz&EZ__*-tAU zhhg!XzwlM7d{?%GKU=$1W#0F^jM*+b*T1=AZt_3vncF6}^vU)^3#P-14(IRh`fv&U zyDdzC*Lql~pH%m9_P^W%(Xadu^}3B8Hg-b%QL_Q!zjxbv>#QgX^}s<2KoNU))ficq-G0Mxg-O|Wo<<^JVd5uTwFvcq`amK-HDoe!+( zDsrCtj7q;rd*9wXCB1pkE_$Dq%Kk6wl?xR@WuYA;g%v@QS4p@@0d^M@#4CbOMZIod zSufg5p(ZG~D?`Z*>B^O5VAEJb`86BVy+Xx08fb31-xz#!44~aX&(+26w3H@iq5zqp zhAum_a;9ioz#2?Y_b3%>2-U^iE8+dC&UE!rtll%^^2J`X6y&b94h2L(*8xdwMHs&S z=xd6jhqjwmd8DVX4u8quSoqlPp9)Aerh>7Y?SHSO1 zCZQEDMgI&Qidl8HqpW~x*5R>K94?t<>;c{Hr*N8W3M<6Kktq0?G8XXc!DX3DE&8XV zIK9F)2~vviPfW%}MZ9YU)^$^QYJ&Rx6B7hb7n9kH)^(8IV{!1A#rTVH_*qkWOyr9{ zvEkw8PB2L90IePP#A^dMBF*{%My0>S#%yroNOb^Y%D{0+Z6JCjkl!#UF_I>eA`IN3 zHcN$~;yw@@>hFW!rbJF07&4)ki+^Cn-`1U4jjbH9wqfnuI>{t24ZQOx4r zOv?`q(%j?4T>G_3x&0i?jTCb^bb0C>EPr!eYp2!QiWMB!nrK$5o&|T}uOFp(2Nu0+ z@?NkTt;=~gT<*VZ@S4jO9hF!1KevB2oO5hms;HZNEMMQ6t#5s={QcT@YUO=Lv)hk- zJe5Bl$sUj7D!wN>zo&ihXwK2JSW)-t{#WWC?G8eJ-XZMIHpG7OrL*GYGjCR3>v%6AI}YAu4CXRWr)Bri1@72q z9xwQg&1K(8>tNeIbRS?3*D)V?yN&Sphstg%JU*`5(r+{V6URcVaFGZelt=`AUp6@C zH}*kf7#vhh&6~*ILnDek5*eAqYs--c0d&TY4>8vy7k^jIQZfcq$R3FvVQK?aSZK)W!sKa6?LU_>n?6IhBa`3WdiZ$_bEi0~M- zW7LPy07fE435=+s@FJ#=Bddgm{Fsshc7_+&fx7L&6kJFc@4W=StpRb#(2GSX>gV>s zjRD}S`^l$J0K~&czlI2I*s|<@V^04ov;N;0&%ZK_pEC~n-}X5J|I0qd%<3=72QGU) zE3cYK<;pkCZ@zv=_V3G<@4M{zin{X;{|N4%*JrKuIcsCyx+!bjG|y(OTP~XxZ7$i% zXKnmt%c9M3rS|#Snd3QI(`CyY3(M}m!x`A!ckEua^3KLG_Aq;UGt<1~cZ{7q!QQEL zvIFd$20Pos-m9&E8rv-FS@vFe1-t3qHapvR4=T9sG}N(WxA!w9=X54#toh7d`Qp$^ Sr*if>*;EG?BTcn*{{IJ; item?.number) + .filter(number => Number.isSafeInteger(number) && number > 0); + if (numbers.length) return [...new Set(numbers)]; + const owner = run.head_repository?.owner?.login; + if (!owner || typeof run.head_branch !== 'string' || !run.head_branch) return []; + const pulls = await github.paginate(github.rest.pulls.list, + { ...repo, state: 'open', base: defaultBranch, head: `${owner}:${run.head_branch}`, per_page: 100 }); + return pulls.map(pull => pull.number); + } if (context.eventName === 'workflow_dispatch') { if (context.ref !== `refs/heads/${defaultBranch}`) throw new Error('Dispatch must use the default branch'); const number = Number(event.inputs?.pr_number); diff --git a/pr-governance/scripts/secret_scan.py b/pr-governance/scripts/secret_scan.py index 711625d..92bec15 100644 --- a/pr-governance/scripts/secret_scan.py +++ b/pr-governance/scripts/secret_scan.py @@ -69,10 +69,15 @@ def fail_walk(error): raise RuntimeError('Source ignore files are not permitted') -def git(target, *arguments): +def git(target, *arguments, capture=False): + # Patch output is only needed for its exit status, and historical blobs are + # not guaranteed to be UTF-8. Decoding them would abort the scan on any + # repository that ever committed a non-UTF-8 file. try: return subprocess.run(['git', '-C', str(target), *arguments], - capture_output=True, text=True, check=False) + stdout=subprocess.PIPE if capture else subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + text=True, encoding='utf-8', errors='replace', check=False) except OSError: raise RuntimeError('Git history is unreadable; scan blocked') from None @@ -81,7 +86,7 @@ def verify_history(target): # Gitleaks can report success after its internal Git reader fails. Read the # complete patch history first, suppressing every source-controlled diagnostic. try: - root = git(target, 'rev-parse', '--show-toplevel') + root = git(target, 'rev-parse', '--show-toplevel', capture=True) if root.returncode or Path(root.stdout.strip()).resolve() != Path(target).resolve(): raise RuntimeError('Git history is unreadable; scan blocked') except (OSError, ValueError): diff --git a/pr-governance/scripts/test-runner.cjs b/pr-governance/scripts/test-runner.cjs index 159cf26..2414e45 100644 --- a/pr-governance/scripts/test-runner.cjs +++ b/pr-governance/scripts/test-runner.cjs @@ -146,6 +146,25 @@ test('a comment on an issue that is not a pull request evaluates nothing', async assert.deepEqual(await runner.selectTargets(m.github, m.context, 'main'), [8]); }); +test('a review relay evaluates the pull requests named by the run', async () => { + const m = mock(fixture(), { eventName: 'workflow_run' }); + m.context.payload = { workflow_run: { pull_requests: [{ number: 42 }, { number: 7 }, { number: 42 }] } }; + assert.deepEqual(await runner.selectTargets(m.github, m.context, 'main'), [42, 7]); + m.context.payload = { workflow_run: { pull_requests: [{ number: 0 }, { number: 'x' }] } }; + assert.deepEqual(await runner.selectTargets(m.github, m.context, 'main'), []); +}); + +test('a review relay without pull request entries falls back to the head branch', async () => { + const m = mock(fixture(), { eventName: 'workflow_run', open: [{ number: 42 }] }); + m.context.payload = { workflow_run: { pull_requests: [], head_repository: { owner: { login: 'example-org' } }, head_branch: 'feature' } }; + assert.deepEqual(await runner.selectTargets(m.github, m.context, 'main'), [42]); + assert.equal(m.calls.at(-1).params.head, 'example-org:feature'); + m.context.payload = { workflow_run: { pull_requests: [], head_branch: 'feature' } }; + assert.deepEqual(await runner.selectTargets(m.github, m.context, 'main'), []); + m.context.payload = {}; + await assert.rejects(runner.selectTargets(m.github, m.context, 'main'), /workflow run payload/); +}); + test('dispatch validates the number and the default branch', async () => { const m = mock(fixture(), { eventName: 'workflow_dispatch' }); m.context.payload = { inputs: { pr_number: '42' } }; diff --git a/pr-governance/scripts/test_secret_scan.py b/pr-governance/scripts/test_secret_scan.py index d7a9db4..c537cf5 100644 --- a/pr-governance/scripts/test_secret_scan.py +++ b/pr-governance/scripts/test_secret_scan.py @@ -123,6 +123,13 @@ def test_range_scan_empty_range_passes(self): base = self.suppression_repository() self.assertEqual(self.range_scan(base), 0) + def test_range_scan_survives_non_utf8_history(self): + base = self.suppression_repository() + (self.source / 'legacy.txt').write_bytes(b'\xd2\xbb\xb6\xce\xce\xc4\xb1\xbe\n') + self.git('add', '.') + self.git('commit', '--quiet', '-m', 'test: legacy encoding') + self.assertEqual(self.range_scan(base), 0) + def test_range_scan_unknown_head_fails(self): base = self.suppression_repository() with self.assertRaises(RuntimeError): diff --git a/pr-governance/templates/codex-nudge.yml b/pr-governance/templates/codex-nudge.yml index 053ce61..352362a 100644 --- a/pr-governance/templates/codex-nudge.yml +++ b/pr-governance/templates/codex-nudge.yml @@ -25,6 +25,6 @@ jobs: timeout-minutes: 5 steps: - name: Request - uses: __GOVERNANCE_REPO__/pr-governance@v1 + uses: __GOVERNANCE_REPO__/pr-governance@__GOVERNANCE_SHA__ with: mode: nudge diff --git a/pr-governance/templates/gitleaks.yml b/pr-governance/templates/gitleaks.yml index b1fa5a7..8e1706b 100644 --- a/pr-governance/templates/gitleaks.yml +++ b/pr-governance/templates/gitleaks.yml @@ -24,6 +24,6 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Scan added commits - uses: __GOVERNANCE_REPO__/pr-governance@v1 + uses: __GOVERNANCE_REPO__/pr-governance@__GOVERNANCE_SHA__ with: mode: scan diff --git a/pr-governance/templates/pr-review-signal.yml b/pr-governance/templates/pr-review-signal.yml new file mode 100644 index 0000000..0d9fc3b --- /dev/null +++ b/pr-governance/templates/pr-review-signal.yml @@ -0,0 +1,25 @@ +name: PR review signal + +# Copy to .github/workflows/pr-review-signal.yml and replace nothing: this +# workflow holds no permissions and runs no repository code. It exists because a +# review event on a fork or Dependabot pull request gets a read-only token, so +# the gate that publishes statuses has to be started by `workflow_run` instead. +# Keep the workflow name; risk-gate.yml refers to it. + +on: + pull_request_review: + types: [submitted, edited, dismissed] + pull_request_review_comment: + types: [created, edited, deleted] + +permissions: + contents: read + +jobs: + signal: + name: Signal review change + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Signal + run: echo "review event recorded" diff --git a/pr-governance/templates/risk-gate.yml b/pr-governance/templates/risk-gate.yml index 3585f67..7874788 100644 --- a/pr-governance/templates/risk-gate.yml +++ b/pr-governance/templates/risk-gate.yml @@ -9,10 +9,12 @@ on: types: [opened, reopened, synchronize, edited, labeled, unlabeled, ready_for_review, closed] issue_comment: types: [created, edited, deleted] - pull_request_review: - types: [submitted, edited, dismissed] - pull_request_review_comment: - types: [created, edited, deleted] + # A review or inline comment on a fork or Dependabot pull request gets a + # read-only token, so those events are relayed through the permissionless + # `PR review signal` workflow and evaluated by this trusted run. + workflow_run: + workflows: ["PR review signal"] + types: [completed] push: branches: [main, master] # A merge made with GITHUB_TOKEN raises no push, and nothing raises an event @@ -49,7 +51,7 @@ jobs: statuses: write steps: - name: Record label head - uses: __GOVERNANCE_REPO__/pr-governance@v1 + uses: __GOVERNANCE_REPO__/pr-governance@__GOVERNANCE_SHA__ with: mode: gate @@ -74,10 +76,10 @@ jobs: # Job-level, so an irrelevant comment skips the job without cancelling an # evaluation already running. Label evidence no longer rides on this run. concurrency: - group: pr-governance-${{ github.event.pull_request.number || github.event.issue.number || inputs.pr_number || github.run_id }} + group: pr-governance-${{ github.event.pull_request.number || github.event.issue.number || github.event.workflow_run.pull_requests[0].number || inputs.pr_number || github.run_id }} cancel-in-progress: true steps: - name: Evaluate - uses: __GOVERNANCE_REPO__/pr-governance@v1 + uses: __GOVERNANCE_REPO__/pr-governance@__GOVERNANCE_SHA__ with: mode: gate diff --git a/pr-governance/templates/weekly-secret-scan.yml b/pr-governance/templates/weekly-secret-scan.yml index 8fcd03a..339de27 100644 --- a/pr-governance/templates/weekly-secret-scan.yml +++ b/pr-governance/templates/weekly-secret-scan.yml @@ -30,7 +30,7 @@ jobs: - name: Fetch all remote branches run: git fetch origin '+refs/heads/*:refs/remotes/origin/*' - name: Scan history - uses: __GOVERNANCE_REPO__/pr-governance@v1 + uses: __GOVERNANCE_REPO__/pr-governance@__GOVERNANCE_SHA__ env: SECRET_SCAN_BASELINE_B64: ${{ secrets.SECRET_SCAN_BASELINE_B64 }} with: