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
118 changes: 0 additions & 118 deletions .github/workflows/stale.yml

This file was deleted.

30 changes: 11 additions & 19 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
74 changes: 59 additions & 15 deletions scripts/__tests__/check-action-ref-convention.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>) => {
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', () => {
Expand All @@ -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',
]);
});
});
Expand Down
2 changes: 1 addition & 1 deletion scripts/__tests__/ci-cd-pipeline-doc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).` +
Expand Down
46 changes: 29 additions & 17 deletions scripts/__tests__/workflow-cache-save-bound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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
Expand All @@ -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[] = [];
Expand Down
Loading
Loading