@@ -211,8 +211,50 @@ export function concurrencyVerdict(groupExpression, { workflow = 'W' } = {}) {
211211// ⛔ The marker is PLAIN TEXT, never an HTML comment. This platform's body
212212// sanitizer is measured to eat short angle-bracket fragments, and a de-dup key
213213// that can be swallowed files a duplicate an hour.
214- const MARKER_PREFIX = 'os-hourly-full-run' ;
215- const TITLE_PREFIX = 'hourly full run: red on main' ;
214+ //
215+ // Both prefixes carry the watched workflow's CADENCE (#23880): the card for a
216+ // nightly workflow must not call itself hourly. For an hourly workflow they
217+ // spell exactly what they always did -- `hourly full run: red on main` and
218+ // `os-hourly-full-run` -- so an open card filed before the cadence existed is
219+ // still found by its title and by its marker, and refreshed, never duplicated.
220+ const markerPrefix = ( cadence ) => `os-${ cadence } -full-run` ;
221+ const titlePrefix = ( cadence ) => `${ cadence } full run: red on main` ;
222+
223+ // THE CADENCE OF EACH WATCHED WORKFLOW, by its byte-exact `name:` -- the same
224+ // spelling `scheduled-full-run-card.yml` lists in `workflow_run.workflows`.
225+ // CLOSED on purpose: a workflow missing here is REFUSED by `cardIdentity`,
226+ // never defaulted, because a defaulted cadence prints the wrong one on the card
227+ // and the card is the only reader of that run. Adding a workflow to the card's
228+ // watched list therefore means adding its cadence here in the same change.
229+ export const CARD_CADENCES = Object . freeze ( {
230+ CI : 'hourly' ,
231+ 'Lint & Type Check' : 'hourly' ,
232+ 'Rerun Safety' : 'nightly' ,
233+ } ) ;
234+
235+ // The words each cadence puts in the body. The `hourly` entry is the text the
236+ // body has always carried, byte for byte.
237+ const CADENCE_WORDS = Object . freeze ( {
238+ hourly : Object . freeze ( {
239+ every : 'every hour' ,
240+ previous : 'an hour ago' ,
241+ about : ( name ) => [
242+ `\`${ name } \` runs the FULL battery on \`main\` every hour (#16467). A \`push\` run on` ,
243+ '`main` tests only the packages the merge touched, and a merge-queue build tests only what the group' ,
244+ 'contained — so this hourly run is the only thing that says whether the WHOLE tree is green, and' ,
245+ 'this card is the only channel that reports it. Nothing is blocked by it.' ,
246+ ] ,
247+ } ) ,
248+ nightly : Object . freeze ( {
249+ every : 'every night' ,
250+ previous : 'the night before' ,
251+ about : ( name ) => [
252+ `\`${ name } \` runs on \`main\` once a night, and no pull request, merge-queue build or other` ,
253+ 'scheduled run takes its measurement -- so this nightly run is the only thing that reports it, and' ,
254+ 'this card is the only channel that carries it. Nothing is blocked by it.' ,
255+ ] ,
256+ } ) ,
257+ } ) ;
216258
217259// ONE CARD PER WORKFLOW, not one card for both, and that is a decision rather
218260// than an oversight. `CI` and `Lint & Type Check` are two different batteries
@@ -229,10 +271,19 @@ export function cardIdentity(workflowName) {
229271 . replace ( / [ ^ a - z 0 - 9 ] + / g, '-' )
230272 . replace ( / ^ - + | - + $ / g, '' ) ;
231273 if ( slug === '' ) throw new Error ( `cardIdentity: '${ name } ' slugs to nothing, so it cannot be an identity` ) ;
274+ if ( ! Object . hasOwn ( CARD_CADENCES , name ) ) {
275+ throw new Error (
276+ `cardIdentity: '${ name } ' has no cadence in CARD_CADENCES (${ Object . keys ( CARD_CADENCES ) . join ( ', ' ) } ). ` +
277+ 'Add it there in the same change that makes scheduled-full-run-card.yml watch it -- a defaulted ' +
278+ 'cadence would print the wrong one on the only card that reports this run.'
279+ ) ;
280+ }
281+ const cadence = CARD_CADENCES [ name ] ;
232282 return {
233283 workflowName : name ,
234- title : `${ TITLE_PREFIX } (${ name } )` ,
235- marker : `${ MARKER_PREFIX } -${ slug } ` ,
284+ cadence,
285+ title : `${ titlePrefix ( cadence ) } (${ name } )` ,
286+ marker : `${ markerPrefix ( cadence ) } -${ slug } ` ,
236287 labels : [ 'bug' , 'domain:devx' , 'priority:p1' ] ,
237288 } ;
238289}
@@ -298,8 +349,9 @@ export async function fileOrRefreshCard({ identity, body, listPage, createIssue,
298349}
299350
300351// The card body. The run link and the commit range are the whole product: a red
301- // hourly run says "main broke somewhere in the last hour", and the commits
302- // between the previous GREEN scheduled run and this one are that hour.
352+ // scheduled run says "main broke somewhere since the last green one" -- the last
353+ // hour for an hourly workflow, the last night for a nightly one -- and the
354+ // commits between the previous GREEN scheduled run and this one are that span.
303355export function renderBody ( {
304356 identity,
305357 runUrl,
@@ -316,6 +368,13 @@ export function renderBody({
316368 // opposite directions.
317369 rangeNote = null ,
318370} ) {
371+ // The cadence's words. `cardIdentity` refuses a workflow with no cadence; an
372+ // identity built by hand without one is refused here for the same reason.
373+ const words = CADENCE_WORDS [ identity ?. cadence ] ;
374+ if ( ! words ) {
375+ throw new Error ( `renderBody: the identity carries no known cadence ('${ identity ?. cadence } ') -- build it with cardIdentity` ) ;
376+ }
377+ const heading = `## What landed since the last green ${ identity . cadence } run` ;
319378 const range = previousGreen
320379 ? `\`${ String ( previousGreen . head_sha ) . slice ( 0 , 10 ) } \` (run ${ previousGreen . run_id } , the last green ` +
321380 `\`${ identity . workflowName } \` schedule run) .. \`${ String ( headSha ) . slice ( 0 , 10 ) } \``
@@ -324,7 +383,7 @@ export function renderBody({
324383 const suspects = previousGreen
325384 ? commits . length > 0
326385 ? [
327- `## What landed since the last green hourly run` ,
386+ heading ,
328387 '' ,
329388 `Range: ${ range } ${ compareUrl ? ` — [compare](${ compareUrl } )` : '' } ` ,
330389 commitsTruncated
@@ -335,17 +394,17 @@ export function renderBody({
335394 '' ,
336395 ]
337396 : [
338- `## What landed since the last green hourly run` ,
397+ heading ,
339398 '' ,
340399 `Range: ${ range } ` ,
341400 '' ,
342- ' ⚠️ Nothing landed in that range. The same tree was green an hour ago and is red now, so' ,
401+ ` ⚠️ Nothing landed in that range. The same tree was green ${ words . previous } and is red now, so` ,
343402 'this is a FLAKE, an infrastructure fault, or a suite that depends on wall-clock time or on' ,
344403 'something outside the repository. ⛔ Do not go looking for the commit that broke it.' ,
345404 '' ,
346405 ]
347406 : [
348- `## What landed since the last green hourly run` ,
407+ heading ,
349408 '' ,
350409 '⚠️ No commit range could be named for this red run.' ,
351410 rangeNote ?? '' ,
@@ -356,19 +415,16 @@ export function renderBody({
356415
357416 return [
358417 `${ identity . marker } — machine-findable marker for this generated card. ⛔ Do not delete this line: ` +
359- ' it is how the hourly full run finds this card instead of filing a new one every hour.' ,
418+ ` it is how the ${ identity . cadence } full run finds this card instead of filing a new one ${ words . every } .` ,
360419 '' ,
361420 `# ${ identity . title } ` ,
362421 '' ,
363422 `_Swept ${ sweptAt } · [run log](${ runUrl } ) · commit \`${ headSha } \` · conclusion \`${ conclusion } \`._` ,
364423 '' ,
365- `\`${ identity . workflowName } \` runs the FULL battery on \`main\` every hour (#16467). A \`push\` run on` ,
366- '`main` tests only the packages the merge touched, and a merge-queue build tests only what the group' ,
367- 'contained — so this hourly run is the only thing that says whether the WHOLE tree is green, and' ,
368- 'this card is the only channel that reports it. Nothing is blocked by it.' ,
424+ ...words . about ( identity . workflowName ) ,
369425 '' ,
370- ' ⛔ The remedy is never to narrow, skip or delete the failing check to make the hourly run green.' ,
371- ' Fix what it names and let the next hourly run refresh this card; a card nobody reopens is closed by' ,
426+ ` ⛔ The remedy is never to narrow, skip or delete the failing check to make the ${ identity . cadence } run green.` ,
427+ ` Fix what it names and let the next ${ identity . cadence } run refresh this card; a card nobody reopens is closed by` ,
372428 'the next green run being uneventful, not by editing this one.' ,
373429 '' ,
374430 ...suspects ,
@@ -632,7 +688,7 @@ async function releaseGate(argv) {
632688// remedy is to find what stopped registering, never to lower the number.
633689const SELF_TEST_BATTERIES = Object . freeze ( {
634690 'scheduled-full-run concurrency isolation' : 20 ,
635- 'scheduled-full-run card identity and de-dup' : 23 ,
691+ 'scheduled-full-run card identity and de-dup' : 34 ,
636692 'release gate: the selection rule on the recorded 2026-10-12 listing' : 8 ,
637693 'release gate: the selection rule on synthetic runs' : 17 ,
638694 'release gate: release.yml wiring' : 6 ,
@@ -899,6 +955,94 @@ async function selfTest() {
899955 if ( found ?. number !== 42 ) throw new Error ( 'de-dup: a multi-page scan lost the card' ) ;
900956 } ) ;
901957
958+ // -- The cadence (#23880): each cadence, the back-compat pins for the card
959+ // already open under the hourly identity, and the refusal of a workflow
960+ // the closed map does not name.
961+ const rerun = cardIdentity ( 'Rerun Safety' ) ;
962+ const OPEN_CI_TITLE = 'hourly full run: red on main (CI)' ;
963+ const OPEN_CI_MARKER = 'os-hourly-full-run-ci' ;
964+ const OPEN_CI_MARKER_LINE =
965+ 'os-hourly-full-run-ci — machine-findable marker for this generated card. ⛔ Do not delete this line: ' +
966+ 'it is how the hourly full run finds this card instead of filing a new one every hour.' ;
967+ const bodiesOf = ( identityUnderTest ) => [
968+ renderBody ( { identity : identityUnderTest , runUrl : 'u' , headSha : 'a' , conclusion : 'failure' , sweptAt : 't' ,
969+ previousGreen : { run_id : 4 , head_sha : 'b' } , commits : [ { sha : 'c' , title : 'x' } ] } ) ,
970+ renderBody ( { identity : identityUnderTest , runUrl : 'u' , headSha : 'a' , conclusion : 'failure' , sweptAt : 't' ,
971+ previousGreen : { run_id : 4 , head_sha : 'b' } , commits : [ ] } ) ,
972+ renderBody ( { identity : identityUnderTest , runUrl : 'u' , headSha : 'a' , conclusion : 'failure' , sweptAt : 't' , rangeNote : 'n' } ) ,
973+ ] ;
974+ await check ( ( ) => {
975+ if ( ci . cadence !== 'hourly' || ci . title !== OPEN_CI_TITLE || ci . marker !== OPEN_CI_MARKER ) {
976+ throw new Error ( `cadence: the CI identity moved (${ ci . cadence } , '${ ci . title } ', '${ ci . marker } ') -- the open card would be duplicated` ) ;
977+ }
978+ } ) ;
979+ await check ( ( ) => {
980+ for ( const b of bodiesOf ( ci ) ) {
981+ if ( b . split ( '\n' ) [ 0 ] !== OPEN_CI_MARKER_LINE ) throw new Error ( `cadence: the CI marker line moved: '${ b . split ( '\n' ) [ 0 ] } '` ) ;
982+ }
983+ } ) ;
984+ await check ( async ( ) => {
985+ // The open CI card exactly as it was filed before the cadence existed.
986+ const open = { number : 23885 , title : OPEN_CI_TITLE , body : `${ OPEN_CI_MARKER_LINE } \n# ${ OPEN_CI_TITLE } ` } ;
987+ const found = await findExistingCard ( { identity : cardIdentity ( 'CI' ) , listPage : page ( [ issue ( 7 ) , open ] ) } ) ;
988+ if ( found ?. number !== 23885 ) throw new Error ( `cadence: the open CI card was not found by its title (${ found ?. number } )` ) ;
989+ } ) ;
990+ await check ( async ( ) => {
991+ const renamed = { number : 23885 , title : 'renamed by a human' , body : `${ OPEN_CI_MARKER_LINE } \n` } ;
992+ const found = await findExistingCard ( { identity : cardIdentity ( 'CI' ) , listPage : page ( [ renamed ] ) } ) ;
993+ if ( found ?. number !== 23885 ) throw new Error ( 'cadence: the open CI card was not found by its marker alone' ) ;
994+ } ) ;
995+ await check ( ( ) => {
996+ if ( lint . cadence !== 'hourly' || lint . title !== 'hourly full run: red on main (Lint & Type Check)' ) {
997+ throw new Error ( `cadence: Lint & Type Check is not hourly ('${ lint . title } ')` ) ;
998+ }
999+ } ) ;
1000+ await check ( ( ) => {
1001+ if (
1002+ rerun . cadence !== 'nightly' ||
1003+ rerun . title !== 'nightly full run: red on main (Rerun Safety)' ||
1004+ rerun . marker !== 'os-nightly-full-run-rerun-safety'
1005+ ) {
1006+ throw new Error ( `cadence: Rerun Safety is not nightly ('${ rerun . title } ', '${ rerun . marker } ')` ) ;
1007+ }
1008+ } ) ;
1009+ await check ( ( ) => {
1010+ for ( const b of bodiesOf ( rerun ) ) {
1011+ if ( / h o u r l y | e v e r y h o u r | a n h o u r a g o / . test ( b ) ) throw new Error ( `cadence: a nightly card body says hourly:\n${ b } ` ) ;
1012+ if ( ! b . includes ( 'every night' ) || ! b . includes ( '## What landed since the last green nightly run' ) ) {
1013+ throw new Error ( `cadence: a nightly card body lacks its own cadence words:\n${ b } ` ) ;
1014+ }
1015+ }
1016+ if ( ! bodiesOf ( rerun ) [ 1 ] . includes ( 'green the night before' ) ) throw new Error ( 'cadence: the nightly flake reading is not nightly' ) ;
1017+ } ) ;
1018+ await check ( ( ) => {
1019+ for ( const b of bodiesOf ( ci ) ) {
1020+ if ( / n i g h t l y | e v e r y n i g h t | t h e n i g h t b e f o r e / . test ( b ) ) throw new Error ( `cadence: an hourly card body says nightly:\n${ b } ` ) ;
1021+ }
1022+ } ) ;
1023+ await check ( ( ) => {
1024+ let message = '' ;
1025+ try {
1026+ cardIdentity ( 'Spec Coverage' ) ;
1027+ } catch ( error ) {
1028+ message = error . message ;
1029+ }
1030+ if ( ! message . includes ( 'CARD_CADENCES' ) ) {
1031+ throw new Error ( `cadence: a workflow outside the map was not refused by name (${ message || 'accepted' } )` ) ;
1032+ }
1033+ } ) ;
1034+ await check ( ( ) => {
1035+ const { cadence : _dropped , ...handBuilt } = ci ;
1036+ if ( ! threw ( ( ) => renderBody ( { identity : handBuilt , runUrl : 'u' , headSha : 'a' , conclusion : 'failure' , sweptAt : 't' } ) ) ) {
1037+ throw new Error ( 'cadence: renderBody defaulted the cadence of an identity that carries none' ) ;
1038+ }
1039+ } ) ;
1040+ await check ( ( ) => {
1041+ if ( matchesCard ( issue ( 1 , { title : ci . title , body : ci . marker } ) , rerun ) || matchesCard ( issue ( 1 , { title : rerun . title , body : rerun . marker } ) , ci ) ) {
1042+ throw new Error ( "cadence: the nightly and hourly identities matched each other's cards" ) ;
1043+ }
1044+ } ) ;
1045+
9021046 // -- THE ACCEPTANCE SHAPE, driven end to end against a mutable board: one red
9031047 // run files exactly one card, and the NEXT red run refreshes that card and
9041048 // files no second one. This is the property the filer exists for, and a
0 commit comments