From 61ed687e7fb1b3831e9c8944ed5c729a20244352 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 09:28:34 +0000 Subject: [PATCH] ci: retire the stale-issues workflow under enforce-or-remove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight months, 236 scheduled runs, zero successes, zero consumers and zero external authors on the open board. Every run failed in `Set up job` before `actions/stale` started, because the workflow's SHA pin resolved to no commit, so the job never once swept anything. A declared automation nobody uses is removed, not repaired (director seat, decision batch 89). The workflow's three declared references go with it, and the exception entry goes in THIS commit rather than a follow-up: - `scripts/check-action-ref-convention.mjs` — the `DECLARED_EXCEPTIONS` entry naming the workflow and `actions/stale`. That gate's rule 2 fails when an entry matches nothing, and it runs on every pull request, so deleting the workflow and leaving the entry would have reddened `main` for everyone. Verified counterfactually: scanning this tree with the entry still present reports one stale exception. - `content/docs/guide/ci-cd-pipeline.md` — the Workflow Inventory row, the section, and the prose naming the file. The page is pinned to `.github/workflows/` in both directions, so all three had to move. - `scripts/__tests__/workflow-cache-save-bound.test.ts` — the unbounded-job entry. Its lookup asserts the job still exists. Two suites read the real workflow off disk and are repaired, not deleted: the behaviour they pin still needs pinning. `check-action-ref-convention.test.ts` reproduces both the stale-exception verdict and the "silences only the entry that names it" case over synthetic tables and other workflows, which no longer depends on one particular file surviving. Part of objectui#8465, whose one SHA pin this disposes of. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr --- .github/workflows/stale.yml | 118 ------------------ content/docs/guide/ci-cd-pipeline.md | 30 ++--- .../check-action-ref-convention.test.ts | 74 ++++++++--- scripts/__tests__/ci-cd-pipeline-doc.test.ts | 2 +- .../workflow-cache-save-bound.test.ts | 46 ++++--- scripts/check-action-ref-convention.mjs | 62 +++++---- 6 files changed, 136 insertions(+), 196 deletions(-) delete mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 5504e5c6f0..0000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: Stale Issues & PRs - -on: - schedule: - # Run daily at 00:00 UTC - - cron: '0 0 * * *' - workflow_dispatch: - -permissions: - issues: write - pull-requests: write - -jobs: - stale: - runs-on: ubuntu-latest - - # ── NO job ceiling, and that is the DECISION, not its absence (objectui#7956) ── - # This job runs under GitHub's 360-minute default. objectui#7956 asked for a - # ceiling DERIVED from this job's own measured run-time distribution, in the - # shape objectui#7270 left beside `lint.yml::lint`. It is recorded here - # instead, because every run this job has to offer measures the wrong thing. - # - # - Population: every completed run of this workflow, no status filter, - # window 2026-01-16T01:14Z .. 2026-09-06T00:19Z, n=234, all `schedule`. - # Measured 2026-09-06T17:24Z. - # - `?status=success` returns a `total_count` of 0 over that same window. - # This job has never once succeeded in the whole retained history. - # - Wall clock, from the Actions jobs endpoint: min 1s / median 2s / - # p95 4s / max 4s. - # - ⚠️ Those seconds are NOT this job's work. Ten runs sampled evenly - # across the window all report the same shape: a single step, `Set up - # job`, with conclusion `failure` — the `actions/stale` step below never - # starts. So the distribution above measures how fast this job fails to - # begin, and says nothing whatever about how long it takes to sweep this - # repository's issues and pull requests. - # - # ⛔ Do NOT derive 20-ish minutes from a 4-second maximum that belongs to a - # setup failure. That would be a ceiling with the look of provenance and - # none of the substance, sitting over a job whose real cost has never been - # observed — and `actions/stale` pages the issue and pull-request lists, - # which is the kind of work that grows with the repository. A ceiling set - # under a job's honest slowest run converts a working job into a permanently - # red one (objectui#7048); objectstack#16173 is the live counter-example, - # where a distribution mis-estimated by ~2.6x killed a test shard while the - # rollup read green. Here the mis-estimate would not be 2.6x, it would be - # unbounded, because the numerator has never been measured at all. - # - # ⚠️ Two separate things are true and only one of them is objectui#7956's: - # the exposure (no ceiling) is that card's, and the FAILURE (234 runs, 234 - # failures, never a success) is objectui#8126's — filed separately, because - # bounding a job that cannot start would fix nothing and would hide the - # louder defect behind a tidy-looking diff. - # - # What would change this: fix the setup failure, let a few real sweeps run, - # then derive the number from THOSE the way `lint.yml::lint` documents, - # write the derivation here, and move this job out of the accept-360 table - # and into the `derived` table in - # `scripts/__tests__/workflow-cache-save-bound.test.ts` in the same commit — - # that pin holds this job's UNBOUNDED state deliberately, so adding a - # ceiling has to be an edit someone makes on purpose rather than a silent - # one. - steps: - - uses: actions/stale@e00e804f6792d3fedb5bd3a27df2761c5f86c981 # v9.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - # Issues configuration - stale-issue-message: | - This issue has been automatically marked as stale because it has not had recent activity. - It will be closed in 7 days if no further activity occurs. - - If you believe this issue is still relevant, please: - - Comment on this issue to keep it open - - Add more details or context - - Update to the latest version and confirm the issue still exists - - Thank you for your contributions! - close-issue-message: | - This issue has been automatically closed due to inactivity. - - If you believe this issue should remain open, please: - - Reopen the issue with updated information - - Create a new issue with a detailed description and reproduction steps - - Thank you for your understanding! - stale-issue-label: 'stale' - exempt-issue-labels: 'pinned,security,critical,bug,enhancement' - days-before-issue-stale: 60 - days-before-issue-close: 7 - - # Pull requests configuration - stale-pr-message: | - This pull request has been automatically marked as stale because it has not had recent activity. - It will be closed in 14 days if no further activity occurs. - - If you're still working on this PR: - - Comment on this PR to keep it open - - Rebase on the latest main branch - - Address any review comments - - Thank you for your contributions! - close-pr-message: | - This pull request has been automatically closed due to inactivity. - - If you'd like to continue working on this: - - Reopen the PR and rebase on the latest main branch - - Create a new PR with updated changes - - Thank you for your understanding! - stale-pr-label: 'stale' - exempt-pr-labels: 'pinned,security,in-progress,blocked' - days-before-pr-stale: 45 - days-before-pr-close: 14 - - # General configuration - operations-per-run: 100 - remove-stale-when-updated: true - ascending: true diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index 78f3bdd8fa..514f090eba 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -50,7 +50,6 @@ one has its own section below. | `cross-repo-issue-closer.yml` | Cross-repo Issue Closer | PR `closed` (acts only when merged) | No — runs after merge | | `changeset-release.yml` | Changeset Release | Push to `main` (publish half); 6-hourly cron `0 */6 * * *`; manual (version-PR refresh half) | n/a | | `changelog.yml` | Auto Changelog | Manual dispatch only — nothing triggers it automatically | n/a | -| `stale.yml` | Stale Issues & PRs | Daily cron `0 0 * * *`; manual | n/a | | `shadcn-check.yml` | Check Shadcn Components | Weekly cron `0 9 * * 1`; manual | n/a | | `check-links.yml` | Check Links | Weekly cron `17 4 * * 0`; manual | n/a — reports, never gates | | `published-dist-gate.yml` | Published Dist Tooling Scan | Nightly cron `41 3 * * *`; push to `main` touching the gate; manual | No — the blocking copy runs on the publish path, not here | @@ -411,9 +410,17 @@ not a reason, and the gate does not read it as one. [objectui#8465](https://github.com/objectstack-ai/objectui/issues/8465). There were 13 distinct action references in this directory. Exactly **one** was spelled differently from the other twelve — a commit SHA on `actions/stale` — and it was the only reference in the repository that had **never -resolved**: 236 scheduled runs of `stale.yml` since 2026-01-16, **0 successes**, every one failing -in `Set up job`, unnoticed for eight months because nothing downstream consumes that job. The broken -reference itself is [objectui#8126](https://github.com/objectstack-ai/objectui/issues/8126). +resolved**: 236 scheduled runs of the stale-issues workflow since 2026-01-16, **0 successes**, every +one failing in `Set up job`, unnoticed for eight months because nothing downstream consumes that +job. The broken reference itself was +[objectui#8126](https://github.com/objectstack-ai/objectui/issues/8126). + +That workflow no longer exists. [objectui#8548](https://github.com/objectstack-ai/objectui/issues/8548) +retired it under enforce-or-remove — a declared automation with zero successes, zero consumers and no +external authors on the open board is removed rather than repaired — and #8126 closed with it. The +`DECLARED_EXCEPTIONS` entry that had covered its SHA pin was deleted in the same commit: an entry +matching nothing is red under the second rule below, so a deletion that left it behind would have +reddened this gate on `main` for every pull request. This is **not** an argument that SHA pinning is wrong — it is normally the *more* secure spelling and supply-chain guidance recommends it. The failure was the *shape*: one ref written in a form nothing @@ -2189,21 +2196,6 @@ secrets from fork-originated runs. The usual hazard of `pull_request_target` doe the job never checks out the head ref and never executes anything from the PR — it reads the body and calls the issues API. -### Stale Issues (`stale.yml`) - -**Trigger:** Daily at 00:00 UTC (cron), or manual dispatch. - -| Resource | Stale after | Close after | Exempt labels | -|----------|-------------|-------------|---------------| -| Issues | 60 days | 7 days | `pinned`, `security`, `critical`, `bug`, `enhancement` | -| Pull Requests | 45 days | 14 days | `pinned`, `security`, `in-progress`, `blocked` | - -The two exemption lists are set separately (`exempt-issue-labels` and `exempt-pr-labels`) and -neither is a subset of the other: `critical`, `bug` and `enhancement` exempt issues only, -`in-progress` and `blocked` exempt pull requests only. This page used to state one merged list -— `pinned`, `security`, `critical`, `in-progress` — which was wrong in both directions for -both resources ([#3724](https://github.com/objectstack-ai/objectui/issues/3724)). - ### Half-State Patrol (`half-state-patrol.yml`) **Trigger:** Four times a day at `:37` past the hour (cron `37 1,7,13,19 * * *`), manual dispatch, diff --git a/scripts/__tests__/check-action-ref-convention.test.ts b/scripts/__tests__/check-action-ref-convention.test.ts index fa52b0f626..3e748d929c 100644 --- a/scripts/__tests__/check-action-ref-convention.test.ts +++ b/scripts/__tests__/check-action-ref-convention.test.ts @@ -154,16 +154,47 @@ describe('the gate can fail (non-vacuity)', () => { }); it('goes red on a stale exception — an entry that matches nothing', () => { - const result = scanMutated( - (files) => { - // The stale.yml SHA becomes a tag: exactly what objectui#8126 landing - // one way would do. The entry must then be deleted, loudly. - const name = 'stale.yml'; - files.set(name, files.get(name)!.replace(/actions\/stale@[0-9a-f]{40}/, 'actions/stale@v9')); + // This case used to mutate the real `stale.yml`, whose SHA pin was the only + // entry DECLARED_EXCEPTIONS ever held. objectui#8548 deleted that workflow + // and the entry with it, so the table is empty and the shape has to be + // reproduced over a synthetic one — which is stricter, not weaker: the + // assertion no longer depends on one particular workflow surviving. + // + // Both halves are pinned, because only the pair says the entry is doing + // work. An entry that never silences anything would satisfy the second half + // on its own. + const declared = [ + { + workflow: 'control-bytes.yml', + action: 'actions/checkout', + issue: 'objectui#8465', + reason: 'synthetic entry for the non-vacuity proof below — not a real exception', }, - ); - expect(result.offenders).toEqual([]); - expect(result.stale.map((e: { workflow: string }) => e.workflow)).toEqual(['stale.yml']); + ]; + const shaPin = (files: Map) => { + const name = 'control-bytes.yml'; + files.set( + name, + files.get(name)!.replace('uses: actions/checkout@v7', 'uses: actions/checkout@' + 'a'.repeat(40)), + ); + }; + + // Matching: the ref is off-convention and the entry names it, so nothing is + // reported from either direction. + const matching = scanMutated(shaPin, declared); + expect(matching.offenders).toEqual([]); + expect(matching.stale).toEqual([]); + + // Orphaned: the workflow the entry names is deleted — literally what + // objectui#8548 did to the real table. The entry now matches nothing and + // must be reported, loudly, so that deleting a workflow cannot leave a + // permanently red `main` behind it. + const orphaned = scanMutated((files) => { + shaPin(files); + files.delete('control-bytes.yml'); + }, declared); + expect(orphaned.offenders).toEqual([]); + expect(orphaned.stale.map((e: { workflow: string }) => e.workflow)).toEqual(['control-bytes.yml']); }); it('goes red when the census collapses, instead of reporting a clean tree', () => { @@ -180,18 +211,31 @@ describe('the gate can fail (non-vacuity)', () => { const fake = [ { workflow: 'control-bytes.yml', action: 'actions/checkout', issue: 'objectui#1', reason: 'x'.repeat(50) }, ]; + // Two off-convention refs, in two different workflows, and `fake` names only + // the first. The second is the whole point of the case: until objectui#8548 + // it was `stale.yml`'s real SHA pin, which the tree no longer carries, so it + // is injected here instead. ⛔ Do not drop it and keep only the silenced + // half — a gate that reported nothing at all would pass that alone. const result = scanMutated((files) => { - const name = 'control-bytes.yml'; - files.set(name, files.get(name)!.replace('uses: actions/checkout@v7', 'uses: actions/checkout@main')); + files.set( + 'control-bytes.yml', + files.get('control-bytes.yml')!.replace('uses: actions/checkout@v7', 'uses: actions/checkout@main'), + ); + files.set( + 'shadcn-check.yml', + files + .get('shadcn-check.yml')! + .replace('uses: actions/checkout@v7', 'uses: actions/checkout@' + 'd'.repeat(40)), + ); }, fake); // The ref `fake` covers is silenced... expect(result.offenders.map((o: { ref: string }) => o.ref)).not.toContain('actions/checkout@main'); expect(result.stale).toEqual([]); - // ...and ONLY that one: `fake` replaced the real table, so stale.yml's SHA - // is now undeclared and reported. Proof that what silences a reference is an - // entry naming it, not the gate being lax about off-convention spellings. + // ...and ONLY that one: the second workflow's SHA is undeclared and is + // reported. Proof that what silences a reference is an entry naming it, not + // the gate being lax about off-convention spellings. expect(result.offenders.map((o: { file: string; kind: string }) => `${o.file} ${o.kind}`)).toEqual([ - 'stale.yml sha', + 'shadcn-check.yml sha', ]); }); }); diff --git a/scripts/__tests__/ci-cd-pipeline-doc.test.ts b/scripts/__tests__/ci-cd-pipeline-doc.test.ts index a6f3fd6a01..45a7d82d23 100644 --- a/scripts/__tests__/ci-cd-pipeline-doc.test.ts +++ b/scripts/__tests__/ci-cd-pipeline-doc.test.ts @@ -145,7 +145,7 @@ describe('ci-cd-pipeline.md — workflow inventory', () => { `content/docs/guide/ci-cd-pipeline.md names them:\n` + undocumented.map((f) => ` - ${f}`).join('\n') + `\n\nAdd a section to that page — a heading that contains the file name ` + - `(e.g. "### Stale Issues (\`stale.yml\`)"), what triggers it, and whether it can ` + + `(e.g. "### Hook Self-Tests (\`hook-selftests.yml\`)"), what triggers it, and whether it can ` + `block a merge — and a row in the "Workflow Inventory" table. A workflow nobody ` + `documented is a check contributors get blocked by without knowing it exists ` + `(objectui#3212: \`lint.yml\` gated PRs for months while this page never mentioned it).` + diff --git a/scripts/__tests__/workflow-cache-save-bound.test.ts b/scripts/__tests__/workflow-cache-save-bound.test.ts index 0457986973..b7c7e42cff 100644 --- a/scripts/__tests__/workflow-cache-save-bound.test.ts +++ b/scripts/__tests__/workflow-cache-save-bound.test.ts @@ -62,13 +62,21 @@ import { parse as parseYaml } from 'yaml'; * - Four had a distribution to derive from, and carry a ceiling with the * derivation written beside the key. * - Two did not, and carry NO ceiling ON PURPOSE, with the reason written - * beside the job. `changelog.yml::changelog` has never run at all; - * `stale.yml::stale` has never succeeded, and every run it has fails in - * `Set up job` before the marketplace action starts, so its seconds measure - * a setup failure rather than the job's work. Pinning an ABSENCE reads - * oddly until you notice the failure mode it catches: someone tidying up - * the inconsistency by copying `20` onto them, which is precisely the - * inherited number objectui#7048 fences and objectui#7956's triage forbade. + * beside the job. Pinning an ABSENCE reads oddly until you notice the + * failure mode it catches: someone tidying up the inconsistency by copying + * `20` onto them, which is precisely the inherited number objectui#7048 + * fences and objectui#7956's triage forbade. + * + * ⚠️ That second half is now ONE job, not two. `stale.yml::stale` was the other + * one, and objectui#8548 deleted the whole workflow under enforce-or-remove: + * eight months, 236 runs, zero successes, zero consumers. Its entry left the + * `accepted` table in that same commit, because a pin naming a job that no + * longer exists fails on the lookup and says nothing about ceilings. + * ⛔ Do not read the shrink as the rule weakening — `changelog.yml::changelog` + * still holds it, and the case it catches (copying `20` onto an unmeasured job) + * is unchanged. ⭐ A job leaving this table by being DELETED and a job leaving + * it by acquiring a derived ceiling are opposite events; only the second one + * moves an entry into the `derived` table above. * * ## Deliberately NOT asserted * @@ -438,22 +446,26 @@ describe('every workflow cache save is bounded and non-fatal (objectui#7048)', ( ).toEqual([]); }); - it('keeps the two jobs objectui#7956 could not measure UNBOUNDED, with their reason recorded', () => { + it('keeps the job objectui#7956 could not measure UNBOUNDED, with its reason recorded', () => { // The other half of objectui#7956, and the half that is easy to undo by - // being helpful. Four of its six jobs got a ceiling; these two did not, - // because neither has a distribution that measures the job doing its work: + // being helpful. Four of its six jobs got a ceiling; this one did not, + // because it has no distribution that measures the job doing its work: // // - `changelog.yml::changelog` — `total_count: 0` runs, ever. It is // dispatch-only and has never been dispatched. (Control on the same // endpoint: `changeset-release.yml` answers with thousands, so the zero // is a reading and not a broken query.) - // - `stale.yml::stale` — 234 completed runs, 0 successful, over eight - // months. Ten sampled evenly across that window all fail in `Set up - // job`, before `actions/stale` starts, so their 1-4 seconds measure how - // fast the job fails to begin. Its real cost has never been observed, - // and it is the kind that grows with the repository. // - // A number invented for either one would look derived and be a guess, and a + // objectui#7956 left TWO jobs here. The second was `stale.yml::stale` — 234 + // completed runs, 0 successful, over eight months, every sampled one failing + // in `Set up job` before the marketplace action started, so its 1-4 seconds + // measured how fast the job failed to begin rather than any work it did. + // objectui#8548 deleted that workflow outright (enforce-or-remove: zero + // consumers, zero successes), which is why this table lists one key and not + // two. ⛔ Re-adding `stale.yml::stale` here does not restore a pin — the + // lookup below would fail on a workflow that is gone. + // + // A number invented for it would look derived and be a guess, and a // ceiling under a job's honest slowest run converts a working job into a // permanently red one — objectui#7048's fence, and objectstack#16173 is the // live counter-example (a distribution mis-estimated by ~2.6x killed a test @@ -464,7 +476,7 @@ describe('every workflow cache save is bounded and non-fatal (objectui#7048)', ( // jobs above onto these two. Adding a real ceiling here is welcome — derive // it from runs that did the work, write the derivation beside the key, and // move the entry up into the `derived` table above in the same commit. - const accepted = ['changelog.yml::changelog', 'stale.yml::stale']; + const accepted = ['changelog.yml::changelog']; const bounded: string[] = []; const undocumented: string[] = []; diff --git a/scripts/check-action-ref-convention.mjs b/scripts/check-action-ref-convention.mjs index 50f860d6ce..82f677e629 100644 --- a/scripts/check-action-ref-convention.mjs +++ b/scripts/check-action-ref-convention.mjs @@ -23,12 +23,20 @@ * ## WHY THE EXCEPTION MECHANISM IS THE POINT, NOT THE DEFAULT * * objectui#8465. On `origin/main` there were 13 distinct action references. - * Exactly ONE was spelled differently from the other twelve — a commit SHA, - * `actions/stale@e00e804f...` — and it was the only reference in the repository - * that had never resolved. 236 scheduled runs of `stale.yml` since 2026-01-16, - * 0 successes, every one of them failing in `Set up job`. Eight months, silent, - * because nothing downstream consumes that job (the broken ref itself is - * objectui#8126). + * Exactly ONE was spelled differently from the other twelve — a commit SHA on + * `actions/stale` — and it was the only reference in the repository that had + * never resolved. 236 scheduled runs of the stale-issues workflow since + * 2026-01-16, 0 successes, every one of them failing in `Set up job`. Eight + * months, silent, because nothing downstream consumes that job (the broken ref + * itself was objectui#8126). + * + * That workflow is GONE — objectui#8548 retired it under enforce-or-remove + * rather than repairing it (a declared automation with zero consumers and zero + * successes), and objectui#8126 closed with it. Its entry left this table in + * the same commit, because rule 2 below would otherwise have turned the + * deletion into a red `main`. ⛔ The convention did NOT leave with it: it was + * never about that one reference, and the next off-convention ref is what it + * exists to make loud. * * ⛔ The finding is NOT "SHA pinning is wrong". It is normally the MORE secure * spelling and supply-chain guidance recommends it; whether this repository @@ -98,39 +106,41 @@ export const DEFAULT_SPELLING_LABEL = 'a floating major tag (@vN)'; /** * `workflow file + action path -> why this ref is spelled differently`. + * **Deliberately empty — every `uses:` in the tree follows the convention.** * * ⛔ Adding an entry here is the deliberate, reviewable act that objectui#8465 * found missing. Every entry needs a real reason and the issue that owns it — * "it was like that already" is not a reason, it is the defect. * * The test below rejects an entry that no longer matches an off-convention ref, - * so this table cannot rot into a permanent skip-list. + * so this table cannot rot into a permanent skip-list. That is not theory: the + * one entry this table ever held named `stale.yml :: actions/stale`, and when + * objectui#8548 deleted that workflow the entry stopped matching anything. + * Leaving it would have made every pull request red on a `main` nobody broke, + * so it went in the deletion's own commit — which is exactly the rule working. + * + * ⚠️ An empty table does NOT mean this mechanism is dormant. It means the tree + * currently has nothing to excuse, which is the state the convention is for. + * The gate's red branches are pinned over synthetic tables in + * `scripts/__tests__/check-action-ref-convention.test.ts`, so emptiness here + * costs no coverage. */ -export const DECLARED_EXCEPTIONS = [ - { - workflow: 'stale.yml', - action: 'actions/stale', - issue: 'objectui#8126', - reason: - 'SHA-pinned while objectui#8126 owns this line. The pinned SHA resolves to no commit ' + - 'in actions/stale: 236 scheduled runs since 2026-01-16, 0 successes, every one failing ' + - 'in `Set up job`. This entry exists so that this gate can be green on a tree it did not ' + - 'break, WITHOUT pre-empting what objectui#8126 decides to write there — the reason had ' + - 'to survive in the tree either way (objectui#8465 triage). When #8126 lands: if the ref ' + - 'becomes a tag, DELETE this entry (rule 2 below will say so); if it stays a SHA, replace ' + - 'this reason with the one that justifies keeping it.', - }, -]; +export const DECLARED_EXCEPTIONS = []; /** * Non-vacuity floors. A census that collapses reports an empty offender list, * and an empty offender list renders exactly like a healthy repository. * - * Sized well under today's readings (34 workflow files, 13 distinct refs) so + * Sized well under today's readings — 35 workflow files, 110 refs, 12 distinct, + * measured on this commit, one workflow and one distinct ref below the reading + * these floors were written against (objectui#8548 retired `stale.yml`) — so * ordinary churn never trips them, but far enough above zero that a parser - * regression cannot pass. `CONTROL_ACTION` is the positive control: a term - * known to be present, asserted in the same run as the counts, because a zero - * without one is not a reading. + * regression cannot pass. ⚠️ The floors themselves are NOT lowered for that + * deletion: they still sit far under the reading, and moving a floor down every + * time the population shrinks by one is how a floor stops being able to fail. + * `CONTROL_ACTION` is the positive control: a term known to be present, + * asserted in the same run as the counts, because a zero without one is not a + * reading. */ export const FLOORS = { workflowFiles: 20, distinctRefs: 8, totalRefs: 40 }; export const CONTROL_ACTION = 'actions/checkout';