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
24 changes: 15 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -645,15 +645,21 @@ jobs:
# The docs *link* check is deliberately NOT here any more — do not add it
# back. `node scripts/check-doc-links.mjs` ran in this job from #3213 /
# #3292 (PR #3450) until #3448 promoted it to its own workflow,
# `docs-links.yml`, with no path filters. The reason is this workflow's own
# `paths-ignore`: it lists `content/**` and `'**/*.md'`, and GitHub has no
# per-job path filter, so a docs-ONLY pull request never started `ci.yml`
# at all and the link check never saw the class of PR most likely to break
# a link. It is removed rather than kept in both places: the standalone
# workflow's trigger set is a strict superset of this job's, so a copy here
# could only ever add a second red check for the same broken link, and a
# second place to forget. `scripts/__tests__/docs-links-workflow.test.ts`
# pins the gate to exactly one home.
# `docs-links.yml`, with no path filters. The reason THEN was this
# workflow's own `paths-ignore`, which at the time listed `content/**` and
# `'**/*.md'` on both triggers: GitHub has no per-job path filter, so a
# docs-ONLY pull request never started `ci.yml` at all and the link check
# never saw the class of PR most likely to break a link. Since
# objectui#3523 step 2 that filter is gone from `pull_request` and survives
# only on `push` (objectui#3857), so the pull-request half of that
# blindness is gone — the push half is not, and this job still does not run
# on a docs-only push to `main`. objectui#4381 corrected the present tense
# this note used to carry. It stays removed rather than kept in both places
# either way: the standalone workflow's trigger set is a strict superset of
# this job's, so a copy here could only ever add a second red check for the
# same broken link, and a second place to forget.
# `scripts/__tests__/docs-links-workflow.test.ts` pins the gate to exactly
# one home.

- name: Turbo Cache
if: steps.docs-changes.outputs.should_run == 'true'
Expand Down
35 changes: 27 additions & 8 deletions .github/workflows/control-bytes.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
name: Control Bytes

# Why this is its own workflow instead of a job in `ci.yml` or `lint.yml`: both
# of those list `'**/*.md'`, `content/**`, `docs/**` and `.changeset/**` under
# `paths-ignore`, and GitHub has no per-job path filter. A raw control byte lands
# in markdown exactly as easily as in TypeScript — objectstack#4890 was a NUL in
# a `.claude/` skill file, emitted by the very PR that was writing the rule
# against it — so a gate that cannot see a markdown-only PR rebuilds the hole it
# exists to close. `changeset-guard.yml` sits in this repo for the same reason
# and says so in its own header.
# Why this is its own workflow instead of a job in `ci.yml` or `lint.yml`: on a
# markdown-only change neither of those runs anything expensive, so a gate living
# inside one of them would never see it. A raw control byte lands in markdown
# exactly as easily as in TypeScript — objectstack#4890 was a NUL in a `.claude/`
# skill file, emitted by the very PR that was writing the rule against it — so a
# gate that cannot see a markdown-only PR rebuilds the hole it exists to close.
# `changeset-guard.yml` sits in this repo for the same reason and says so in its
# own header.
#
# That is no longer the reason this header used to give, and objectui#4381
# corrected it. It said both of those workflows list `'**/*.md'`, `content/**`,
# `docs/**` and `.changeset/**` under `paths-ignore`, with no per-job path filter
# available in GitHub Actions, so a markdown-only PR started neither.
# objectui#3523 step 2 deleted `paths-ignore` from their `pull_request` trigger;
# it remains ONLY on `push`. Such a PR does start both workflows and does produce
# their contexts — measured: PR #3856 (one markdown file) 16 checks, PR #4339
# (one line added to AGENTS.md) 17. The correction is objectui#3857; an author
# had already acted on the old sentence and got the opposite result.
#
# What #3523 moved rather than deleted is the path DECISION: it is now the
# `Decide whether this change needs a full run` step in `ci.yml`, with a twin in
# `lint.yml`, and its exclusion list is that `push` filter unchanged — markdown
# among it, held identical to it by
# `scripts/__tests__/merge-queue-reporting.test.ts`. Both workflows therefore
# start, report, and skip every expensive step on a markdown-only PR, and both
# still skip the run outright on a markdown-only push to `main`. The conclusion
# outlived its premise: this gate has to run outside that in-job switch.
#
# Hence: no `paths` and no `paths-ignore` here, deliberately.
# `scripts/__tests__/check-control-bytes.test.ts` fails if either is ever added.
Expand Down
35 changes: 27 additions & 8 deletions .github/workflows/docs-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@ name: Docs Links

# Why this is its own workflow instead of a step in `ci.yml`'s `docs` job, which
# is where this check lived when it first reached CI (#3213 / #3292, PR #3450):
# `ci.yml` lists `'**/*.md'`, `content/**`, `docs/**` and `apps/site/**` under
# `paths-ignore`, and GitHub has no per-job path filter. The published site is
# built from `content/docs/**`, so a docs-ONLY pull request matched every ignore
# pattern, started no workflow at all, and was never link-checked — while a
# docs-only PR is the likeliest way an internal link breaks in the first place.
# The step therefore only ever saw PRs that touched docs *alongside code*, plus
# pushes to `main`; a broken link could land through a pure-docs PR and only turn
# `main` red later, under an unrelated author (#3448).
# `ci.yml` THEN listed `'**/*.md'`, `content/**`, `docs/**` and `apps/site/**`
# under the `paths-ignore` of both its triggers, and GitHub has no per-job path
# filter. The published site is built from `content/docs/**`, so a docs-ONLY
# pull request matched every ignore pattern, started no workflow at all, and was
# never link-checked — while a docs-only PR is the likeliest way an internal link
# breaks in the first place. The step therefore only ever saw PRs that touched
# docs *alongside code*, plus pushes to `main`; a broken link could land through
# a pure-docs PR and only turn `main` red later, under an unrelated author
# (#3448).
#
# Read that paragraph as history, and do not act on its present tense — the
# lead-in used to be written as one, which is what objectui#4381 corrected.
# objectui#3523 step 2 deleted `paths-ignore` from `ci.yml`'s `pull_request`
# trigger; it survives ONLY on `push`. A docs-only PR therefore DOES start
# `ci.yml` today and DOES produce its contexts — measured: PR #3856 (one markdown
# file) 16 checks, PR #4339 (one line added to AGENTS.md) 17. objectui#3857
# pinned that correction after an author acted on the old sentence and got the
# opposite result. Nor would the in-job switch keep a link check out the way it
# keeps the changeset gates out: `ci.yml`'s `docs` job gates its steps on
# `apps/site/` or `content/` having CHANGED, which a docs-only PR satisfies.
#
# Two reasons outlived that premise, and they are why nothing moves back.
# `ci.yml` still filters its `push` lane, so a docs-only push to `main` starts it
# not at all and this workflow is the only thing that link-checks that merge. And
# #3448 settled one gate, one home: this workflow's trigger set is a strict
# superset of that job's, so a copy there could only add a second red check for
# one broken link, and a second place to forget.
#
# `control-bytes.yml` hit the same wall and its header names the consequence: a
# gate that cannot see a markdown-only PR "rebuilds the hole it exists to close".
Expand Down
17 changes: 11 additions & 6 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,10 +456,10 @@ all, and `main` sat with a broken link it would have caught (objectui#3213, obje

**Why it is a separate workflow.** This is the second instance of the lesson `control-bytes.yml`
records, and it was found by the PR that first put this check into CI. That PR added it as a step
in `ci.yml`'s `docs` job — where it could never see the PRs that matter. `ci.yml` lists
`'**/*.md'`, `content/**`, `docs/**` and `apps/site/**` under `paths-ignore`, GitHub's
`paths-ignore` skips the *whole workflow* when every changed file matches, and GitHub has no
per-job path filter. So a **docs-only** PR — the likeliest way an internal link breaks — started no
in `ci.yml`'s `docs` job — where it could never see the PRs that matter. `ci.yml` *then* listed
`'**/*.md'`, `content/**`, `docs/**` and `apps/site/**` under the `paths-ignore` of **both** its
triggers, GitHub's `paths-ignore` skips the *whole workflow* when every changed file matches, and
GitHub has no per-job path filter. So a **docs-only** PR — the likeliest way an internal link breaks — started no
workflow at all, and the check only ever ran on PRs that touched docs alongside code, plus pushes
to `main`. A bad link could merge through a pure-docs PR and turn `main` red later under an
unrelated author (objectui#3448).
Expand Down Expand Up @@ -674,8 +674,13 @@ covers change, and if so, does this change **add** a `.changeset/*.md`?
of the guarded surface, free to drift from the config the script reads.

**Why this is separate from `changeset-guard.yml`, which also polices changesets:** that workflow's
trigger is `paths: ['.changeset/**']`, and the inversion is deliberate — a PR adding *only* a
changeset starts no other workflow, and that guard exists to see it. A PR that **forgot** its
trigger is `paths: ['.changeset/**']`, and the inversion is deliberate — on a PR that adds *only* a
changeset, every gate inside `ci.yml` and `lint.yml` short-circuits, so nothing in either of them
ever reads the changeset, and that guard exists to see exactly that PR. (It is *not*, as this
paragraph said until [#4381](https://github.com/objectstack-ai/objectui/issues/4381), that such a PR
"starts no other workflow": since [#3523](https://github.com/objectstack-ai/objectui/issues/3523)
both workflows start and report on it — see **Changeset Guard** above, and the path-filter bullets
at the top of this page.) A PR that **forgot** its
changeset does not touch `.changeset/**` at all, so the one check able to notice was the one check
guaranteed not to run. Widening those paths would have broken the case that guard was built for.
Two workflows, opposite directions: one polices the *level* of a declaration that exists, the other
Expand Down
27 changes: 22 additions & 5 deletions scripts/__tests__/check-changeset-presence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,28 @@ describe('changeset-presence.yml — the gate can start on the PR that needs it'
});

it('leaves changeset-guard.yml alone — the two gates face opposite directions', () => {
// `changeset-guard.yml`'s `paths: ['.changeset/**']` is deliberate: a PR
// adding ONLY a changeset starts no other workflow, and that guard exists to
// see it. A PR that FORGOT its changeset does not touch `.changeset/**`, so
// widening those paths would have broken the case it was built for while
// still not catching this one. Two workflows, one direction each.
// `changeset-guard.yml`'s `paths: ['.changeset/**']` is deliberate: on a PR
// that adds ONLY a changeset, every gate inside `ci.yml` and `lint.yml`
// skips, so nothing in either of them ever reads the changeset — and that
// guard exists to see exactly that PR. A PR that FORGOT its changeset does
// not touch `.changeset/**`, so widening those paths would have broken the
// case it was built for while still not catching this one. Two workflows,
// one direction each.
//
// Not the reason this comment used to give — "a PR adding ONLY a changeset
// starts no other workflow". That is the sentence PR #4371 rewrote in the
// docblock of `check-changeset-presence.mjs`, the script this file
// accompanies, and the pair disagreed across the two until objectui#4381.
// objectui#3523 step 2 deleted `paths-ignore` from `ci.yml`'s and
// `lint.yml`'s `pull_request` trigger; it survives only on `push`. Such a PR
// therefore DOES start both and DOES produce their contexts — measured: PR
// #3856 (one markdown file) 16 checks, PR #4339 (one line added to
// AGENTS.md) 17. What still skips is the expensive work: the `Decide whether
// this change needs a full run` step excludes markdown and `.changeset/**`
// and skips every step below itself, and its exclusion list is that `push`
// filter unchanged, held identical to it by
// `scripts/__tests__/merge-queue-reporting.test.ts`. The conclusion outlived
// its premise; objectui#3857 pinned the correction.
const guard = withoutComments(fs.readFileSync(path.join(workflowDir, 'changeset-guard.yml'), 'utf8'));
expect(guard).toMatch(/^\s+paths:/m);
expect(guard).toMatch(/\.changeset\//);
Expand Down
21 changes: 16 additions & 5 deletions scripts/__tests__/check-control-bytes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,22 @@ describe('wiring — the gate is actually reachable and actually runs', () => {

it('does NOT filter that workflow by path', () => {
// This is the whole reason it is its own workflow rather than a step in
// ci.yml or lint.yml: both of those `paths-ignore` markdown, `content/**`,
// `docs/**` and `.changeset/**`. Markdown is precisely the carrier
// objectstack#4890 was found in, so a path filter here would rebuild the
// hole this guard exists to close. `changeset-guard.yml` is in the repo for
// the same reason and says so in its own header.
// ci.yml or lint.yml: on a markdown-only change both of those skip every
// expensive step, so a gate inside either would never see it. Markdown is
// precisely the carrier objectstack#4890 was found in, so a path filter here
// would rebuild the hole this guard exists to close. `changeset-guard.yml`
// is in the repo for the same reason and says so in its own header.
//
// Not the reason this comment used to give — "both of those `paths-ignore`
// markdown, `content/**`, `docs/**` and `.changeset/**`" — which
// objectui#4381 corrected here and in the workflow's own header. Since
// objectui#3523 step 2 that filter is gone from their `pull_request` trigger
// and survives only on `push`, so a markdown-only PR does start both and
// does produce their contexts (measured — PR #3856, one markdown file, 16
// checks). What still skips is the work inside them: the `Decide whether
// this change needs a full run` step excludes markdown and skips every step
// below itself, and the `push` lane is filtered at the trigger as before.
// The conclusion outlived its premise; objectui#3857 pinned the correction.
const workflow = fs.readFileSync(workflowPath, 'utf8');
expect(workflow).not.toMatch(/paths-ignore:/);
expect(workflow).not.toMatch(/^\s+paths:/m);
Expand Down
23 changes: 17 additions & 6 deletions scripts/__tests__/docs-links-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,23 @@ import { fileURLToPath } from 'node:url';
* `.github/` called it, so it had never run in CI at all and `main` carried a
* broken link it would have caught (#3213, #3292). PR #3450 fixed that by adding
* it as a step in `ci.yml`'s `docs` job — and rebuilt half the hole in the
* process: `ci.yml` lists every markdown path, plus `content/**`, `docs/**` and
* `apps/site/**`, under `paths-ignore`; `paths-ignore` skips the *entire*
* workflow when every changed file matches it, and GitHub has no per-job path
* filter. A docs-only PR therefore started no workflow, so the one class of
* change most likely to break an internal link was the one class the link check
* could never see.
* process: `ci.yml` THEN listed every markdown path, plus `content/**`,
* `docs/**` and `apps/site/**`, under the `paths-ignore` of both its triggers;
* `paths-ignore` skips the *entire* workflow when every changed file matches it,
* and GitHub has no per-job path filter. A docs-only PR therefore started no
* workflow, so the one class of change most likely to break an internal link was
* the one class the link check could never see.
*
* That paragraph is history — its lead-in used to be written in the present
* tense, which objectui#4381 corrected here and in the workflow's own header.
* objectui#3523 step 2 deleted `paths-ignore` from `ci.yml`'s `pull_request`
* trigger; it survives only on `push`, so a docs-only PR does start `ci.yml` now
* (measured: PR #3856, one markdown file, 16 checks) and objectui#3857 pinned
* the correction. The in-job switch would not keep a link check out either — the
* `docs` job runs its steps precisely when `content/` or `apps/site/` changed.
* What outlived the premise is what the assertions below pin: `ci.yml` keeps the
* filter on its `push` lane, so a docs-only push to `main` starts it not at all,
* and #3448 settled one gate, one home.
*
* `control-bytes.yml` hit this exact wall first and its header states the
* consequence: a gate that cannot see a markdown-only PR "rebuilds the hole it
Expand Down
Loading