chore(ci): slow both dependabot channels from weekly to monthly #5366

chore(ci): slow both dependabot channels from weekly to monthly

chore(ci): slow both dependabot channels from weekly to monthly #5366

Workflow file for this run

name: Lint
# Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never
# gated a PR. That mattered more than it looked: every `object-ui/*` rule that
# `eslint.config.js` sets to `error` is a ratchet — added *specifically* so a
# new violation fails CI, with its existing sites pre-cleaned first so the rule
# lints clean on the day it lands. While nothing ran them, every one of them was
# inert.
#
# `eslint.config.js` is the single list of those rules, and this comment
# deliberately neither counts them nor names them: it used to hand-count, and
# the count was stale by the time anyone read it (#3261). A hand-copied
# enumeration drifts by construction, and a stale one still reads as
# authoritative — `content/docs/guide/ci-cd-pipeline.md` avoids the number for
# the same reason. `scripts/__tests__/lint-workflow.test.ts` holds that in
# place: it fails if a count or a rule name reappears here, if the config stops
# setting any `object-ui/*` rule to `error`, or if this workflow stops gating
# pull requests.
#
# `--max-warnings` is deliberately not set: warnings repo-wide run into the
# thousands, dominated by `no-explicit-any` plus React Compiler rules the config
# downgrades on purpose — known historical debt, not a signal. This gate is
# about errors. No exact figure here, for the reason above: this paragraph used
# to carry a hand-maintained warning count and percentage that nothing
# recomputed and nothing alarmed on as they aged (#3274), and
# `scripts/check-lint-coverage.mjs` held a copy of the same number that would
# have gone stale on its own clock. The order of magnitude is the whole
# argument; the integer never was.
on:
push:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'content/**'
- 'docs/**'
- '.changeset/**'
# No `paths-ignore` here any more (objectui#3523, step 2) — it skipped the
# whole workflow on a docs-only / changeset-only PR, so the `Lint` context was
# absent exactly where a required check must still report. The path decision
# moved into the job below. `push` above keeps its copy: nothing judges a push
# to `main`.
pull_request:
branches: [main, develop]
# ── Merge queue (objectui#3523) ────────────────────────────────────────
# The merge queue is ENFORCED on this repository by a ruleset — a direct push
# to `main` returns 405 `Changes must be made through the merge queue`
# (measured in #3243). Until this trigger landed, not one of the repository's
# workflows subscribed `merge_group`: repo-wide `event=merge_group` runs stood
# at total_count = 0, historically. A queue with nothing subscribed to it can
# only have an EMPTY required-check set, so it rebuilt each PR on the current
# `main` and let it through without validating anything.
#
# That is not a theoretical hole; it was cashed in on 2026-08-07. #3498 landed
# a `scripts/` type gate, itself fully green, that left a TS2578 on `main`;
# #3503, #3510 and #3516 then merged between 02:11Z and 02:15Z with `Type
# Check` at conclusion=failure, and #3505 hot-fixed the result. objectstack
# went through the same frames (objectstack#6067 -> #5615).
#
# `types:` is spelled out although `checks_requested` is the ONLY activity
# type GitHub defines for `merge_group` today — the two spellings are
# equivalent right now (objectstack's `ci.yml` and `lint.yml` use the bare
# `merge_group:` form and produce queue builds normally, 3552 of them). Naming
# the type means a second activity type added later cannot silently start
# queue builds this workflow was never written for.
#
# `concurrency` below needs no merge-queue special case, and that was checked
# rather than assumed: on `merge_group` the `github.event.pull_request` half of
# the group expression is null, so the group falls back to `github.ref`, which
# on a queue build is the queue's own generation — measured on objectstack,
# `gh-readonly-queue/main/pr-6594-251e888ac9ace8226f3a8450951e5b40a0a84c2c`.
# It can collide with neither a pull-request group (a bare PR number) nor a
# push group (`refs/heads/main`), so a queue build and the PR build it came
# from never cancel each other.
merge_group:
types: [checks_requested]
workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true
# `fetch-depth: 0` for the gate step below (objectui#3523): it diffs
# against the merge base, which a depth-1 clone cannot resolve.
fetch-depth: 0
# ── Always report; run only when it matters (objectui#3523) ──────────
# `on.pull_request.paths-ignore` used to skip this whole workflow on a
# docs-only or changeset-only pull request, so the `Lint` context was
# simply absent there — and a required check that never reports leaves the
# PR pending forever (in the merge queue, until the ruleset's 60-minute
# timeout fails it). The filter moved from the trigger into the job: the
# job always runs and always reports, the paths decide only whether the
# expensive steps execute. `ci.yml`'s `docs` job is the in-repo precedent
# for the shape, and its `type-check` job carries the long version of this
# note. The list below IS the `paths-ignore` it replaced; the `push`
# trigger keeps its copy, because nothing judges a push to `main`.
#
# Fails OPEN: if the diff cannot be computed the job runs everything,
# rather than reporting green having linted nothing (objectstack#4928).
- name: Decide whether this change needs a full run
id: relevant
run: |
if [ "${{ github.event_name }}" != 'pull_request' ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Not a pull request: push is filtered at the trigger, and a merge_group build is the last validation before main. Running everything.'
exit 0
fi
if ! CHANGED=$(git diff --name-only \
'${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}' -- \
. \
':(exclude,glob)**/*.md' \
':(exclude,glob)content/**' \
':(exclude,glob)docs/**' \
':(exclude,glob).changeset/**'); then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Could not diff against the merge base. Running everything rather than skipping silently.'
exit 0
fi
if [ -n "$CHANGED" ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo "$CHANGED"
else
echo 'should_run=false' >> "$GITHUB_OUTPUT"
echo 'Only ignored paths changed. Skipping the steps below; this check still reports.'
fi
- name: Enable Corepack
if: steps.relevant.outputs.should_run == 'true'
run: corepack enable
- name: Verify pnpm version
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --version
- name: Setup Node.js
if: steps.relevant.outputs.should_run == 'true'
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# Every package must run ESLint or be declared a known gap. turbo skips
# scriptless packages silently, so without this a package reads as clean
# because nothing linted it. Runs before install: only reads package.json.
- name: Verify lint coverage
if: steps.relevant.outputs.should_run == 'true'
run: node scripts/check-lint-coverage.mjs
# ── One entry guard, one predicate (objectui#6092) ───────────────────
# A `scripts/**` CLI has to answer "did node run me, or did something
# import me?" before it does anything, and the hand-typed answers in this
# tree had drifted into NINE spellings across 28 `.mjs` files. Every one
# of them fails the same way: node resolves symlinks for the module graph
# but leaves `process.argv[1]` as the caller typed it, so a script reached
# through a symlink compares two different paths, answers false, and does
# NOTHING — exit 0, no output. The wrappers that spawn these tools hold
# `result.status` only, so an inert child is a green gate. Measured on a
# real blocking gate in this repository (objectui#6078):
# `check-skills-paths.mjs` run directly on a deliberately broken tree
# exits 1 with 696 bytes naming the dead path; run through a symlink
# against the SAME tree it exits 0 with no output.
#
# `scripts/invoked-as.mjs` landed here in #5984 with a header stating that
# a gate enforced the single-spelling rule. It did not exist, and the
# sweep it described had not happened either (objectui#6078). This is that
# gate. It lands BEFORE the conversion on purpose: the worklist grew while
# the card sat open, so a sweep with nothing under it can be undone
# silently by the next pull request. The 29 existing guards are baselined,
# SHRINK-ONLY, and the script's own header says why that is safe.
#
# Runs before install, next to `check-lint-coverage.mjs` above and for the
# same reason: it reads sources with no dependency beyond node builtins
# and two local modules, so nothing it needs is in `node_modules`. Placed
# before `pnpm install` it also cannot be made green by an install
# failure. `--self-test` runs FIRST and is the half that stops the gate
# rotting into decoration — it drives the scanner over fixture sources,
# including the nine spellings measured in this tree, and pins the
# baseline in every direction it can move.
#
# Invoked as `node` rather than through the `pnpm check:entry-guard` alias
# for the pre-install placement, matching `check-lint-coverage.mjs` above
# and `check-cross-repo-closer-outcome.mjs` below. The alias exists in
# `package.json` for local use and `scripts/__tests__/entry-guard-wiring.test.ts`
# holds the two spellings to the same script.
- name: Verify every scripts/ entry guard goes through one predicate
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-entry-guard.mjs --self-test
node scripts/check-entry-guard.mjs
# ── The ported objectstack tooling is a PINNED copy (objectui#6642) ───
# `scripts/pm/check-half-states.mjs` came from objectstack (objectui#5791)
# under a workflow header calling it a verbatim copy and enumerating the
# three things a re-sync must not clobber. Nothing checked either half.
# Measured 2026-08-28, before this step existed: the ported copy stood at
# 9,340 lines against upstream's 12,948 — a 4,637-line `diff` — and its
# own `--self-test` ran 1,116 cases where upstream's ran 1,574. So 458
# predicate cases had landed upstream and never arrived here, while the
# patrol went on rendering a confident report with the corresponding rows
# simply missing.
#
# The direction of harm is this repository's least visible one: a drifted
# copy does not fail, it REPORTS. It became load-bearing once already —
# objectui#6641 had to hand-port H22's closure floor into this copy,
# because wiring the new environment variable in the workflow alone would
# have set a variable this copy did not read.
#
# The gate reverses the DECLARED divergences out of each ported file and
# requires the reconstruction to hash to the pinned upstream digest, so
# drift beyond the declared set is byte-detectable in both directions —
# an edit here, or upstream moving. ⛔ It fetches nothing: a gate that
# reached api.github.com would be red on a network hiccup and green on a
# cached 200, and this repo's whole reason for owning a patrol is that a
# check which cannot read its input must never read as clean (#4690).
#
# Runs before install, next to the two gates above and for the same
# reason: node builtins and one local module only, so an install failure
# cannot take it down with it. `--self-test` runs FIRST — it drives the
# real comparer over fixtures (parity holds, drift outside a region,
# drift inside one, an ambiguous anchor, the pin-bump procedure, and
# every malformed-pin shape), which is what stops a comparer that
# recognises nothing from reading as a clean tree.
- name: Verify the ported objectstack tooling still matches its pin
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-upstream-port-parity.mjs --self-test
node scripts/check-upstream-port-parity.mjs
- name: Turbo Cache
if: steps.relevant.outputs.should_run == 'true'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
if: steps.relevant.outputs.should_run == 'true'
run: pnpm install --frozen-lockfile
- name: Run linter
if: steps.relevant.outputs.should_run == 'true'
run: pnpm lint
# ── The cross-repo closer's outcome contract (#5261) ──────────────────
# `cross-repo-issue-closer.yml` carries ~250 lines of inline
# github-script, and until this step existed it was code nobody had ever
# seen run: it fires only on a merge, its conclusion is required by
# nothing, and every one of its runs so far has been green. That last part
# is the problem rather than the reassurance — measured over every merged
# pull request in this repository, its close loop has had a live target
# roughly one and a half times a day since it landed, took the same
# `already closed -- skipping` exit every time, and left no backlink on
# any of them. Greenly. The header of the workflow carries the figures.
#
# This step is the exercise: the shipped script is extracted from the YAML
# with a real parser (never retyped) and run under doubles the way
# actions/github-script runs it, as one AsyncFunction body. The scenarios
# pin the target parse, the target KIND, and the outcome of every exit —
# which of setFailed / warning / job summary fires, and which API calls
# were made.
#
# Assertion 0 is the compile, and it is not theoretical: the framework's
# copy of this workflow was taken down twice in one day by a `SyntaxError`
# in the inline block, i.e. a script that never ran at all, on a
# post-merge workflow whose red nothing else in CI can see.
#
# `--self-test` runs FIRST and is the half that stops the battery rotting
# into decoration: it mutates the shipped script — downgrade the verdict
# to a warning, break out of the loop instead of isolating, drop the
# same-repo skip, collapse the already-closed branch, strip the backlink
# marker, drop the pull-request guard, and the rest — and requires the
# battery to go RED for each, naming the scenario that catches it. Neither
# the mutations nor the scenarios are counted here: a hand-copied
# enumeration drifts by construction, which is the lesson this workflow's
# own header records. A mutation whose anchor no longer exists is a
# failure too, so rewriting the workflow cannot leave them silently
# matching nothing.
#
# Invoked as `node` rather than through a `pnpm check:*` alias, matching
# `check-lint-coverage.mjs` above. No network, no build; well under a
# second.
- name: Cross-repo closer outcome contract
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-cross-repo-closer-outcome.mjs --self-test
node scripts/check-cross-repo-closer-outcome.mjs
# ── The product's own check command, on the product's own tree ────────
# `objectui check` is what a consumer runs against their schema tree, and
# the root `check` script points that same command at this repository.
# Nothing ran it. It exited 1 with 64 errors on `main` — and had done
# since the first `tsconfig.json` grew a comment — until someone ran it by
# hand while measuring something unrelated (objectui#5237, fixed by
# #5245). A shipped command sitting red on its own repository is the
# dogfood invariant failing, and the reason it could sit there is that no
# gate ever asked. This is that gate (objectui#5246).
#
# The build step below is not a convenience. The root script is
# `node packages/cli/dist/cli.js check`, and this job installs without
# building, so without it the step dies on a missing file. That failure
# mode is worse than no gate at all: it is red for a reason that has
# nothing to do with the tree being checked, and the obvious repair from
# outside is to delete the step — leaving the hole exactly as it was, now
# with a commit saying it was considered.
#
# `...` is pnpm's dependency closure: the CLI plus every workspace
# package it depends on, in topological order, derived from the graph
# rather than listed here. The closure is the requirement and not just
# tidiness — `dist/cli.js` imports `@object-ui/types`' built output at
# startup, so building the CLI package alone produces a binary that
# cannot load (objectui#5237 records the same import as the reproduce
# recipe's first step).
#
# Deliberately NOT `turbo run build`, although the Turbo cache above is
# restored by this point and would usually make it free. A turbo cache
# HIT restores a task's recorded outputs, and an entry recorded with an
# empty output set replays as "cache hit, replaying logs" plus FULL
# TURBO while writing no `dist/` at all — measured on this repo, where
# the CLI then died with ERR_MODULE_NOT_FOUND on the types import above.
# A blocking gate must not be able to fail for a reason that lives in a
# cache rather than in the tree it is judging, which is the same argument
# as the paragraph above one level down. Building through pnpm has no
# cache layer to replay, and it costs well under a minute.
- name: Build the CLI the self-check runs
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --filter '@object-ui/cli...' build
# Errors only, which is the command's existing behaviour rather than a
# setting chosen here: a parse failure increments the error count, a
# non-zero count is the only thing that exits 1, and the unknown-schema-
# type arm prints and moves on. So this step blocks on the same arm
# `pnpm lint` above does, and for the same reason — errors are a signal,
# the warning stream is known debt. Nothing here promotes those warnings
# to failures, and no output-suppressing flag hides them either: they stay
# visible in the log and non-blocking. That arm belongs to objectui#5127,
# which is open; whatever it settles changes what this step PRINTS, never
# what it fails on. No count is quoted, for the reason the `--max-warnings`
# paragraph in the header gives.
- name: Verify the CLI's own check command passes on this repository
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

chore(ci): slow both dependabot channels from weekly to monthly #5366

chore(ci): slow both dependabot channels from weekly to monthly

chore(ci): slow both dependabot channels from weekly to monthly #5366

Workflow file for this run

name: Lint
# Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never
# gated a PR. That mattered more than it looked: every `object-ui/*` rule that
# `eslint.config.js` sets to `error` is a ratchet — added *specifically* so a
# new violation fails CI, with its existing sites pre-cleaned first so the rule
# lints clean on the day it lands. While nothing ran them, every one of them was
# inert.
#
# `eslint.config.js` is the single list of those rules, and this comment
# deliberately neither counts them nor names them: it used to hand-count, and
# the count was stale by the time anyone read it (#3261). A hand-copied
# enumeration drifts by construction, and a stale one still reads as
# authoritative — `content/docs/guide/ci-cd-pipeline.md` avoids the number for
# the same reason. `scripts/__tests__/lint-workflow.test.ts` holds that in
# place: it fails if a count or a rule name reappears here, if the config stops
# setting any `object-ui/*` rule to `error`, or if this workflow stops gating
# pull requests.
#
# `--max-warnings` is deliberately not set: warnings repo-wide run into the
# thousands, dominated by `no-explicit-any` plus React Compiler rules the config
# downgrades on purpose — known historical debt, not a signal. This gate is
# about errors. No exact figure here, for the reason above: this paragraph used
# to carry a hand-maintained warning count and percentage that nothing
# recomputed and nothing alarmed on as they aged (#3274), and
# `scripts/check-lint-coverage.mjs` held a copy of the same number that would
# have gone stale on its own clock. The order of magnitude is the whole
# argument; the integer never was.
on:
push:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'content/**'
- 'docs/**'
- '.changeset/**'
# No `paths-ignore` here any more (objectui#3523, step 2) — it skipped the
# whole workflow on a docs-only / changeset-only PR, so the `Lint` context was
# absent exactly where a required check must still report. The path decision
# moved into the job below. `push` above keeps its copy: nothing judges a push
# to `main`.
pull_request:
branches: [main, develop]
# ── Merge queue (objectui#3523) ────────────────────────────────────────
# The merge queue is ENFORCED on this repository by a ruleset — a direct push
# to `main` returns 405 `Changes must be made through the merge queue`
# (measured in #3243). Until this trigger landed, not one of the repository's
# workflows subscribed `merge_group`: repo-wide `event=merge_group` runs stood
# at total_count = 0, historically. A queue with nothing subscribed to it can
# only have an EMPTY required-check set, so it rebuilt each PR on the current
# `main` and let it through without validating anything.
#
# That is not a theoretical hole; it was cashed in on 2026-08-07. #3498 landed
# a `scripts/` type gate, itself fully green, that left a TS2578 on `main`;
# #3503, #3510 and #3516 then merged between 02:11Z and 02:15Z with `Type
# Check` at conclusion=failure, and #3505 hot-fixed the result. objectstack
# went through the same frames (objectstack#6067 -> #5615).
#
# `types:` is spelled out although `checks_requested` is the ONLY activity
# type GitHub defines for `merge_group` today — the two spellings are
# equivalent right now (objectstack's `ci.yml` and `lint.yml` use the bare
# `merge_group:` form and produce queue builds normally, 3552 of them). Naming
# the type means a second activity type added later cannot silently start
# queue builds this workflow was never written for.
#
# `concurrency` below needs no merge-queue special case, and that was checked
# rather than assumed: on `merge_group` the `github.event.pull_request` half of
# the group expression is null, so the group falls back to `github.ref`, which
# on a queue build is the queue's own generation — measured on objectstack,
# `gh-readonly-queue/main/pr-6594-251e888ac9ace8226f3a8450951e5b40a0a84c2c`.
# It can collide with neither a pull-request group (a bare PR number) nor a
# push group (`refs/heads/main`), so a queue build and the PR build it came
# from never cancel each other.
merge_group:
types: [checks_requested]
workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true
# `fetch-depth: 0` for the gate step below (objectui#3523): it diffs
# against the merge base, which a depth-1 clone cannot resolve.
fetch-depth: 0
# ── Always report; run only when it matters (objectui#3523) ──────────
# `on.pull_request.paths-ignore` used to skip this whole workflow on a
# docs-only or changeset-only pull request, so the `Lint` context was
# simply absent there — and a required check that never reports leaves the
# PR pending forever (in the merge queue, until the ruleset's 60-minute
# timeout fails it). The filter moved from the trigger into the job: the
# job always runs and always reports, the paths decide only whether the
# expensive steps execute. `ci.yml`'s `docs` job is the in-repo precedent
# for the shape, and its `type-check` job carries the long version of this
# note. The list below IS the `paths-ignore` it replaced; the `push`
# trigger keeps its copy, because nothing judges a push to `main`.
#
# Fails OPEN: if the diff cannot be computed the job runs everything,
# rather than reporting green having linted nothing (objectstack#4928).
- name: Decide whether this change needs a full run
id: relevant
run: |
if [ "${{ github.event_name }}" != 'pull_request' ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Not a pull request: push is filtered at the trigger, and a merge_group build is the last validation before main. Running everything.'
exit 0
fi
if ! CHANGED=$(git diff --name-only \
'${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}' -- \
. \
':(exclude,glob)**/*.md' \
':(exclude,glob)content/**' \
':(exclude,glob)docs/**' \
':(exclude,glob).changeset/**'); then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Could not diff against the merge base. Running everything rather than skipping silently.'
exit 0
fi
if [ -n "$CHANGED" ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo "$CHANGED"
else
echo 'should_run=false' >> "$GITHUB_OUTPUT"
echo 'Only ignored paths changed. Skipping the steps below; this check still reports.'
fi
- name: Enable Corepack
if: steps.relevant.outputs.should_run == 'true'
run: corepack enable
- name: Verify pnpm version
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --version
- name: Setup Node.js
if: steps.relevant.outputs.should_run == 'true'
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# Every package must run ESLint or be declared a known gap. turbo skips
# scriptless packages silently, so without this a package reads as clean
# because nothing linted it. Runs before install: only reads package.json.
- name: Verify lint coverage
if: steps.relevant.outputs.should_run == 'true'
run: node scripts/check-lint-coverage.mjs
# ── One entry guard, one predicate (objectui#6092) ───────────────────
# A `scripts/**` CLI has to answer "did node run me, or did something
# import me?" before it does anything, and the hand-typed answers in this
# tree had drifted into NINE spellings across 28 `.mjs` files. Every one
# of them fails the same way: node resolves symlinks for the module graph
# but leaves `process.argv[1]` as the caller typed it, so a script reached
# through a symlink compares two different paths, answers false, and does
# NOTHING — exit 0, no output. The wrappers that spawn these tools hold
# `result.status` only, so an inert child is a green gate. Measured on a
# real blocking gate in this repository (objectui#6078):
# `check-skills-paths.mjs` run directly on a deliberately broken tree
# exits 1 with 696 bytes naming the dead path; run through a symlink
# against the SAME tree it exits 0 with no output.
#
# `scripts/invoked-as.mjs` landed here in #5984 with a header stating that
# a gate enforced the single-spelling rule. It did not exist, and the
# sweep it described had not happened either (objectui#6078). This is that
# gate. It lands BEFORE the conversion on purpose: the worklist grew while
# the card sat open, so a sweep with nothing under it can be undone
# silently by the next pull request. The 29 existing guards are baselined,
# SHRINK-ONLY, and the script's own header says why that is safe.
#
# Runs before install, next to `check-lint-coverage.mjs` above and for the
# same reason: it reads sources with no dependency beyond node builtins
# and two local modules, so nothing it needs is in `node_modules`. Placed
# before `pnpm install` it also cannot be made green by an install
# failure. `--self-test` runs FIRST and is the half that stops the gate
# rotting into decoration — it drives the scanner over fixture sources,
# including the nine spellings measured in this tree, and pins the
# baseline in every direction it can move.
#
# Invoked as `node` rather than through the `pnpm check:entry-guard` alias
# for the pre-install placement, matching `check-lint-coverage.mjs` above
# and `check-cross-repo-closer-outcome.mjs` below. The alias exists in
# `package.json` for local use and `scripts/__tests__/entry-guard-wiring.test.ts`
# holds the two spellings to the same script.
- name: Verify every scripts/ entry guard goes through one predicate
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-entry-guard.mjs --self-test
node scripts/check-entry-guard.mjs
# ── The ported objectstack tooling is a PINNED copy (objectui#6642) ───
# `scripts/pm/check-half-states.mjs` came from objectstack (objectui#5791)
# under a workflow header calling it a verbatim copy and enumerating the
# three things a re-sync must not clobber. Nothing checked either half.
# Measured 2026-08-28, before this step existed: the ported copy stood at
# 9,340 lines against upstream's 12,948 — a 4,637-line `diff` — and its
# own `--self-test` ran 1,116 cases where upstream's ran 1,574. So 458
# predicate cases had landed upstream and never arrived here, while the
# patrol went on rendering a confident report with the corresponding rows
# simply missing.
#
# The direction of harm is this repository's least visible one: a drifted
# copy does not fail, it REPORTS. It became load-bearing once already —
# objectui#6641 had to hand-port H22's closure floor into this copy,
# because wiring the new environment variable in the workflow alone would
# have set a variable this copy did not read.
#
# The gate reverses the DECLARED divergences out of each ported file and
# requires the reconstruction to hash to the pinned upstream digest, so
# drift beyond the declared set is byte-detectable in both directions —
# an edit here, or upstream moving. ⛔ It fetches nothing: a gate that
# reached api.github.com would be red on a network hiccup and green on a
# cached 200, and this repo's whole reason for owning a patrol is that a
# check which cannot read its input must never read as clean (#4690).
#
# Runs before install, next to the two gates above and for the same
# reason: node builtins and one local module only, so an install failure
# cannot take it down with it. `--self-test` runs FIRST — it drives the
# real comparer over fixtures (parity holds, drift outside a region,
# drift inside one, an ambiguous anchor, the pin-bump procedure, and
# every malformed-pin shape), which is what stops a comparer that
# recognises nothing from reading as a clean tree.
- name: Verify the ported objectstack tooling still matches its pin
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-upstream-port-parity.mjs --self-test
node scripts/check-upstream-port-parity.mjs
- name: Turbo Cache
if: steps.relevant.outputs.should_run == 'true'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
if: steps.relevant.outputs.should_run == 'true'
run: pnpm install --frozen-lockfile
- name: Run linter
if: steps.relevant.outputs.should_run == 'true'
run: pnpm lint
# ── The cross-repo closer's outcome contract (#5261) ──────────────────
# `cross-repo-issue-closer.yml` carries ~250 lines of inline
# github-script, and until this step existed it was code nobody had ever
# seen run: it fires only on a merge, its conclusion is required by
# nothing, and every one of its runs so far has been green. That last part
# is the problem rather than the reassurance — measured over every merged
# pull request in this repository, its close loop has had a live target
# roughly one and a half times a day since it landed, took the same
# `already closed -- skipping` exit every time, and left no backlink on
# any of them. Greenly. The header of the workflow carries the figures.
#
# This step is the exercise: the shipped script is extracted from the YAML
# with a real parser (never retyped) and run under doubles the way
# actions/github-script runs it, as one AsyncFunction body. The scenarios
# pin the target parse, the target KIND, and the outcome of every exit —
# which of setFailed / warning / job summary fires, and which API calls
# were made.
#
# Assertion 0 is the compile, and it is not theoretical: the framework's
# copy of this workflow was taken down twice in one day by a `SyntaxError`
# in the inline block, i.e. a script that never ran at all, on a
# post-merge workflow whose red nothing else in CI can see.
#
# `--self-test` runs FIRST and is the half that stops the battery rotting
# into decoration: it mutates the shipped script — downgrade the verdict
# to a warning, break out of the loop instead of isolating, drop the
# same-repo skip, collapse the already-closed branch, strip the backlink
# marker, drop the pull-request guard, and the rest — and requires the
# battery to go RED for each, naming the scenario that catches it. Neither
# the mutations nor the scenarios are counted here: a hand-copied
# enumeration drifts by construction, which is the lesson this workflow's
# own header records. A mutation whose anchor no longer exists is a
# failure too, so rewriting the workflow cannot leave them silently
# matching nothing.
#
# Invoked as `node` rather than through a `pnpm check:*` alias, matching
# `check-lint-coverage.mjs` above. No network, no build; well under a
# second.
- name: Cross-repo closer outcome contract
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-cross-repo-closer-outcome.mjs --self-test
node scripts/check-cross-repo-closer-outcome.mjs
# ── The product's own check command, on the product's own tree ────────
# `objectui check` is what a consumer runs against their schema tree, and
# the root `check` script points that same command at this repository.
# Nothing ran it. It exited 1 with 64 errors on `main` — and had done
# since the first `tsconfig.json` grew a comment — until someone ran it by
# hand while measuring something unrelated (objectui#5237, fixed by
# #5245). A shipped command sitting red on its own repository is the
# dogfood invariant failing, and the reason it could sit there is that no
# gate ever asked. This is that gate (objectui#5246).
#
# The build step below is not a convenience. The root script is
# `node packages/cli/dist/cli.js check`, and this job installs without
# building, so without it the step dies on a missing file. That failure
# mode is worse than no gate at all: it is red for a reason that has
# nothing to do with the tree being checked, and the obvious repair from
# outside is to delete the step — leaving the hole exactly as it was, now
# with a commit saying it was considered.
#
# `...` is pnpm's dependency closure: the CLI plus every workspace
# package it depends on, in topological order, derived from the graph
# rather than listed here. The closure is the requirement and not just
# tidiness — `dist/cli.js` imports `@object-ui/types`' built output at
# startup, so building the CLI package alone produces a binary that
# cannot load (objectui#5237 records the same import as the reproduce
# recipe's first step).
#
# Deliberately NOT `turbo run build`, although the Turbo cache above is
# restored by this point and would usually make it free. A turbo cache
# HIT restores a task's recorded outputs, and an entry recorded with an
# empty output set replays as "cache hit, replaying logs" plus FULL
# TURBO while writing no `dist/` at all — measured on this repo, where
# the CLI then died with ERR_MODULE_NOT_FOUND on the types import above.
# A blocking gate must not be able to fail for a reason that lives in a
# cache rather than in the tree it is judging, which is the same argument
# as the paragraph above one level down. Building through pnpm has no
# cache layer to replay, and it costs well under a minute.
- name: Build the CLI the self-check runs
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --filter '@object-ui/cli...' build
# Errors only, which is the command's existing behaviour rather than a
# setting chosen here: a parse failure increments the error count, a
# non-zero count is the only thing that exits 1, and the unknown-schema-
# type arm prints and moves on. So this step blocks on the same arm
# `pnpm lint` above does, and for the same reason — errors are a signal,
# the warning stream is known debt. Nothing here promotes those warnings
# to failures, and no output-suppressing flag hides them either: they stay
# visible in the log and non-blocking. That arm belongs to objectui#5127,
# which is open; whatever it settles changes what this step PRINTS, never
# what it fails on. No count is quoted, for the reason the `--max-warnings`
# paragraph in the header gives.
- name: Verify the CLI's own check command passes on this repository
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore(ci): slow both dependabot channels from weekly to monthly #5366

chore(ci): slow both dependabot channels from weekly to monthly

chore(ci): slow both dependabot channels from weekly to monthly #5366

Workflow file for this run

name: Lint
# Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never
# gated a PR. That mattered more than it looked: every `object-ui/*` rule that
# `eslint.config.js` sets to `error` is a ratchet — added *specifically* so a
# new violation fails CI, with its existing sites pre-cleaned first so the rule
# lints clean on the day it lands. While nothing ran them, every one of them was
# inert.
#
# `eslint.config.js` is the single list of those rules, and this comment
# deliberately neither counts them nor names them: it used to hand-count, and
# the count was stale by the time anyone read it (#3261). A hand-copied
# enumeration drifts by construction, and a stale one still reads as
# authoritative — `content/docs/guide/ci-cd-pipeline.md` avoids the number for
# the same reason. `scripts/__tests__/lint-workflow.test.ts` holds that in
# place: it fails if a count or a rule name reappears here, if the config stops
# setting any `object-ui/*` rule to `error`, or if this workflow stops gating
# pull requests.
#
# `--max-warnings` is deliberately not set: warnings repo-wide run into the
# thousands, dominated by `no-explicit-any` plus React Compiler rules the config
# downgrades on purpose — known historical debt, not a signal. This gate is
# about errors. No exact figure here, for the reason above: this paragraph used
# to carry a hand-maintained warning count and percentage that nothing
# recomputed and nothing alarmed on as they aged (#3274), and
# `scripts/check-lint-coverage.mjs` held a copy of the same number that would
# have gone stale on its own clock. The order of magnitude is the whole
# argument; the integer never was.
on:
push:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'content/**'
- 'docs/**'
- '.changeset/**'
# No `paths-ignore` here any more (objectui#3523, step 2) — it skipped the
# whole workflow on a docs-only / changeset-only PR, so the `Lint` context was
# absent exactly where a required check must still report. The path decision
# moved into the job below. `push` above keeps its copy: nothing judges a push
# to `main`.
pull_request:
branches: [main, develop]
# ── Merge queue (objectui#3523) ────────────────────────────────────────
# The merge queue is ENFORCED on this repository by a ruleset — a direct push
# to `main` returns 405 `Changes must be made through the merge queue`
# (measured in #3243). Until this trigger landed, not one of the repository's
# workflows subscribed `merge_group`: repo-wide `event=merge_group` runs stood
# at total_count = 0, historically. A queue with nothing subscribed to it can
# only have an EMPTY required-check set, so it rebuilt each PR on the current
# `main` and let it through without validating anything.
#
# That is not a theoretical hole; it was cashed in on 2026-08-07. #3498 landed
# a `scripts/` type gate, itself fully green, that left a TS2578 on `main`;
# #3503, #3510 and #3516 then merged between 02:11Z and 02:15Z with `Type
# Check` at conclusion=failure, and #3505 hot-fixed the result. objectstack
# went through the same frames (objectstack#6067 -> #5615).
#
# `types:` is spelled out although `checks_requested` is the ONLY activity
# type GitHub defines for `merge_group` today — the two spellings are
# equivalent right now (objectstack's `ci.yml` and `lint.yml` use the bare
# `merge_group:` form and produce queue builds normally, 3552 of them). Naming
# the type means a second activity type added later cannot silently start
# queue builds this workflow was never written for.
#
# `concurrency` below needs no merge-queue special case, and that was checked
# rather than assumed: on `merge_group` the `github.event.pull_request` half of
# the group expression is null, so the group falls back to `github.ref`, which
# on a queue build is the queue's own generation — measured on objectstack,
# `gh-readonly-queue/main/pr-6594-251e888ac9ace8226f3a8450951e5b40a0a84c2c`.
# It can collide with neither a pull-request group (a bare PR number) nor a
# push group (`refs/heads/main`), so a queue build and the PR build it came
# from never cancel each other.
merge_group:
types: [checks_requested]
workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true
# `fetch-depth: 0` for the gate step below (objectui#3523): it diffs
# against the merge base, which a depth-1 clone cannot resolve.
fetch-depth: 0
# ── Always report; run only when it matters (objectui#3523) ──────────
# `on.pull_request.paths-ignore` used to skip this whole workflow on a
# docs-only or changeset-only pull request, so the `Lint` context was
# simply absent there — and a required check that never reports leaves the
# PR pending forever (in the merge queue, until the ruleset's 60-minute
# timeout fails it). The filter moved from the trigger into the job: the
# job always runs and always reports, the paths decide only whether the
# expensive steps execute. `ci.yml`'s `docs` job is the in-repo precedent
# for the shape, and its `type-check` job carries the long version of this
# note. The list below IS the `paths-ignore` it replaced; the `push`
# trigger keeps its copy, because nothing judges a push to `main`.
#
# Fails OPEN: if the diff cannot be computed the job runs everything,
# rather than reporting green having linted nothing (objectstack#4928).
- name: Decide whether this change needs a full run
id: relevant
run: |
if [ "${{ github.event_name }}" != 'pull_request' ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Not a pull request: push is filtered at the trigger, and a merge_group build is the last validation before main. Running everything.'
exit 0
fi
if ! CHANGED=$(git diff --name-only \
'${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}' -- \
. \
':(exclude,glob)**/*.md' \
':(exclude,glob)content/**' \
':(exclude,glob)docs/**' \
':(exclude,glob).changeset/**'); then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Could not diff against the merge base. Running everything rather than skipping silently.'
exit 0
fi
if [ -n "$CHANGED" ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo "$CHANGED"
else
echo 'should_run=false' >> "$GITHUB_OUTPUT"
echo 'Only ignored paths changed. Skipping the steps below; this check still reports.'
fi
- name: Enable Corepack
if: steps.relevant.outputs.should_run == 'true'
run: corepack enable
- name: Verify pnpm version
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --version
- name: Setup Node.js
if: steps.relevant.outputs.should_run == 'true'
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# Every package must run ESLint or be declared a known gap. turbo skips
# scriptless packages silently, so without this a package reads as clean
# because nothing linted it. Runs before install: only reads package.json.
- name: Verify lint coverage
if: steps.relevant.outputs.should_run == 'true'
run: node scripts/check-lint-coverage.mjs
# ── One entry guard, one predicate (objectui#6092) ───────────────────
# A `scripts/**` CLI has to answer "did node run me, or did something
# import me?" before it does anything, and the hand-typed answers in this
# tree had drifted into NINE spellings across 28 `.mjs` files. Every one
# of them fails the same way: node resolves symlinks for the module graph
# but leaves `process.argv[1]` as the caller typed it, so a script reached
# through a symlink compares two different paths, answers false, and does
# NOTHING — exit 0, no output. The wrappers that spawn these tools hold
# `result.status` only, so an inert child is a green gate. Measured on a
# real blocking gate in this repository (objectui#6078):
# `check-skills-paths.mjs` run directly on a deliberately broken tree
# exits 1 with 696 bytes naming the dead path; run through a symlink
# against the SAME tree it exits 0 with no output.
#
# `scripts/invoked-as.mjs` landed here in #5984 with a header stating that
# a gate enforced the single-spelling rule. It did not exist, and the
# sweep it described had not happened either (objectui#6078). This is that
# gate. It lands BEFORE the conversion on purpose: the worklist grew while
# the card sat open, so a sweep with nothing under it can be undone
# silently by the next pull request. The 29 existing guards are baselined,
# SHRINK-ONLY, and the script's own header says why that is safe.
#
# Runs before install, next to `check-lint-coverage.mjs` above and for the
# same reason: it reads sources with no dependency beyond node builtins
# and two local modules, so nothing it needs is in `node_modules`. Placed
# before `pnpm install` it also cannot be made green by an install
# failure. `--self-test` runs FIRST and is the half that stops the gate
# rotting into decoration — it drives the scanner over fixture sources,
# including the nine spellings measured in this tree, and pins the
# baseline in every direction it can move.
#
# Invoked as `node` rather than through the `pnpm check:entry-guard` alias
# for the pre-install placement, matching `check-lint-coverage.mjs` above
# and `check-cross-repo-closer-outcome.mjs` below. The alias exists in
# `package.json` for local use and `scripts/__tests__/entry-guard-wiring.test.ts`
# holds the two spellings to the same script.
- name: Verify every scripts/ entry guard goes through one predicate
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-entry-guard.mjs --self-test
node scripts/check-entry-guard.mjs
# ── The ported objectstack tooling is a PINNED copy (objectui#6642) ───
# `scripts/pm/check-half-states.mjs` came from objectstack (objectui#5791)
# under a workflow header calling it a verbatim copy and enumerating the
# three things a re-sync must not clobber. Nothing checked either half.
# Measured 2026-08-28, before this step existed: the ported copy stood at
# 9,340 lines against upstream's 12,948 — a 4,637-line `diff` — and its
# own `--self-test` ran 1,116 cases where upstream's ran 1,574. So 458
# predicate cases had landed upstream and never arrived here, while the
# patrol went on rendering a confident report with the corresponding rows
# simply missing.
#
# The direction of harm is this repository's least visible one: a drifted
# copy does not fail, it REPORTS. It became load-bearing once already —
# objectui#6641 had to hand-port H22's closure floor into this copy,
# because wiring the new environment variable in the workflow alone would
# have set a variable this copy did not read.
#
# The gate reverses the DECLARED divergences out of each ported file and
# requires the reconstruction to hash to the pinned upstream digest, so
# drift beyond the declared set is byte-detectable in both directions —
# an edit here, or upstream moving. ⛔ It fetches nothing: a gate that
# reached api.github.com would be red on a network hiccup and green on a
# cached 200, and this repo's whole reason for owning a patrol is that a
# check which cannot read its input must never read as clean (#4690).
#
# Runs before install, next to the two gates above and for the same
# reason: node builtins and one local module only, so an install failure
# cannot take it down with it. `--self-test` runs FIRST — it drives the
# real comparer over fixtures (parity holds, drift outside a region,
# drift inside one, an ambiguous anchor, the pin-bump procedure, and
# every malformed-pin shape), which is what stops a comparer that
# recognises nothing from reading as a clean tree.
- name: Verify the ported objectstack tooling still matches its pin
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-upstream-port-parity.mjs --self-test
node scripts/check-upstream-port-parity.mjs
- name: Turbo Cache
if: steps.relevant.outputs.should_run == 'true'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
if: steps.relevant.outputs.should_run == 'true'
run: pnpm install --frozen-lockfile
- name: Run linter
if: steps.relevant.outputs.should_run == 'true'
run: pnpm lint
# ── The cross-repo closer's outcome contract (#5261) ──────────────────
# `cross-repo-issue-closer.yml` carries ~250 lines of inline
# github-script, and until this step existed it was code nobody had ever
# seen run: it fires only on a merge, its conclusion is required by
# nothing, and every one of its runs so far has been green. That last part
# is the problem rather than the reassurance — measured over every merged
# pull request in this repository, its close loop has had a live target
# roughly one and a half times a day since it landed, took the same
# `already closed -- skipping` exit every time, and left no backlink on
# any of them. Greenly. The header of the workflow carries the figures.
#
# This step is the exercise: the shipped script is extracted from the YAML
# with a real parser (never retyped) and run under doubles the way
# actions/github-script runs it, as one AsyncFunction body. The scenarios
# pin the target parse, the target KIND, and the outcome of every exit —
# which of setFailed / warning / job summary fires, and which API calls
# were made.
#
# Assertion 0 is the compile, and it is not theoretical: the framework's
# copy of this workflow was taken down twice in one day by a `SyntaxError`
# in the inline block, i.e. a script that never ran at all, on a
# post-merge workflow whose red nothing else in CI can see.
#
# `--self-test` runs FIRST and is the half that stops the battery rotting
# into decoration: it mutates the shipped script — downgrade the verdict
# to a warning, break out of the loop instead of isolating, drop the
# same-repo skip, collapse the already-closed branch, strip the backlink
# marker, drop the pull-request guard, and the rest — and requires the
# battery to go RED for each, naming the scenario that catches it. Neither
# the mutations nor the scenarios are counted here: a hand-copied
# enumeration drifts by construction, which is the lesson this workflow's
# own header records. A mutation whose anchor no longer exists is a
# failure too, so rewriting the workflow cannot leave them silently
# matching nothing.
#
# Invoked as `node` rather than through a `pnpm check:*` alias, matching
# `check-lint-coverage.mjs` above. No network, no build; well under a
# second.
- name: Cross-repo closer outcome contract
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-cross-repo-closer-outcome.mjs --self-test
node scripts/check-cross-repo-closer-outcome.mjs
# ── The product's own check command, on the product's own tree ────────
# `objectui check` is what a consumer runs against their schema tree, and
# the root `check` script points that same command at this repository.
# Nothing ran it. It exited 1 with 64 errors on `main` — and had done
# since the first `tsconfig.json` grew a comment — until someone ran it by
# hand while measuring something unrelated (objectui#5237, fixed by
# #5245). A shipped command sitting red on its own repository is the
# dogfood invariant failing, and the reason it could sit there is that no
# gate ever asked. This is that gate (objectui#5246).
#
# The build step below is not a convenience. The root script is
# `node packages/cli/dist/cli.js check`, and this job installs without
# building, so without it the step dies on a missing file. That failure
# mode is worse than no gate at all: it is red for a reason that has
# nothing to do with the tree being checked, and the obvious repair from
# outside is to delete the step — leaving the hole exactly as it was, now
# with a commit saying it was considered.
#
# `...` is pnpm's dependency closure: the CLI plus every workspace
# package it depends on, in topological order, derived from the graph
# rather than listed here. The closure is the requirement and not just
# tidiness — `dist/cli.js` imports `@object-ui/types`' built output at
# startup, so building the CLI package alone produces a binary that
# cannot load (objectui#5237 records the same import as the reproduce
# recipe's first step).
#
# Deliberately NOT `turbo run build`, although the Turbo cache above is
# restored by this point and would usually make it free. A turbo cache
# HIT restores a task's recorded outputs, and an entry recorded with an
# empty output set replays as "cache hit, replaying logs" plus FULL
# TURBO while writing no `dist/` at all — measured on this repo, where
# the CLI then died with ERR_MODULE_NOT_FOUND on the types import above.
# A blocking gate must not be able to fail for a reason that lives in a
# cache rather than in the tree it is judging, which is the same argument
# as the paragraph above one level down. Building through pnpm has no
# cache layer to replay, and it costs well under a minute.
- name: Build the CLI the self-check runs
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --filter '@object-ui/cli...' build
# Errors only, which is the command's existing behaviour rather than a
# setting chosen here: a parse failure increments the error count, a
# non-zero count is the only thing that exits 1, and the unknown-schema-
# type arm prints and moves on. So this step blocks on the same arm
# `pnpm lint` above does, and for the same reason — errors are a signal,
# the warning stream is known debt. Nothing here promotes those warnings
# to failures, and no output-suppressing flag hides them either: they stay
# visible in the log and non-blocking. That arm belongs to objectui#5127,
# which is open; whatever it settles changes what this step PRINTS, never
# what it fails on. No count is quoted, for the reason the `--max-warnings`
# paragraph in the header gives.
- name: Verify the CLI's own check command passes on this repository
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore(ci): slow both dependabot channels from weekly to monthly #5366

chore(ci): slow both dependabot channels from weekly to monthly

chore(ci): slow both dependabot channels from weekly to monthly #5366

Workflow file for this run

name: Lint
# Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never
# gated a PR. That mattered more than it looked: every `object-ui/*` rule that
# `eslint.config.js` sets to `error` is a ratchet — added *specifically* so a
# new violation fails CI, with its existing sites pre-cleaned first so the rule
# lints clean on the day it lands. While nothing ran them, every one of them was
# inert.
#
# `eslint.config.js` is the single list of those rules, and this comment
# deliberately neither counts them nor names them: it used to hand-count, and
# the count was stale by the time anyone read it (#3261). A hand-copied
# enumeration drifts by construction, and a stale one still reads as
# authoritative — `content/docs/guide/ci-cd-pipeline.md` avoids the number for
# the same reason. `scripts/__tests__/lint-workflow.test.ts` holds that in
# place: it fails if a count or a rule name reappears here, if the config stops
# setting any `object-ui/*` rule to `error`, or if this workflow stops gating
# pull requests.
#
# `--max-warnings` is deliberately not set: warnings repo-wide run into the
# thousands, dominated by `no-explicit-any` plus React Compiler rules the config
# downgrades on purpose — known historical debt, not a signal. This gate is
# about errors. No exact figure here, for the reason above: this paragraph used
# to carry a hand-maintained warning count and percentage that nothing
# recomputed and nothing alarmed on as they aged (#3274), and
# `scripts/check-lint-coverage.mjs` held a copy of the same number that would
# have gone stale on its own clock. The order of magnitude is the whole
# argument; the integer never was.
on:
push:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'content/**'
- 'docs/**'
- '.changeset/**'
# No `paths-ignore` here any more (objectui#3523, step 2) — it skipped the
# whole workflow on a docs-only / changeset-only PR, so the `Lint` context was
# absent exactly where a required check must still report. The path decision
# moved into the job below. `push` above keeps its copy: nothing judges a push
# to `main`.
pull_request:
branches: [main, develop]
# ── Merge queue (objectui#3523) ────────────────────────────────────────
# The merge queue is ENFORCED on this repository by a ruleset — a direct push
# to `main` returns 405 `Changes must be made through the merge queue`
# (measured in #3243). Until this trigger landed, not one of the repository's
# workflows subscribed `merge_group`: repo-wide `event=merge_group` runs stood
# at total_count = 0, historically. A queue with nothing subscribed to it can
# only have an EMPTY required-check set, so it rebuilt each PR on the current
# `main` and let it through without validating anything.
#
# That is not a theoretical hole; it was cashed in on 2026-08-07. #3498 landed
# a `scripts/` type gate, itself fully green, that left a TS2578 on `main`;
# #3503, #3510 and #3516 then merged between 02:11Z and 02:15Z with `Type
# Check` at conclusion=failure, and #3505 hot-fixed the result. objectstack
# went through the same frames (objectstack#6067 -> #5615).
#
# `types:` is spelled out although `checks_requested` is the ONLY activity
# type GitHub defines for `merge_group` today — the two spellings are
# equivalent right now (objectstack's `ci.yml` and `lint.yml` use the bare
# `merge_group:` form and produce queue builds normally, 3552 of them). Naming
# the type means a second activity type added later cannot silently start
# queue builds this workflow was never written for.
#
# `concurrency` below needs no merge-queue special case, and that was checked
# rather than assumed: on `merge_group` the `github.event.pull_request` half of
# the group expression is null, so the group falls back to `github.ref`, which
# on a queue build is the queue's own generation — measured on objectstack,
# `gh-readonly-queue/main/pr-6594-251e888ac9ace8226f3a8450951e5b40a0a84c2c`.
# It can collide with neither a pull-request group (a bare PR number) nor a
# push group (`refs/heads/main`), so a queue build and the PR build it came
# from never cancel each other.
merge_group:
types: [checks_requested]
workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true
# `fetch-depth: 0` for the gate step below (objectui#3523): it diffs
# against the merge base, which a depth-1 clone cannot resolve.
fetch-depth: 0
# ── Always report; run only when it matters (objectui#3523) ──────────
# `on.pull_request.paths-ignore` used to skip this whole workflow on a
# docs-only or changeset-only pull request, so the `Lint` context was
# simply absent there — and a required check that never reports leaves the
# PR pending forever (in the merge queue, until the ruleset's 60-minute
# timeout fails it). The filter moved from the trigger into the job: the
# job always runs and always reports, the paths decide only whether the
# expensive steps execute. `ci.yml`'s `docs` job is the in-repo precedent
# for the shape, and its `type-check` job carries the long version of this
# note. The list below IS the `paths-ignore` it replaced; the `push`
# trigger keeps its copy, because nothing judges a push to `main`.
#
# Fails OPEN: if the diff cannot be computed the job runs everything,
# rather than reporting green having linted nothing (objectstack#4928).
- name: Decide whether this change needs a full run
id: relevant
run: |
if [ "${{ github.event_name }}" != 'pull_request' ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Not a pull request: push is filtered at the trigger, and a merge_group build is the last validation before main. Running everything.'
exit 0
fi
if ! CHANGED=$(git diff --name-only \
'${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}' -- \
. \
':(exclude,glob)**/*.md' \
':(exclude,glob)content/**' \
':(exclude,glob)docs/**' \
':(exclude,glob).changeset/**'); then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Could not diff against the merge base. Running everything rather than skipping silently.'
exit 0
fi
if [ -n "$CHANGED" ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo "$CHANGED"
else
echo 'should_run=false' >> "$GITHUB_OUTPUT"
echo 'Only ignored paths changed. Skipping the steps below; this check still reports.'
fi
- name: Enable Corepack
if: steps.relevant.outputs.should_run == 'true'
run: corepack enable
- name: Verify pnpm version
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --version
- name: Setup Node.js
if: steps.relevant.outputs.should_run == 'true'
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# Every package must run ESLint or be declared a known gap. turbo skips
# scriptless packages silently, so without this a package reads as clean
# because nothing linted it. Runs before install: only reads package.json.
- name: Verify lint coverage
if: steps.relevant.outputs.should_run == 'true'
run: node scripts/check-lint-coverage.mjs
# ── One entry guard, one predicate (objectui#6092) ───────────────────
# A `scripts/**` CLI has to answer "did node run me, or did something
# import me?" before it does anything, and the hand-typed answers in this
# tree had drifted into NINE spellings across 28 `.mjs` files. Every one
# of them fails the same way: node resolves symlinks for the module graph
# but leaves `process.argv[1]` as the caller typed it, so a script reached
# through a symlink compares two different paths, answers false, and does
# NOTHING — exit 0, no output. The wrappers that spawn these tools hold
# `result.status` only, so an inert child is a green gate. Measured on a
# real blocking gate in this repository (objectui#6078):
# `check-skills-paths.mjs` run directly on a deliberately broken tree
# exits 1 with 696 bytes naming the dead path; run through a symlink
# against the SAME tree it exits 0 with no output.
#
# `scripts/invoked-as.mjs` landed here in #5984 with a header stating that
# a gate enforced the single-spelling rule. It did not exist, and the
# sweep it described had not happened either (objectui#6078). This is that
# gate. It lands BEFORE the conversion on purpose: the worklist grew while
# the card sat open, so a sweep with nothing under it can be undone
# silently by the next pull request. The 29 existing guards are baselined,
# SHRINK-ONLY, and the script's own header says why that is safe.
#
# Runs before install, next to `check-lint-coverage.mjs` above and for the
# same reason: it reads sources with no dependency beyond node builtins
# and two local modules, so nothing it needs is in `node_modules`. Placed
# before `pnpm install` it also cannot be made green by an install
# failure. `--self-test` runs FIRST and is the half that stops the gate
# rotting into decoration — it drives the scanner over fixture sources,
# including the nine spellings measured in this tree, and pins the
# baseline in every direction it can move.
#
# Invoked as `node` rather than through the `pnpm check:entry-guard` alias
# for the pre-install placement, matching `check-lint-coverage.mjs` above
# and `check-cross-repo-closer-outcome.mjs` below. The alias exists in
# `package.json` for local use and `scripts/__tests__/entry-guard-wiring.test.ts`
# holds the two spellings to the same script.
- name: Verify every scripts/ entry guard goes through one predicate
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-entry-guard.mjs --self-test
node scripts/check-entry-guard.mjs
# ── The ported objectstack tooling is a PINNED copy (objectui#6642) ───
# `scripts/pm/check-half-states.mjs` came from objectstack (objectui#5791)
# under a workflow header calling it a verbatim copy and enumerating the
# three things a re-sync must not clobber. Nothing checked either half.
# Measured 2026-08-28, before this step existed: the ported copy stood at
# 9,340 lines against upstream's 12,948 — a 4,637-line `diff` — and its
# own `--self-test` ran 1,116 cases where upstream's ran 1,574. So 458
# predicate cases had landed upstream and never arrived here, while the
# patrol went on rendering a confident report with the corresponding rows
# simply missing.
#
# The direction of harm is this repository's least visible one: a drifted
# copy does not fail, it REPORTS. It became load-bearing once already —
# objectui#6641 had to hand-port H22's closure floor into this copy,
# because wiring the new environment variable in the workflow alone would
# have set a variable this copy did not read.
#
# The gate reverses the DECLARED divergences out of each ported file and
# requires the reconstruction to hash to the pinned upstream digest, so
# drift beyond the declared set is byte-detectable in both directions —
# an edit here, or upstream moving. ⛔ It fetches nothing: a gate that
# reached api.github.com would be red on a network hiccup and green on a
# cached 200, and this repo's whole reason for owning a patrol is that a
# check which cannot read its input must never read as clean (#4690).
#
# Runs before install, next to the two gates above and for the same
# reason: node builtins and one local module only, so an install failure
# cannot take it down with it. `--self-test` runs FIRST — it drives the
# real comparer over fixtures (parity holds, drift outside a region,
# drift inside one, an ambiguous anchor, the pin-bump procedure, and
# every malformed-pin shape), which is what stops a comparer that
# recognises nothing from reading as a clean tree.
- name: Verify the ported objectstack tooling still matches its pin
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-upstream-port-parity.mjs --self-test
node scripts/check-upstream-port-parity.mjs
- name: Turbo Cache
if: steps.relevant.outputs.should_run == 'true'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
if: steps.relevant.outputs.should_run == 'true'
run: pnpm install --frozen-lockfile
- name: Run linter
if: steps.relevant.outputs.should_run == 'true'
run: pnpm lint
# ── The cross-repo closer's outcome contract (#5261) ──────────────────
# `cross-repo-issue-closer.yml` carries ~250 lines of inline
# github-script, and until this step existed it was code nobody had ever
# seen run: it fires only on a merge, its conclusion is required by
# nothing, and every one of its runs so far has been green. That last part
# is the problem rather than the reassurance — measured over every merged
# pull request in this repository, its close loop has had a live target
# roughly one and a half times a day since it landed, took the same
# `already closed -- skipping` exit every time, and left no backlink on
# any of them. Greenly. The header of the workflow carries the figures.
#
# This step is the exercise: the shipped script is extracted from the YAML
# with a real parser (never retyped) and run under doubles the way
# actions/github-script runs it, as one AsyncFunction body. The scenarios
# pin the target parse, the target KIND, and the outcome of every exit —
# which of setFailed / warning / job summary fires, and which API calls
# were made.
#
# Assertion 0 is the compile, and it is not theoretical: the framework's
# copy of this workflow was taken down twice in one day by a `SyntaxError`
# in the inline block, i.e. a script that never ran at all, on a
# post-merge workflow whose red nothing else in CI can see.
#
# `--self-test` runs FIRST and is the half that stops the battery rotting
# into decoration: it mutates the shipped script — downgrade the verdict
# to a warning, break out of the loop instead of isolating, drop the
# same-repo skip, collapse the already-closed branch, strip the backlink
# marker, drop the pull-request guard, and the rest — and requires the
# battery to go RED for each, naming the scenario that catches it. Neither
# the mutations nor the scenarios are counted here: a hand-copied
# enumeration drifts by construction, which is the lesson this workflow's
# own header records. A mutation whose anchor no longer exists is a
# failure too, so rewriting the workflow cannot leave them silently
# matching nothing.
#
# Invoked as `node` rather than through a `pnpm check:*` alias, matching
# `check-lint-coverage.mjs` above. No network, no build; well under a
# second.
- name: Cross-repo closer outcome contract
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-cross-repo-closer-outcome.mjs --self-test
node scripts/check-cross-repo-closer-outcome.mjs
# ── The product's own check command, on the product's own tree ────────
# `objectui check` is what a consumer runs against their schema tree, and
# the root `check` script points that same command at this repository.
# Nothing ran it. It exited 1 with 64 errors on `main` — and had done
# since the first `tsconfig.json` grew a comment — until someone ran it by
# hand while measuring something unrelated (objectui#5237, fixed by
# #5245). A shipped command sitting red on its own repository is the
# dogfood invariant failing, and the reason it could sit there is that no
# gate ever asked. This is that gate (objectui#5246).
#
# The build step below is not a convenience. The root script is
# `node packages/cli/dist/cli.js check`, and this job installs without
# building, so without it the step dies on a missing file. That failure
# mode is worse than no gate at all: it is red for a reason that has
# nothing to do with the tree being checked, and the obvious repair from
# outside is to delete the step — leaving the hole exactly as it was, now
# with a commit saying it was considered.
#
# `...` is pnpm's dependency closure: the CLI plus every workspace
# package it depends on, in topological order, derived from the graph
# rather than listed here. The closure is the requirement and not just
# tidiness — `dist/cli.js` imports `@object-ui/types`' built output at
# startup, so building the CLI package alone produces a binary that
# cannot load (objectui#5237 records the same import as the reproduce
# recipe's first step).
#
# Deliberately NOT `turbo run build`, although the Turbo cache above is
# restored by this point and would usually make it free. A turbo cache
# HIT restores a task's recorded outputs, and an entry recorded with an
# empty output set replays as "cache hit, replaying logs" plus FULL
# TURBO while writing no `dist/` at all — measured on this repo, where
# the CLI then died with ERR_MODULE_NOT_FOUND on the types import above.
# A blocking gate must not be able to fail for a reason that lives in a
# cache rather than in the tree it is judging, which is the same argument
# as the paragraph above one level down. Building through pnpm has no
# cache layer to replay, and it costs well under a minute.
- name: Build the CLI the self-check runs
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --filter '@object-ui/cli...' build
# Errors only, which is the command's existing behaviour rather than a
# setting chosen here: a parse failure increments the error count, a
# non-zero count is the only thing that exits 1, and the unknown-schema-
# type arm prints and moves on. So this step blocks on the same arm
# `pnpm lint` above does, and for the same reason — errors are a signal,
# the warning stream is known debt. Nothing here promotes those warnings
# to failures, and no output-suppressing flag hides them either: they stay
# visible in the log and non-blocking. That arm belongs to objectui#5127,
# which is open; whatever it settles changes what this step PRINTS, never
# what it fails on. No count is quoted, for the reason the `--max-warnings`
# paragraph in the header gives.
- name: Verify the CLI's own check command passes on this repository
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

chore(ci): slow both dependabot channels from weekly to monthly #5366

chore(ci): slow both dependabot channels from weekly to monthly

chore(ci): slow both dependabot channels from weekly to monthly #5366

Workflow file for this run

name: Lint
# Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never
# gated a PR. That mattered more than it looked: every `object-ui/*` rule that
# `eslint.config.js` sets to `error` is a ratchet — added *specifically* so a
# new violation fails CI, with its existing sites pre-cleaned first so the rule
# lints clean on the day it lands. While nothing ran them, every one of them was
# inert.
#
# `eslint.config.js` is the single list of those rules, and this comment
# deliberately neither counts them nor names them: it used to hand-count, and
# the count was stale by the time anyone read it (#3261). A hand-copied
# enumeration drifts by construction, and a stale one still reads as
# authoritative — `content/docs/guide/ci-cd-pipeline.md` avoids the number for
# the same reason. `scripts/__tests__/lint-workflow.test.ts` holds that in
# place: it fails if a count or a rule name reappears here, if the config stops
# setting any `object-ui/*` rule to `error`, or if this workflow stops gating
# pull requests.
#
# `--max-warnings` is deliberately not set: warnings repo-wide run into the
# thousands, dominated by `no-explicit-any` plus React Compiler rules the config
# downgrades on purpose — known historical debt, not a signal. This gate is
# about errors. No exact figure here, for the reason above: this paragraph used
# to carry a hand-maintained warning count and percentage that nothing
# recomputed and nothing alarmed on as they aged (#3274), and
# `scripts/check-lint-coverage.mjs` held a copy of the same number that would
# have gone stale on its own clock. The order of magnitude is the whole
# argument; the integer never was.
on:
push:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'content/**'
- 'docs/**'
- '.changeset/**'
# No `paths-ignore` here any more (objectui#3523, step 2) — it skipped the
# whole workflow on a docs-only / changeset-only PR, so the `Lint` context was
# absent exactly where a required check must still report. The path decision
# moved into the job below. `push` above keeps its copy: nothing judges a push
# to `main`.
pull_request:
branches: [main, develop]
# ── Merge queue (objectui#3523) ────────────────────────────────────────
# The merge queue is ENFORCED on this repository by a ruleset — a direct push
# to `main` returns 405 `Changes must be made through the merge queue`
# (measured in #3243). Until this trigger landed, not one of the repository's
# workflows subscribed `merge_group`: repo-wide `event=merge_group` runs stood
# at total_count = 0, historically. A queue with nothing subscribed to it can
# only have an EMPTY required-check set, so it rebuilt each PR on the current
# `main` and let it through without validating anything.
#
# That is not a theoretical hole; it was cashed in on 2026-08-07. #3498 landed
# a `scripts/` type gate, itself fully green, that left a TS2578 on `main`;
# #3503, #3510 and #3516 then merged between 02:11Z and 02:15Z with `Type
# Check` at conclusion=failure, and #3505 hot-fixed the result. objectstack
# went through the same frames (objectstack#6067 -> #5615).
#
# `types:` is spelled out although `checks_requested` is the ONLY activity
# type GitHub defines for `merge_group` today — the two spellings are
# equivalent right now (objectstack's `ci.yml` and `lint.yml` use the bare
# `merge_group:` form and produce queue builds normally, 3552 of them). Naming
# the type means a second activity type added later cannot silently start
# queue builds this workflow was never written for.
#
# `concurrency` below needs no merge-queue special case, and that was checked
# rather than assumed: on `merge_group` the `github.event.pull_request` half of
# the group expression is null, so the group falls back to `github.ref`, which
# on a queue build is the queue's own generation — measured on objectstack,
# `gh-readonly-queue/main/pr-6594-251e888ac9ace8226f3a8450951e5b40a0a84c2c`.
# It can collide with neither a pull-request group (a bare PR number) nor a
# push group (`refs/heads/main`), so a queue build and the PR build it came
# from never cancel each other.
merge_group:
types: [checks_requested]
workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true
# `fetch-depth: 0` for the gate step below (objectui#3523): it diffs
# against the merge base, which a depth-1 clone cannot resolve.
fetch-depth: 0
# ── Always report; run only when it matters (objectui#3523) ──────────
# `on.pull_request.paths-ignore` used to skip this whole workflow on a
# docs-only or changeset-only pull request, so the `Lint` context was
# simply absent there — and a required check that never reports leaves the
# PR pending forever (in the merge queue, until the ruleset's 60-minute
# timeout fails it). The filter moved from the trigger into the job: the
# job always runs and always reports, the paths decide only whether the
# expensive steps execute. `ci.yml`'s `docs` job is the in-repo precedent
# for the shape, and its `type-check` job carries the long version of this
# note. The list below IS the `paths-ignore` it replaced; the `push`
# trigger keeps its copy, because nothing judges a push to `main`.
#
# Fails OPEN: if the diff cannot be computed the job runs everything,
# rather than reporting green having linted nothing (objectstack#4928).
- name: Decide whether this change needs a full run
id: relevant
run: |
if [ "${{ github.event_name }}" != 'pull_request' ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Not a pull request: push is filtered at the trigger, and a merge_group build is the last validation before main. Running everything.'
exit 0
fi
if ! CHANGED=$(git diff --name-only \
'${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}' -- \
. \
':(exclude,glob)**/*.md' \
':(exclude,glob)content/**' \
':(exclude,glob)docs/**' \
':(exclude,glob).changeset/**'); then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Could not diff against the merge base. Running everything rather than skipping silently.'
exit 0
fi
if [ -n "$CHANGED" ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo "$CHANGED"
else
echo 'should_run=false' >> "$GITHUB_OUTPUT"
echo 'Only ignored paths changed. Skipping the steps below; this check still reports.'
fi
- name: Enable Corepack
if: steps.relevant.outputs.should_run == 'true'
run: corepack enable
- name: Verify pnpm version
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --version
- name: Setup Node.js
if: steps.relevant.outputs.should_run == 'true'
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# Every package must run ESLint or be declared a known gap. turbo skips
# scriptless packages silently, so without this a package reads as clean
# because nothing linted it. Runs before install: only reads package.json.
- name: Verify lint coverage
if: steps.relevant.outputs.should_run == 'true'
run: node scripts/check-lint-coverage.mjs
# ── One entry guard, one predicate (objectui#6092) ───────────────────
# A `scripts/**` CLI has to answer "did node run me, or did something
# import me?" before it does anything, and the hand-typed answers in this
# tree had drifted into NINE spellings across 28 `.mjs` files. Every one
# of them fails the same way: node resolves symlinks for the module graph
# but leaves `process.argv[1]` as the caller typed it, so a script reached
# through a symlink compares two different paths, answers false, and does
# NOTHING — exit 0, no output. The wrappers that spawn these tools hold
# `result.status` only, so an inert child is a green gate. Measured on a
# real blocking gate in this repository (objectui#6078):
# `check-skills-paths.mjs` run directly on a deliberately broken tree
# exits 1 with 696 bytes naming the dead path; run through a symlink
# against the SAME tree it exits 0 with no output.
#
# `scripts/invoked-as.mjs` landed here in #5984 with a header stating that
# a gate enforced the single-spelling rule. It did not exist, and the
# sweep it described had not happened either (objectui#6078). This is that
# gate. It lands BEFORE the conversion on purpose: the worklist grew while
# the card sat open, so a sweep with nothing under it can be undone
# silently by the next pull request. The 29 existing guards are baselined,
# SHRINK-ONLY, and the script's own header says why that is safe.
#
# Runs before install, next to `check-lint-coverage.mjs` above and for the
# same reason: it reads sources with no dependency beyond node builtins
# and two local modules, so nothing it needs is in `node_modules`. Placed
# before `pnpm install` it also cannot be made green by an install
# failure. `--self-test` runs FIRST and is the half that stops the gate
# rotting into decoration — it drives the scanner over fixture sources,
# including the nine spellings measured in this tree, and pins the
# baseline in every direction it can move.
#
# Invoked as `node` rather than through the `pnpm check:entry-guard` alias
# for the pre-install placement, matching `check-lint-coverage.mjs` above
# and `check-cross-repo-closer-outcome.mjs` below. The alias exists in
# `package.json` for local use and `scripts/__tests__/entry-guard-wiring.test.ts`
# holds the two spellings to the same script.
- name: Verify every scripts/ entry guard goes through one predicate
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-entry-guard.mjs --self-test
node scripts/check-entry-guard.mjs
# ── The ported objectstack tooling is a PINNED copy (objectui#6642) ───
# `scripts/pm/check-half-states.mjs` came from objectstack (objectui#5791)
# under a workflow header calling it a verbatim copy and enumerating the
# three things a re-sync must not clobber. Nothing checked either half.
# Measured 2026-08-28, before this step existed: the ported copy stood at
# 9,340 lines against upstream's 12,948 — a 4,637-line `diff` — and its
# own `--self-test` ran 1,116 cases where upstream's ran 1,574. So 458
# predicate cases had landed upstream and never arrived here, while the
# patrol went on rendering a confident report with the corresponding rows
# simply missing.
#
# The direction of harm is this repository's least visible one: a drifted
# copy does not fail, it REPORTS. It became load-bearing once already —
# objectui#6641 had to hand-port H22's closure floor into this copy,
# because wiring the new environment variable in the workflow alone would
# have set a variable this copy did not read.
#
# The gate reverses the DECLARED divergences out of each ported file and
# requires the reconstruction to hash to the pinned upstream digest, so
# drift beyond the declared set is byte-detectable in both directions —
# an edit here, or upstream moving. ⛔ It fetches nothing: a gate that
# reached api.github.com would be red on a network hiccup and green on a
# cached 200, and this repo's whole reason for owning a patrol is that a
# check which cannot read its input must never read as clean (#4690).
#
# Runs before install, next to the two gates above and for the same
# reason: node builtins and one local module only, so an install failure
# cannot take it down with it. `--self-test` runs FIRST — it drives the
# real comparer over fixtures (parity holds, drift outside a region,
# drift inside one, an ambiguous anchor, the pin-bump procedure, and
# every malformed-pin shape), which is what stops a comparer that
# recognises nothing from reading as a clean tree.
- name: Verify the ported objectstack tooling still matches its pin
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-upstream-port-parity.mjs --self-test
node scripts/check-upstream-port-parity.mjs
- name: Turbo Cache
if: steps.relevant.outputs.should_run == 'true'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
if: steps.relevant.outputs.should_run == 'true'
run: pnpm install --frozen-lockfile
- name: Run linter
if: steps.relevant.outputs.should_run == 'true'
run: pnpm lint
# ── The cross-repo closer's outcome contract (#5261) ──────────────────
# `cross-repo-issue-closer.yml` carries ~250 lines of inline
# github-script, and until this step existed it was code nobody had ever
# seen run: it fires only on a merge, its conclusion is required by
# nothing, and every one of its runs so far has been green. That last part
# is the problem rather than the reassurance — measured over every merged
# pull request in this repository, its close loop has had a live target
# roughly one and a half times a day since it landed, took the same
# `already closed -- skipping` exit every time, and left no backlink on
# any of them. Greenly. The header of the workflow carries the figures.
#
# This step is the exercise: the shipped script is extracted from the YAML
# with a real parser (never retyped) and run under doubles the way
# actions/github-script runs it, as one AsyncFunction body. The scenarios
# pin the target parse, the target KIND, and the outcome of every exit —
# which of setFailed / warning / job summary fires, and which API calls
# were made.
#
# Assertion 0 is the compile, and it is not theoretical: the framework's
# copy of this workflow was taken down twice in one day by a `SyntaxError`
# in the inline block, i.e. a script that never ran at all, on a
# post-merge workflow whose red nothing else in CI can see.
#
# `--self-test` runs FIRST and is the half that stops the battery rotting
# into decoration: it mutates the shipped script — downgrade the verdict
# to a warning, break out of the loop instead of isolating, drop the
# same-repo skip, collapse the already-closed branch, strip the backlink
# marker, drop the pull-request guard, and the rest — and requires the
# battery to go RED for each, naming the scenario that catches it. Neither
# the mutations nor the scenarios are counted here: a hand-copied
# enumeration drifts by construction, which is the lesson this workflow's
# own header records. A mutation whose anchor no longer exists is a
# failure too, so rewriting the workflow cannot leave them silently
# matching nothing.
#
# Invoked as `node` rather than through a `pnpm check:*` alias, matching
# `check-lint-coverage.mjs` above. No network, no build; well under a
# second.
- name: Cross-repo closer outcome contract
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-cross-repo-closer-outcome.mjs --self-test
node scripts/check-cross-repo-closer-outcome.mjs
# ── The product's own check command, on the product's own tree ────────
# `objectui check` is what a consumer runs against their schema tree, and
# the root `check` script points that same command at this repository.
# Nothing ran it. It exited 1 with 64 errors on `main` — and had done
# since the first `tsconfig.json` grew a comment — until someone ran it by
# hand while measuring something unrelated (objectui#5237, fixed by
# #5245). A shipped command sitting red on its own repository is the
# dogfood invariant failing, and the reason it could sit there is that no
# gate ever asked. This is that gate (objectui#5246).
#
# The build step below is not a convenience. The root script is
# `node packages/cli/dist/cli.js check`, and this job installs without
# building, so without it the step dies on a missing file. That failure
# mode is worse than no gate at all: it is red for a reason that has
# nothing to do with the tree being checked, and the obvious repair from
# outside is to delete the step — leaving the hole exactly as it was, now
# with a commit saying it was considered.
#
# `...` is pnpm's dependency closure: the CLI plus every workspace
# package it depends on, in topological order, derived from the graph
# rather than listed here. The closure is the requirement and not just
# tidiness — `dist/cli.js` imports `@object-ui/types`' built output at
# startup, so building the CLI package alone produces a binary that
# cannot load (objectui#5237 records the same import as the reproduce
# recipe's first step).
#
# Deliberately NOT `turbo run build`, although the Turbo cache above is
# restored by this point and would usually make it free. A turbo cache
# HIT restores a task's recorded outputs, and an entry recorded with an
# empty output set replays as "cache hit, replaying logs" plus FULL
# TURBO while writing no `dist/` at all — measured on this repo, where
# the CLI then died with ERR_MODULE_NOT_FOUND on the types import above.
# A blocking gate must not be able to fail for a reason that lives in a
# cache rather than in the tree it is judging, which is the same argument
# as the paragraph above one level down. Building through pnpm has no
# cache layer to replay, and it costs well under a minute.
- name: Build the CLI the self-check runs
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --filter '@object-ui/cli...' build
# Errors only, which is the command's existing behaviour rather than a
# setting chosen here: a parse failure increments the error count, a
# non-zero count is the only thing that exits 1, and the unknown-schema-
# type arm prints and moves on. So this step blocks on the same arm
# `pnpm lint` above does, and for the same reason — errors are a signal,
# the warning stream is known debt. Nothing here promotes those warnings
# to failures, and no output-suppressing flag hides them either: they stay
# visible in the log and non-blocking. That arm belongs to objectui#5127,
# which is open; whatever it settles changes what this step PRINTS, never
# what it fails on. No count is quoted, for the reason the `--max-warnings`
# paragraph in the header gives.
- name: Verify the CLI's own check command passes on this repository
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore(ci): slow both dependabot channels from weekly to monthly #5366

chore(ci): slow both dependabot channels from weekly to monthly

chore(ci): slow both dependabot channels from weekly to monthly #5366

Workflow file for this run

name: Lint
# Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never
# gated a PR. That mattered more than it looked: every `object-ui/*` rule that
# `eslint.config.js` sets to `error` is a ratchet — added *specifically* so a
# new violation fails CI, with its existing sites pre-cleaned first so the rule
# lints clean on the day it lands. While nothing ran them, every one of them was
# inert.
#
# `eslint.config.js` is the single list of those rules, and this comment
# deliberately neither counts them nor names them: it used to hand-count, and
# the count was stale by the time anyone read it (#3261). A hand-copied
# enumeration drifts by construction, and a stale one still reads as
# authoritative — `content/docs/guide/ci-cd-pipeline.md` avoids the number for
# the same reason. `scripts/__tests__/lint-workflow.test.ts` holds that in
# place: it fails if a count or a rule name reappears here, if the config stops
# setting any `object-ui/*` rule to `error`, or if this workflow stops gating
# pull requests.
#
# `--max-warnings` is deliberately not set: warnings repo-wide run into the
# thousands, dominated by `no-explicit-any` plus React Compiler rules the config
# downgrades on purpose — known historical debt, not a signal. This gate is
# about errors. No exact figure here, for the reason above: this paragraph used
# to carry a hand-maintained warning count and percentage that nothing
# recomputed and nothing alarmed on as they aged (#3274), and
# `scripts/check-lint-coverage.mjs` held a copy of the same number that would
# have gone stale on its own clock. The order of magnitude is the whole
# argument; the integer never was.
on:
push:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'content/**'
- 'docs/**'
- '.changeset/**'
# No `paths-ignore` here any more (objectui#3523, step 2) — it skipped the
# whole workflow on a docs-only / changeset-only PR, so the `Lint` context was
# absent exactly where a required check must still report. The path decision
# moved into the job below. `push` above keeps its copy: nothing judges a push
# to `main`.
pull_request:
branches: [main, develop]
# ── Merge queue (objectui#3523) ────────────────────────────────────────
# The merge queue is ENFORCED on this repository by a ruleset — a direct push
# to `main` returns 405 `Changes must be made through the merge queue`
# (measured in #3243). Until this trigger landed, not one of the repository's
# workflows subscribed `merge_group`: repo-wide `event=merge_group` runs stood
# at total_count = 0, historically. A queue with nothing subscribed to it can
# only have an EMPTY required-check set, so it rebuilt each PR on the current
# `main` and let it through without validating anything.
#
# That is not a theoretical hole; it was cashed in on 2026-08-07. #3498 landed
# a `scripts/` type gate, itself fully green, that left a TS2578 on `main`;
# #3503, #3510 and #3516 then merged between 02:11Z and 02:15Z with `Type
# Check` at conclusion=failure, and #3505 hot-fixed the result. objectstack
# went through the same frames (objectstack#6067 -> #5615).
#
# `types:` is spelled out although `checks_requested` is the ONLY activity
# type GitHub defines for `merge_group` today — the two spellings are
# equivalent right now (objectstack's `ci.yml` and `lint.yml` use the bare
# `merge_group:` form and produce queue builds normally, 3552 of them). Naming
# the type means a second activity type added later cannot silently start
# queue builds this workflow was never written for.
#
# `concurrency` below needs no merge-queue special case, and that was checked
# rather than assumed: on `merge_group` the `github.event.pull_request` half of
# the group expression is null, so the group falls back to `github.ref`, which
# on a queue build is the queue's own generation — measured on objectstack,
# `gh-readonly-queue/main/pr-6594-251e888ac9ace8226f3a8450951e5b40a0a84c2c`.
# It can collide with neither a pull-request group (a bare PR number) nor a
# push group (`refs/heads/main`), so a queue build and the PR build it came
# from never cancel each other.
merge_group:
types: [checks_requested]
workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true
# `fetch-depth: 0` for the gate step below (objectui#3523): it diffs
# against the merge base, which a depth-1 clone cannot resolve.
fetch-depth: 0
# ── Always report; run only when it matters (objectui#3523) ──────────
# `on.pull_request.paths-ignore` used to skip this whole workflow on a
# docs-only or changeset-only pull request, so the `Lint` context was
# simply absent there — and a required check that never reports leaves the
# PR pending forever (in the merge queue, until the ruleset's 60-minute
# timeout fails it). The filter moved from the trigger into the job: the
# job always runs and always reports, the paths decide only whether the
# expensive steps execute. `ci.yml`'s `docs` job is the in-repo precedent
# for the shape, and its `type-check` job carries the long version of this
# note. The list below IS the `paths-ignore` it replaced; the `push`
# trigger keeps its copy, because nothing judges a push to `main`.
#
# Fails OPEN: if the diff cannot be computed the job runs everything,
# rather than reporting green having linted nothing (objectstack#4928).
- name: Decide whether this change needs a full run
id: relevant
run: |
if [ "${{ github.event_name }}" != 'pull_request' ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Not a pull request: push is filtered at the trigger, and a merge_group build is the last validation before main. Running everything.'
exit 0
fi
if ! CHANGED=$(git diff --name-only \
'${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}' -- \
. \
':(exclude,glob)**/*.md' \
':(exclude,glob)content/**' \
':(exclude,glob)docs/**' \
':(exclude,glob).changeset/**'); then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Could not diff against the merge base. Running everything rather than skipping silently.'
exit 0
fi
if [ -n "$CHANGED" ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo "$CHANGED"
else
echo 'should_run=false' >> "$GITHUB_OUTPUT"
echo 'Only ignored paths changed. Skipping the steps below; this check still reports.'
fi
- name: Enable Corepack
if: steps.relevant.outputs.should_run == 'true'
run: corepack enable
- name: Verify pnpm version
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --version
- name: Setup Node.js
if: steps.relevant.outputs.should_run == 'true'
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# Every package must run ESLint or be declared a known gap. turbo skips
# scriptless packages silently, so without this a package reads as clean
# because nothing linted it. Runs before install: only reads package.json.
- name: Verify lint coverage
if: steps.relevant.outputs.should_run == 'true'
run: node scripts/check-lint-coverage.mjs
# ── One entry guard, one predicate (objectui#6092) ───────────────────
# A `scripts/**` CLI has to answer "did node run me, or did something
# import me?" before it does anything, and the hand-typed answers in this
# tree had drifted into NINE spellings across 28 `.mjs` files. Every one
# of them fails the same way: node resolves symlinks for the module graph
# but leaves `process.argv[1]` as the caller typed it, so a script reached
# through a symlink compares two different paths, answers false, and does
# NOTHING — exit 0, no output. The wrappers that spawn these tools hold
# `result.status` only, so an inert child is a green gate. Measured on a
# real blocking gate in this repository (objectui#6078):
# `check-skills-paths.mjs` run directly on a deliberately broken tree
# exits 1 with 696 bytes naming the dead path; run through a symlink
# against the SAME tree it exits 0 with no output.
#
# `scripts/invoked-as.mjs` landed here in #5984 with a header stating that
# a gate enforced the single-spelling rule. It did not exist, and the
# sweep it described had not happened either (objectui#6078). This is that
# gate. It lands BEFORE the conversion on purpose: the worklist grew while
# the card sat open, so a sweep with nothing under it can be undone
# silently by the next pull request. The 29 existing guards are baselined,
# SHRINK-ONLY, and the script's own header says why that is safe.
#
# Runs before install, next to `check-lint-coverage.mjs` above and for the
# same reason: it reads sources with no dependency beyond node builtins
# and two local modules, so nothing it needs is in `node_modules`. Placed
# before `pnpm install` it also cannot be made green by an install
# failure. `--self-test` runs FIRST and is the half that stops the gate
# rotting into decoration — it drives the scanner over fixture sources,
# including the nine spellings measured in this tree, and pins the
# baseline in every direction it can move.
#
# Invoked as `node` rather than through the `pnpm check:entry-guard` alias
# for the pre-install placement, matching `check-lint-coverage.mjs` above
# and `check-cross-repo-closer-outcome.mjs` below. The alias exists in
# `package.json` for local use and `scripts/__tests__/entry-guard-wiring.test.ts`
# holds the two spellings to the same script.
- name: Verify every scripts/ entry guard goes through one predicate
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-entry-guard.mjs --self-test
node scripts/check-entry-guard.mjs
# ── The ported objectstack tooling is a PINNED copy (objectui#6642) ───
# `scripts/pm/check-half-states.mjs` came from objectstack (objectui#5791)
# under a workflow header calling it a verbatim copy and enumerating the
# three things a re-sync must not clobber. Nothing checked either half.
# Measured 2026-08-28, before this step existed: the ported copy stood at
# 9,340 lines against upstream's 12,948 — a 4,637-line `diff` — and its
# own `--self-test` ran 1,116 cases where upstream's ran 1,574. So 458
# predicate cases had landed upstream and never arrived here, while the
# patrol went on rendering a confident report with the corresponding rows
# simply missing.
#
# The direction of harm is this repository's least visible one: a drifted
# copy does not fail, it REPORTS. It became load-bearing once already —
# objectui#6641 had to hand-port H22's closure floor into this copy,
# because wiring the new environment variable in the workflow alone would
# have set a variable this copy did not read.
#
# The gate reverses the DECLARED divergences out of each ported file and
# requires the reconstruction to hash to the pinned upstream digest, so
# drift beyond the declared set is byte-detectable in both directions —
# an edit here, or upstream moving. ⛔ It fetches nothing: a gate that
# reached api.github.com would be red on a network hiccup and green on a
# cached 200, and this repo's whole reason for owning a patrol is that a
# check which cannot read its input must never read as clean (#4690).
#
# Runs before install, next to the two gates above and for the same
# reason: node builtins and one local module only, so an install failure
# cannot take it down with it. `--self-test` runs FIRST — it drives the
# real comparer over fixtures (parity holds, drift outside a region,
# drift inside one, an ambiguous anchor, the pin-bump procedure, and
# every malformed-pin shape), which is what stops a comparer that
# recognises nothing from reading as a clean tree.
- name: Verify the ported objectstack tooling still matches its pin
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-upstream-port-parity.mjs --self-test
node scripts/check-upstream-port-parity.mjs
- name: Turbo Cache
if: steps.relevant.outputs.should_run == 'true'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
if: steps.relevant.outputs.should_run == 'true'
run: pnpm install --frozen-lockfile
- name: Run linter
if: steps.relevant.outputs.should_run == 'true'
run: pnpm lint
# ── The cross-repo closer's outcome contract (#5261) ──────────────────
# `cross-repo-issue-closer.yml` carries ~250 lines of inline
# github-script, and until this step existed it was code nobody had ever
# seen run: it fires only on a merge, its conclusion is required by
# nothing, and every one of its runs so far has been green. That last part
# is the problem rather than the reassurance — measured over every merged
# pull request in this repository, its close loop has had a live target
# roughly one and a half times a day since it landed, took the same
# `already closed -- skipping` exit every time, and left no backlink on
# any of them. Greenly. The header of the workflow carries the figures.
#
# This step is the exercise: the shipped script is extracted from the YAML
# with a real parser (never retyped) and run under doubles the way
# actions/github-script runs it, as one AsyncFunction body. The scenarios
# pin the target parse, the target KIND, and the outcome of every exit —
# which of setFailed / warning / job summary fires, and which API calls
# were made.
#
# Assertion 0 is the compile, and it is not theoretical: the framework's
# copy of this workflow was taken down twice in one day by a `SyntaxError`
# in the inline block, i.e. a script that never ran at all, on a
# post-merge workflow whose red nothing else in CI can see.
#
# `--self-test` runs FIRST and is the half that stops the battery rotting
# into decoration: it mutates the shipped script — downgrade the verdict
# to a warning, break out of the loop instead of isolating, drop the
# same-repo skip, collapse the already-closed branch, strip the backlink
# marker, drop the pull-request guard, and the rest — and requires the
# battery to go RED for each, naming the scenario that catches it. Neither
# the mutations nor the scenarios are counted here: a hand-copied
# enumeration drifts by construction, which is the lesson this workflow's
# own header records. A mutation whose anchor no longer exists is a
# failure too, so rewriting the workflow cannot leave them silently
# matching nothing.
#
# Invoked as `node` rather than through a `pnpm check:*` alias, matching
# `check-lint-coverage.mjs` above. No network, no build; well under a
# second.
- name: Cross-repo closer outcome contract
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-cross-repo-closer-outcome.mjs --self-test
node scripts/check-cross-repo-closer-outcome.mjs
# ── The product's own check command, on the product's own tree ────────
# `objectui check` is what a consumer runs against their schema tree, and
# the root `check` script points that same command at this repository.
# Nothing ran it. It exited 1 with 64 errors on `main` — and had done
# since the first `tsconfig.json` grew a comment — until someone ran it by
# hand while measuring something unrelated (objectui#5237, fixed by
# #5245). A shipped command sitting red on its own repository is the
# dogfood invariant failing, and the reason it could sit there is that no
# gate ever asked. This is that gate (objectui#5246).
#
# The build step below is not a convenience. The root script is
# `node packages/cli/dist/cli.js check`, and this job installs without
# building, so without it the step dies on a missing file. That failure
# mode is worse than no gate at all: it is red for a reason that has
# nothing to do with the tree being checked, and the obvious repair from
# outside is to delete the step — leaving the hole exactly as it was, now
# with a commit saying it was considered.
#
# `...` is pnpm's dependency closure: the CLI plus every workspace
# package it depends on, in topological order, derived from the graph
# rather than listed here. The closure is the requirement and not just
# tidiness — `dist/cli.js` imports `@object-ui/types`' built output at
# startup, so building the CLI package alone produces a binary that
# cannot load (objectui#5237 records the same import as the reproduce
# recipe's first step).
#
# Deliberately NOT `turbo run build`, although the Turbo cache above is
# restored by this point and would usually make it free. A turbo cache
# HIT restores a task's recorded outputs, and an entry recorded with an
# empty output set replays as "cache hit, replaying logs" plus FULL
# TURBO while writing no `dist/` at all — measured on this repo, where
# the CLI then died with ERR_MODULE_NOT_FOUND on the types import above.
# A blocking gate must not be able to fail for a reason that lives in a
# cache rather than in the tree it is judging, which is the same argument
# as the paragraph above one level down. Building through pnpm has no
# cache layer to replay, and it costs well under a minute.
- name: Build the CLI the self-check runs
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --filter '@object-ui/cli...' build
# Errors only, which is the command's existing behaviour rather than a
# setting chosen here: a parse failure increments the error count, a
# non-zero count is the only thing that exits 1, and the unknown-schema-
# type arm prints and moves on. So this step blocks on the same arm
# `pnpm lint` above does, and for the same reason — errors are a signal,
# the warning stream is known debt. Nothing here promotes those warnings
# to failures, and no output-suppressing flag hides them either: they stay
# visible in the log and non-blocking. That arm belongs to objectui#5127,
# which is open; whatever it settles changes what this step PRINTS, never
# what it fails on. No count is quoted, for the reason the `--max-warnings`
# paragraph in the header gives.
- name: Verify the CLI's own check command passes on this repository
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

chore(ci): slow both dependabot channels from weekly to monthly #5366

chore(ci): slow both dependabot channels from weekly to monthly

chore(ci): slow both dependabot channels from weekly to monthly #5366

Workflow file for this run

name: Lint
# Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never
# gated a PR. That mattered more than it looked: every `object-ui/*` rule that
# `eslint.config.js` sets to `error` is a ratchet — added *specifically* so a
# new violation fails CI, with its existing sites pre-cleaned first so the rule
# lints clean on the day it lands. While nothing ran them, every one of them was
# inert.
#
# `eslint.config.js` is the single list of those rules, and this comment
# deliberately neither counts them nor names them: it used to hand-count, and
# the count was stale by the time anyone read it (#3261). A hand-copied
# enumeration drifts by construction, and a stale one still reads as
# authoritative — `content/docs/guide/ci-cd-pipeline.md` avoids the number for
# the same reason. `scripts/__tests__/lint-workflow.test.ts` holds that in
# place: it fails if a count or a rule name reappears here, if the config stops
# setting any `object-ui/*` rule to `error`, or if this workflow stops gating
# pull requests.
#
# `--max-warnings` is deliberately not set: warnings repo-wide run into the
# thousands, dominated by `no-explicit-any` plus React Compiler rules the config
# downgrades on purpose — known historical debt, not a signal. This gate is
# about errors. No exact figure here, for the reason above: this paragraph used
# to carry a hand-maintained warning count and percentage that nothing
# recomputed and nothing alarmed on as they aged (#3274), and
# `scripts/check-lint-coverage.mjs` held a copy of the same number that would
# have gone stale on its own clock. The order of magnitude is the whole
# argument; the integer never was.
on:
push:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'content/**'
- 'docs/**'
- '.changeset/**'
# No `paths-ignore` here any more (objectui#3523, step 2) — it skipped the
# whole workflow on a docs-only / changeset-only PR, so the `Lint` context was
# absent exactly where a required check must still report. The path decision
# moved into the job below. `push` above keeps its copy: nothing judges a push
# to `main`.
pull_request:
branches: [main, develop]
# ── Merge queue (objectui#3523) ────────────────────────────────────────
# The merge queue is ENFORCED on this repository by a ruleset — a direct push
# to `main` returns 405 `Changes must be made through the merge queue`
# (measured in #3243). Until this trigger landed, not one of the repository's
# workflows subscribed `merge_group`: repo-wide `event=merge_group` runs stood
# at total_count = 0, historically. A queue with nothing subscribed to it can
# only have an EMPTY required-check set, so it rebuilt each PR on the current
# `main` and let it through without validating anything.
#
# That is not a theoretical hole; it was cashed in on 2026-08-07. #3498 landed
# a `scripts/` type gate, itself fully green, that left a TS2578 on `main`;
# #3503, #3510 and #3516 then merged between 02:11Z and 02:15Z with `Type
# Check` at conclusion=failure, and #3505 hot-fixed the result. objectstack
# went through the same frames (objectstack#6067 -> #5615).
#
# `types:` is spelled out although `checks_requested` is the ONLY activity
# type GitHub defines for `merge_group` today — the two spellings are
# equivalent right now (objectstack's `ci.yml` and `lint.yml` use the bare
# `merge_group:` form and produce queue builds normally, 3552 of them). Naming
# the type means a second activity type added later cannot silently start
# queue builds this workflow was never written for.
#
# `concurrency` below needs no merge-queue special case, and that was checked
# rather than assumed: on `merge_group` the `github.event.pull_request` half of
# the group expression is null, so the group falls back to `github.ref`, which
# on a queue build is the queue's own generation — measured on objectstack,
# `gh-readonly-queue/main/pr-6594-251e888ac9ace8226f3a8450951e5b40a0a84c2c`.
# It can collide with neither a pull-request group (a bare PR number) nor a
# push group (`refs/heads/main`), so a queue build and the PR build it came
# from never cancel each other.
merge_group:
types: [checks_requested]
workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true
# `fetch-depth: 0` for the gate step below (objectui#3523): it diffs
# against the merge base, which a depth-1 clone cannot resolve.
fetch-depth: 0
# ── Always report; run only when it matters (objectui#3523) ──────────
# `on.pull_request.paths-ignore` used to skip this whole workflow on a
# docs-only or changeset-only pull request, so the `Lint` context was
# simply absent there — and a required check that never reports leaves the
# PR pending forever (in the merge queue, until the ruleset's 60-minute
# timeout fails it). The filter moved from the trigger into the job: the
# job always runs and always reports, the paths decide only whether the
# expensive steps execute. `ci.yml`'s `docs` job is the in-repo precedent
# for the shape, and its `type-check` job carries the long version of this
# note. The list below IS the `paths-ignore` it replaced; the `push`
# trigger keeps its copy, because nothing judges a push to `main`.
#
# Fails OPEN: if the diff cannot be computed the job runs everything,
# rather than reporting green having linted nothing (objectstack#4928).
- name: Decide whether this change needs a full run
id: relevant
run: |
if [ "${{ github.event_name }}" != 'pull_request' ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Not a pull request: push is filtered at the trigger, and a merge_group build is the last validation before main. Running everything.'
exit 0
fi
if ! CHANGED=$(git diff --name-only \
'${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}' -- \
. \
':(exclude,glob)**/*.md' \
':(exclude,glob)content/**' \
':(exclude,glob)docs/**' \
':(exclude,glob).changeset/**'); then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Could not diff against the merge base. Running everything rather than skipping silently.'
exit 0
fi
if [ -n "$CHANGED" ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo "$CHANGED"
else
echo 'should_run=false' >> "$GITHUB_OUTPUT"
echo 'Only ignored paths changed. Skipping the steps below; this check still reports.'
fi
- name: Enable Corepack
if: steps.relevant.outputs.should_run == 'true'
run: corepack enable
- name: Verify pnpm version
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --version
- name: Setup Node.js
if: steps.relevant.outputs.should_run == 'true'
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# Every package must run ESLint or be declared a known gap. turbo skips
# scriptless packages silently, so without this a package reads as clean
# because nothing linted it. Runs before install: only reads package.json.
- name: Verify lint coverage
if: steps.relevant.outputs.should_run == 'true'
run: node scripts/check-lint-coverage.mjs
# ── One entry guard, one predicate (objectui#6092) ───────────────────
# A `scripts/**` CLI has to answer "did node run me, or did something
# import me?" before it does anything, and the hand-typed answers in this
# tree had drifted into NINE spellings across 28 `.mjs` files. Every one
# of them fails the same way: node resolves symlinks for the module graph
# but leaves `process.argv[1]` as the caller typed it, so a script reached
# through a symlink compares two different paths, answers false, and does
# NOTHING — exit 0, no output. The wrappers that spawn these tools hold
# `result.status` only, so an inert child is a green gate. Measured on a
# real blocking gate in this repository (objectui#6078):
# `check-skills-paths.mjs` run directly on a deliberately broken tree
# exits 1 with 696 bytes naming the dead path; run through a symlink
# against the SAME tree it exits 0 with no output.
#
# `scripts/invoked-as.mjs` landed here in #5984 with a header stating that
# a gate enforced the single-spelling rule. It did not exist, and the
# sweep it described had not happened either (objectui#6078). This is that
# gate. It lands BEFORE the conversion on purpose: the worklist grew while
# the card sat open, so a sweep with nothing under it can be undone
# silently by the next pull request. The 29 existing guards are baselined,
# SHRINK-ONLY, and the script's own header says why that is safe.
#
# Runs before install, next to `check-lint-coverage.mjs` above and for the
# same reason: it reads sources with no dependency beyond node builtins
# and two local modules, so nothing it needs is in `node_modules`. Placed
# before `pnpm install` it also cannot be made green by an install
# failure. `--self-test` runs FIRST and is the half that stops the gate
# rotting into decoration — it drives the scanner over fixture sources,
# including the nine spellings measured in this tree, and pins the
# baseline in every direction it can move.
#
# Invoked as `node` rather than through the `pnpm check:entry-guard` alias
# for the pre-install placement, matching `check-lint-coverage.mjs` above
# and `check-cross-repo-closer-outcome.mjs` below. The alias exists in
# `package.json` for local use and `scripts/__tests__/entry-guard-wiring.test.ts`
# holds the two spellings to the same script.
- name: Verify every scripts/ entry guard goes through one predicate
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-entry-guard.mjs --self-test
node scripts/check-entry-guard.mjs
# ── The ported objectstack tooling is a PINNED copy (objectui#6642) ───
# `scripts/pm/check-half-states.mjs` came from objectstack (objectui#5791)
# under a workflow header calling it a verbatim copy and enumerating the
# three things a re-sync must not clobber. Nothing checked either half.
# Measured 2026-08-28, before this step existed: the ported copy stood at
# 9,340 lines against upstream's 12,948 — a 4,637-line `diff` — and its
# own `--self-test` ran 1,116 cases where upstream's ran 1,574. So 458
# predicate cases had landed upstream and never arrived here, while the
# patrol went on rendering a confident report with the corresponding rows
# simply missing.
#
# The direction of harm is this repository's least visible one: a drifted
# copy does not fail, it REPORTS. It became load-bearing once already —
# objectui#6641 had to hand-port H22's closure floor into this copy,
# because wiring the new environment variable in the workflow alone would
# have set a variable this copy did not read.
#
# The gate reverses the DECLARED divergences out of each ported file and
# requires the reconstruction to hash to the pinned upstream digest, so
# drift beyond the declared set is byte-detectable in both directions —
# an edit here, or upstream moving. ⛔ It fetches nothing: a gate that
# reached api.github.com would be red on a network hiccup and green on a
# cached 200, and this repo's whole reason for owning a patrol is that a
# check which cannot read its input must never read as clean (#4690).
#
# Runs before install, next to the two gates above and for the same
# reason: node builtins and one local module only, so an install failure
# cannot take it down with it. `--self-test` runs FIRST — it drives the
# real comparer over fixtures (parity holds, drift outside a region,
# drift inside one, an ambiguous anchor, the pin-bump procedure, and
# every malformed-pin shape), which is what stops a comparer that
# recognises nothing from reading as a clean tree.
- name: Verify the ported objectstack tooling still matches its pin
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-upstream-port-parity.mjs --self-test
node scripts/check-upstream-port-parity.mjs
- name: Turbo Cache
if: steps.relevant.outputs.should_run == 'true'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
if: steps.relevant.outputs.should_run == 'true'
run: pnpm install --frozen-lockfile
- name: Run linter
if: steps.relevant.outputs.should_run == 'true'
run: pnpm lint
# ── The cross-repo closer's outcome contract (#5261) ──────────────────
# `cross-repo-issue-closer.yml` carries ~250 lines of inline
# github-script, and until this step existed it was code nobody had ever
# seen run: it fires only on a merge, its conclusion is required by
# nothing, and every one of its runs so far has been green. That last part
# is the problem rather than the reassurance — measured over every merged
# pull request in this repository, its close loop has had a live target
# roughly one and a half times a day since it landed, took the same
# `already closed -- skipping` exit every time, and left no backlink on
# any of them. Greenly. The header of the workflow carries the figures.
#
# This step is the exercise: the shipped script is extracted from the YAML
# with a real parser (never retyped) and run under doubles the way
# actions/github-script runs it, as one AsyncFunction body. The scenarios
# pin the target parse, the target KIND, and the outcome of every exit —
# which of setFailed / warning / job summary fires, and which API calls
# were made.
#
# Assertion 0 is the compile, and it is not theoretical: the framework's
# copy of this workflow was taken down twice in one day by a `SyntaxError`
# in the inline block, i.e. a script that never ran at all, on a
# post-merge workflow whose red nothing else in CI can see.
#
# `--self-test` runs FIRST and is the half that stops the battery rotting
# into decoration: it mutates the shipped script — downgrade the verdict
# to a warning, break out of the loop instead of isolating, drop the
# same-repo skip, collapse the already-closed branch, strip the backlink
# marker, drop the pull-request guard, and the rest — and requires the
# battery to go RED for each, naming the scenario that catches it. Neither
# the mutations nor the scenarios are counted here: a hand-copied
# enumeration drifts by construction, which is the lesson this workflow's
# own header records. A mutation whose anchor no longer exists is a
# failure too, so rewriting the workflow cannot leave them silently
# matching nothing.
#
# Invoked as `node` rather than through a `pnpm check:*` alias, matching
# `check-lint-coverage.mjs` above. No network, no build; well under a
# second.
- name: Cross-repo closer outcome contract
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-cross-repo-closer-outcome.mjs --self-test
node scripts/check-cross-repo-closer-outcome.mjs
# ── The product's own check command, on the product's own tree ────────
# `objectui check` is what a consumer runs against their schema tree, and
# the root `check` script points that same command at this repository.
# Nothing ran it. It exited 1 with 64 errors on `main` — and had done
# since the first `tsconfig.json` grew a comment — until someone ran it by
# hand while measuring something unrelated (objectui#5237, fixed by
# #5245). A shipped command sitting red on its own repository is the
# dogfood invariant failing, and the reason it could sit there is that no
# gate ever asked. This is that gate (objectui#5246).
#
# The build step below is not a convenience. The root script is
# `node packages/cli/dist/cli.js check`, and this job installs without
# building, so without it the step dies on a missing file. That failure
# mode is worse than no gate at all: it is red for a reason that has
# nothing to do with the tree being checked, and the obvious repair from
# outside is to delete the step — leaving the hole exactly as it was, now
# with a commit saying it was considered.
#
# `...` is pnpm's dependency closure: the CLI plus every workspace
# package it depends on, in topological order, derived from the graph
# rather than listed here. The closure is the requirement and not just
# tidiness — `dist/cli.js` imports `@object-ui/types`' built output at
# startup, so building the CLI package alone produces a binary that
# cannot load (objectui#5237 records the same import as the reproduce
# recipe's first step).
#
# Deliberately NOT `turbo run build`, although the Turbo cache above is
# restored by this point and would usually make it free. A turbo cache
# HIT restores a task's recorded outputs, and an entry recorded with an
# empty output set replays as "cache hit, replaying logs" plus FULL
# TURBO while writing no `dist/` at all — measured on this repo, where
# the CLI then died with ERR_MODULE_NOT_FOUND on the types import above.
# A blocking gate must not be able to fail for a reason that lives in a
# cache rather than in the tree it is judging, which is the same argument
# as the paragraph above one level down. Building through pnpm has no
# cache layer to replay, and it costs well under a minute.
- name: Build the CLI the self-check runs
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --filter '@object-ui/cli...' build
# Errors only, which is the command's existing behaviour rather than a
# setting chosen here: a parse failure increments the error count, a
# non-zero count is the only thing that exits 1, and the unknown-schema-
# type arm prints and moves on. So this step blocks on the same arm
# `pnpm lint` above does, and for the same reason — errors are a signal,
# the warning stream is known debt. Nothing here promotes those warnings
# to failures, and no output-suppressing flag hides them either: they stay
# visible in the log and non-blocking. That arm belongs to objectui#5127,
# which is open; whatever it settles changes what this step PRINTS, never
# what it fails on. No count is quoted, for the reason the `--max-warnings`
# paragraph in the header gives.
- name: Verify the CLI's own check command passes on this repository
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

chore(ci): slow both dependabot channels from weekly to monthly #5366

chore(ci): slow both dependabot channels from weekly to monthly

chore(ci): slow both dependabot channels from weekly to monthly #5366

Workflow file for this run

name: Lint
# Until #2923 this workflow was `workflow_dispatch`-only, so ESLint had never
# gated a PR. That mattered more than it looked: every `object-ui/*` rule that
# `eslint.config.js` sets to `error` is a ratchet — added *specifically* so a
# new violation fails CI, with its existing sites pre-cleaned first so the rule
# lints clean on the day it lands. While nothing ran them, every one of them was
# inert.
#
# `eslint.config.js` is the single list of those rules, and this comment
# deliberately neither counts them nor names them: it used to hand-count, and
# the count was stale by the time anyone read it (#3261). A hand-copied
# enumeration drifts by construction, and a stale one still reads as
# authoritative — `content/docs/guide/ci-cd-pipeline.md` avoids the number for
# the same reason. `scripts/__tests__/lint-workflow.test.ts` holds that in
# place: it fails if a count or a rule name reappears here, if the config stops
# setting any `object-ui/*` rule to `error`, or if this workflow stops gating
# pull requests.
#
# `--max-warnings` is deliberately not set: warnings repo-wide run into the
# thousands, dominated by `no-explicit-any` plus React Compiler rules the config
# downgrades on purpose — known historical debt, not a signal. This gate is
# about errors. No exact figure here, for the reason above: this paragraph used
# to carry a hand-maintained warning count and percentage that nothing
# recomputed and nothing alarmed on as they aged (#3274), and
# `scripts/check-lint-coverage.mjs` held a copy of the same number that would
# have gone stale on its own clock. The order of magnitude is the whole
# argument; the integer never was.
on:
push:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'content/**'
- 'docs/**'
- '.changeset/**'
# No `paths-ignore` here any more (objectui#3523, step 2) — it skipped the
# whole workflow on a docs-only / changeset-only PR, so the `Lint` context was
# absent exactly where a required check must still report. The path decision
# moved into the job below. `push` above keeps its copy: nothing judges a push
# to `main`.
pull_request:
branches: [main, develop]
# ── Merge queue (objectui#3523) ────────────────────────────────────────
# The merge queue is ENFORCED on this repository by a ruleset — a direct push
# to `main` returns 405 `Changes must be made through the merge queue`
# (measured in #3243). Until this trigger landed, not one of the repository's
# workflows subscribed `merge_group`: repo-wide `event=merge_group` runs stood
# at total_count = 0, historically. A queue with nothing subscribed to it can
# only have an EMPTY required-check set, so it rebuilt each PR on the current
# `main` and let it through without validating anything.
#
# That is not a theoretical hole; it was cashed in on 2026-08-07. #3498 landed
# a `scripts/` type gate, itself fully green, that left a TS2578 on `main`;
# #3503, #3510 and #3516 then merged between 02:11Z and 02:15Z with `Type
# Check` at conclusion=failure, and #3505 hot-fixed the result. objectstack
# went through the same frames (objectstack#6067 -> #5615).
#
# `types:` is spelled out although `checks_requested` is the ONLY activity
# type GitHub defines for `merge_group` today — the two spellings are
# equivalent right now (objectstack's `ci.yml` and `lint.yml` use the bare
# `merge_group:` form and produce queue builds normally, 3552 of them). Naming
# the type means a second activity type added later cannot silently start
# queue builds this workflow was never written for.
#
# `concurrency` below needs no merge-queue special case, and that was checked
# rather than assumed: on `merge_group` the `github.event.pull_request` half of
# the group expression is null, so the group falls back to `github.ref`, which
# on a queue build is the queue's own generation — measured on objectstack,
# `gh-readonly-queue/main/pr-6594-251e888ac9ace8226f3a8450951e5b40a0a84c2c`.
# It can collide with neither a pull-request group (a bare PR number) nor a
# push group (`refs/heads/main`), so a queue build and the PR build it came
# from never cancel each other.
merge_group:
types: [checks_requested]
workflow_dispatch:
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true
# `fetch-depth: 0` for the gate step below (objectui#3523): it diffs
# against the merge base, which a depth-1 clone cannot resolve.
fetch-depth: 0
# ── Always report; run only when it matters (objectui#3523) ──────────
# `on.pull_request.paths-ignore` used to skip this whole workflow on a
# docs-only or changeset-only pull request, so the `Lint` context was
# simply absent there — and a required check that never reports leaves the
# PR pending forever (in the merge queue, until the ruleset's 60-minute
# timeout fails it). The filter moved from the trigger into the job: the
# job always runs and always reports, the paths decide only whether the
# expensive steps execute. `ci.yml`'s `docs` job is the in-repo precedent
# for the shape, and its `type-check` job carries the long version of this
# note. The list below IS the `paths-ignore` it replaced; the `push`
# trigger keeps its copy, because nothing judges a push to `main`.
#
# Fails OPEN: if the diff cannot be computed the job runs everything,
# rather than reporting green having linted nothing (objectstack#4928).
- name: Decide whether this change needs a full run
id: relevant
run: |
if [ "${{ github.event_name }}" != 'pull_request' ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Not a pull request: push is filtered at the trigger, and a merge_group build is the last validation before main. Running everything.'
exit 0
fi
if ! CHANGED=$(git diff --name-only \
'${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}' -- \
. \
':(exclude,glob)**/*.md' \
':(exclude,glob)content/**' \
':(exclude,glob)docs/**' \
':(exclude,glob).changeset/**'); then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo 'Could not diff against the merge base. Running everything rather than skipping silently.'
exit 0
fi
if [ -n "$CHANGED" ]; then
echo 'should_run=true' >> "$GITHUB_OUTPUT"
echo "$CHANGED"
else
echo 'should_run=false' >> "$GITHUB_OUTPUT"
echo 'Only ignored paths changed. Skipping the steps below; this check still reports.'
fi
- name: Enable Corepack
if: steps.relevant.outputs.should_run == 'true'
run: corepack enable
- name: Verify pnpm version
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --version
- name: Setup Node.js
if: steps.relevant.outputs.should_run == 'true'
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# Every package must run ESLint or be declared a known gap. turbo skips
# scriptless packages silently, so without this a package reads as clean
# because nothing linted it. Runs before install: only reads package.json.
- name: Verify lint coverage
if: steps.relevant.outputs.should_run == 'true'
run: node scripts/check-lint-coverage.mjs
# ── One entry guard, one predicate (objectui#6092) ───────────────────
# A `scripts/**` CLI has to answer "did node run me, or did something
# import me?" before it does anything, and the hand-typed answers in this
# tree had drifted into NINE spellings across 28 `.mjs` files. Every one
# of them fails the same way: node resolves symlinks for the module graph
# but leaves `process.argv[1]` as the caller typed it, so a script reached
# through a symlink compares two different paths, answers false, and does
# NOTHING — exit 0, no output. The wrappers that spawn these tools hold
# `result.status` only, so an inert child is a green gate. Measured on a
# real blocking gate in this repository (objectui#6078):
# `check-skills-paths.mjs` run directly on a deliberately broken tree
# exits 1 with 696 bytes naming the dead path; run through a symlink
# against the SAME tree it exits 0 with no output.
#
# `scripts/invoked-as.mjs` landed here in #5984 with a header stating that
# a gate enforced the single-spelling rule. It did not exist, and the
# sweep it described had not happened either (objectui#6078). This is that
# gate. It lands BEFORE the conversion on purpose: the worklist grew while
# the card sat open, so a sweep with nothing under it can be undone
# silently by the next pull request. The 29 existing guards are baselined,
# SHRINK-ONLY, and the script's own header says why that is safe.
#
# Runs before install, next to `check-lint-coverage.mjs` above and for the
# same reason: it reads sources with no dependency beyond node builtins
# and two local modules, so nothing it needs is in `node_modules`. Placed
# before `pnpm install` it also cannot be made green by an install
# failure. `--self-test` runs FIRST and is the half that stops the gate
# rotting into decoration — it drives the scanner over fixture sources,
# including the nine spellings measured in this tree, and pins the
# baseline in every direction it can move.
#
# Invoked as `node` rather than through the `pnpm check:entry-guard` alias
# for the pre-install placement, matching `check-lint-coverage.mjs` above
# and `check-cross-repo-closer-outcome.mjs` below. The alias exists in
# `package.json` for local use and `scripts/__tests__/entry-guard-wiring.test.ts`
# holds the two spellings to the same script.
- name: Verify every scripts/ entry guard goes through one predicate
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-entry-guard.mjs --self-test
node scripts/check-entry-guard.mjs
# ── The ported objectstack tooling is a PINNED copy (objectui#6642) ───
# `scripts/pm/check-half-states.mjs` came from objectstack (objectui#5791)
# under a workflow header calling it a verbatim copy and enumerating the
# three things a re-sync must not clobber. Nothing checked either half.
# Measured 2026-08-28, before this step existed: the ported copy stood at
# 9,340 lines against upstream's 12,948 — a 4,637-line `diff` — and its
# own `--self-test` ran 1,116 cases where upstream's ran 1,574. So 458
# predicate cases had landed upstream and never arrived here, while the
# patrol went on rendering a confident report with the corresponding rows
# simply missing.
#
# The direction of harm is this repository's least visible one: a drifted
# copy does not fail, it REPORTS. It became load-bearing once already —
# objectui#6641 had to hand-port H22's closure floor into this copy,
# because wiring the new environment variable in the workflow alone would
# have set a variable this copy did not read.
#
# The gate reverses the DECLARED divergences out of each ported file and
# requires the reconstruction to hash to the pinned upstream digest, so
# drift beyond the declared set is byte-detectable in both directions —
# an edit here, or upstream moving. ⛔ It fetches nothing: a gate that
# reached api.github.com would be red on a network hiccup and green on a
# cached 200, and this repo's whole reason for owning a patrol is that a
# check which cannot read its input must never read as clean (#4690).
#
# Runs before install, next to the two gates above and for the same
# reason: node builtins and one local module only, so an install failure
# cannot take it down with it. `--self-test` runs FIRST — it drives the
# real comparer over fixtures (parity holds, drift outside a region,
# drift inside one, an ambiguous anchor, the pin-bump procedure, and
# every malformed-pin shape), which is what stops a comparer that
# recognises nothing from reading as a clean tree.
- name: Verify the ported objectstack tooling still matches its pin
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-upstream-port-parity.mjs --self-test
node scripts/check-upstream-port-parity.mjs
- name: Turbo Cache
if: steps.relevant.outputs.should_run == 'true'
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
if: steps.relevant.outputs.should_run == 'true'
run: pnpm install --frozen-lockfile
- name: Run linter
if: steps.relevant.outputs.should_run == 'true'
run: pnpm lint
# ── The cross-repo closer's outcome contract (#5261) ──────────────────
# `cross-repo-issue-closer.yml` carries ~250 lines of inline
# github-script, and until this step existed it was code nobody had ever
# seen run: it fires only on a merge, its conclusion is required by
# nothing, and every one of its runs so far has been green. That last part
# is the problem rather than the reassurance — measured over every merged
# pull request in this repository, its close loop has had a live target
# roughly one and a half times a day since it landed, took the same
# `already closed -- skipping` exit every time, and left no backlink on
# any of them. Greenly. The header of the workflow carries the figures.
#
# This step is the exercise: the shipped script is extracted from the YAML
# with a real parser (never retyped) and run under doubles the way
# actions/github-script runs it, as one AsyncFunction body. The scenarios
# pin the target parse, the target KIND, and the outcome of every exit —
# which of setFailed / warning / job summary fires, and which API calls
# were made.
#
# Assertion 0 is the compile, and it is not theoretical: the framework's
# copy of this workflow was taken down twice in one day by a `SyntaxError`
# in the inline block, i.e. a script that never ran at all, on a
# post-merge workflow whose red nothing else in CI can see.
#
# `--self-test` runs FIRST and is the half that stops the battery rotting
# into decoration: it mutates the shipped script — downgrade the verdict
# to a warning, break out of the loop instead of isolating, drop the
# same-repo skip, collapse the already-closed branch, strip the backlink
# marker, drop the pull-request guard, and the rest — and requires the
# battery to go RED for each, naming the scenario that catches it. Neither
# the mutations nor the scenarios are counted here: a hand-copied
# enumeration drifts by construction, which is the lesson this workflow's
# own header records. A mutation whose anchor no longer exists is a
# failure too, so rewriting the workflow cannot leave them silently
# matching nothing.
#
# Invoked as `node` rather than through a `pnpm check:*` alias, matching
# `check-lint-coverage.mjs` above. No network, no build; well under a
# second.
- name: Cross-repo closer outcome contract
if: steps.relevant.outputs.should_run == 'true'
run: |
node scripts/check-cross-repo-closer-outcome.mjs --self-test
node scripts/check-cross-repo-closer-outcome.mjs
# ── The product's own check command, on the product's own tree ────────
# `objectui check` is what a consumer runs against their schema tree, and
# the root `check` script points that same command at this repository.
# Nothing ran it. It exited 1 with 64 errors on `main` — and had done
# since the first `tsconfig.json` grew a comment — until someone ran it by
# hand while measuring something unrelated (objectui#5237, fixed by
# #5245). A shipped command sitting red on its own repository is the
# dogfood invariant failing, and the reason it could sit there is that no
# gate ever asked. This is that gate (objectui#5246).
#
# The build step below is not a convenience. The root script is
# `node packages/cli/dist/cli.js check`, and this job installs without
# building, so without it the step dies on a missing file. That failure
# mode is worse than no gate at all: it is red for a reason that has
# nothing to do with the tree being checked, and the obvious repair from
# outside is to delete the step — leaving the hole exactly as it was, now
# with a commit saying it was considered.
#
# `...` is pnpm's dependency closure: the CLI plus every workspace
# package it depends on, in topological order, derived from the graph
# rather than listed here. The closure is the requirement and not just
# tidiness — `dist/cli.js` imports `@object-ui/types`' built output at
# startup, so building the CLI package alone produces a binary that
# cannot load (objectui#5237 records the same import as the reproduce
# recipe's first step).
#
# Deliberately NOT `turbo run build`, although the Turbo cache above is
# restored by this point and would usually make it free. A turbo cache
# HIT restores a task's recorded outputs, and an entry recorded with an
# empty output set replays as "cache hit, replaying logs" plus FULL
# TURBO while writing no `dist/` at all — measured on this repo, where
# the CLI then died with ERR_MODULE_NOT_FOUND on the types import above.
# A blocking gate must not be able to fail for a reason that lives in a
# cache rather than in the tree it is judging, which is the same argument
# as the paragraph above one level down. Building through pnpm has no
# cache layer to replay, and it costs well under a minute.
- name: Build the CLI the self-check runs
if: steps.relevant.outputs.should_run == 'true'
run: pnpm --filter '@object-ui/cli...' build
# Errors only, which is the command's existing behaviour rather than a
# setting chosen here: a parse failure increments the error count, a
# non-zero count is the only thing that exits 1, and the unknown-schema-
# type arm prints and moves on. So this step blocks on the same arm
# `pnpm lint` above does, and for the same reason — errors are a signal,
# the warning stream is known debt. Nothing here promotes those warnings
# to failures, and no output-suppressing flag hides them either: they stay
# visible in the log and non-blocking. That arm belongs to objectui#5127,
# which is open; whatever it settles changes what this step PRINTS, never
# what it fails on. No count is quoted, for the reason the `--max-warnings`
# paragraph in the header gives.
- name: Verify the CLI's own check command passes on this repository
if: steps.relevant.outputs.should_run == 'true'
run: pnpm check