Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/pre-install-import-graph.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
name: Pre-Install Import Graph

# Why this is its own workflow rather than a step in `lint.yml` or `ci.yml`:
# what it judges is the ARRANGEMENT of the workflows themselves — which steps
# run a `scripts/` gate before their job installs anything. Its whole input is
# `.github/workflows/**` plus the `scripts/` files those steps name, so a run
# lives or dies on files that both `ci.yml` and `lint.yml` keep OUT of their
# expensive lanes. Putting it inside either one would make it skip on exactly
# the change most likely to break it: a workflow edit that moves a step across
# `pnpm install`.
#
# Same shape and the same reasoning as `docs-links.yml`, `control-bytes.yml`,
# `skills-paths.yml` and `changeset-presence.yml`, whose headers record the
# conclusion this repository reached four times already — a gate that cannot see
# the PR shape most likely to trip it "rebuilds the hole it exists to close".
# One gate, one home.
#
# Hence: no `paths` and no `paths-ignore` here, deliberately. Reporting on every
# pull request is also what makes the check requirable, and
# `scripts/dependabot-merge-gate.mjs` classifies it as a required context —
# an unclassified blocking check is one a Dependabot merge would be let past
# (objectui#6135).
#
# It needs no install and no build — a checkout plus one `node` call over 24
# workflow files and a dozen scripts, well under a second. Keep it that way: the
# gate exists to protect install-free gates, so an install here would be the
# joke telling itself.

on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
# Merge queue (objectui#3523 — see `ci.yml`'s trigger block for the full note
# and the measurements behind it). A required check that does not report on a
# queue build stalls the queue until the ruleset's 60-minute timeout fails it,
# so an unfiltered gate that can become required subscribes here from the
# start. `types:` is named although `checks_requested` is currently the only
# activity type GitHub defines for `merge_group`.
merge_group:
types: [checks_requested]
workflow_dispatch:

concurrency:
group: pre-install-import-graph-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
pre-install-import-graph:
name: Pre-Install Import Graph Check
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22.x'

# Two legs, in this order and in one step, for the reason `lint.yml` runs
# the entry-guard gate the same way: a scan whose recogniser is broken
# reports a clean tree. The self-test pins the workflow parser and the
# graph walk against fixtures, so a green scan below means something.
#
# This step is itself in the population the gate derives — it runs a
# `scripts/` file before any install, in a job that never installs — so
# the gate walks its own import graph on every run. That is deliberate: a
# floor that exempted its own enforcer would be the first thing to rot.
- name: Check the pre-install gates' import graphs
run: |
node scripts/check-pre-install-import-graph.mjs --self-test
node scripts/check-pre-install-import-graph.mjs
55 changes: 55 additions & 0 deletions content/docs/guide/ci-cd-pipeline.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@ one has its own section below.
| `doc-component-types.yml` | Doc Component Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `content/docs/**.mdx` snippet teaches a `type` nothing registers |
| `doc-snippet-types.yml` | Doc Snippet Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a covered documentation snippet no longer compiles against the packages' built types |
| `doc-fence-languages.yml` | Doc Fence Language Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a TypeScript block sits under a fence the snippet gate does not read |
| `pre-install-import-graph.yml` | Pre-Install Import Graph Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a gate a workflow runs *before* `pnpm install` reaches a package anywhere in its import graph |
| `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget |
| `live-e2e.yml` | Live E2E (informational) | PR to `main`, `develop` (code paths); nightly cron `30 6 * * *`; manual | No — informational lane, `continue-on-error` |
| `labeler.yml` | Auto Label PRs | PR `opened`, `synchronize`, `reopened` | No |
Expand DownExpand Up@@ -722,6 +723,60 @@ which is why the probe runs before the verdict.
block ```ts (or ```tsx) and fix whatever `check-doc-snippets` then reports, then lower the file's
number. Run it locally with `pnpm check:doc-fences`; it needs no install and no build.

## Pre-Install Import Graphs (`pre-install-import-graph.yml`)

**Triggers:** Push and PR to `main`/`develop`, merge-queue builds, plus manual dispatch — with **no
path filter at all**. What this gate judges is the arrangement of the workflows themselves, so its
input is `.github/workflows/**` plus the `scripts/` files those workflows name, and the change most
likely to break it is a workflow edit. It appears in the checks list as **Pre-Install Import Graph
Check**.

Runs `scripts/check-pre-install-import-graph.mjs`. Several gates in this repository deliberately run
**before any `pnpm install`** — that is what lets them run unfiltered on every pull request shape for
the price of a checkout plus one `node` call. The property that arrangement silently depends on is
that each of those scripts' *whole static import graph* is node builtins plus repo-relative modules,
with nothing in it needing `node_modules`.

**Why it needed a gate.** A violation is invisible everywhere it could be caught cheaply: it is not a
type error (`tsc` is happy with a package import), not a lint error (the package is a real dependency
of the repo), not a local failure (locally `node_modules` exists), and — until
[#6148](https://github.com/objectstack-ai/objectui/issues/6148) — not a test failure, because exactly
one of the pre-install scripts had a test asserting it. It surfaces only as `ERR_MODULE_NOT_FOUND`
inside one CI job, on whichever pull request happens to touch the file; and for the gates that carry
no path filter *precisely so they see every PR shape*, that is a gate which **stops running** rather
than one that fails loudly.

**The population is derived, never listed.** On every run the gate parses every workflow and, per
job, compares each step's index against the index of the first `pnpm install` step **in that same
job**. Move a step above an install and the population grows on the next run; move one below and it
shrinks. A hard-coded list would break silently the first time someone moved a step across an
install, which is exactly the edit that needs catching. Two anchoring decisions the derivation
depends on, each with a case in this repository: `pnpm exec playwright install chromium` installs a
browser rather than the workspace, and `git config merge.pnpm-merge.driver "pnpm install …"` in
`dependabot-auto-merge.yml` *configures* a driver in a job that never installs — reading either as an
install would move a boundary and silently drop a script out of the population.

**It walks the graph, not the entry file.** Requiring each of the entry's own imports to start with
`node:` is too narrow in one direction (a relative import of a builtins-only local module is fine,
and two of these scripts spell their builtins bare as `from "fs"`, which is equally install-free) and
too weak in the other, because it cannot see a package pulled in **one hop away**. Since
[#6092](https://github.com/objectstack-ai/objectui/issues/6092) every one of these scripts imports
`scripts/invoked-as.mjs`, so one hop away is exactly where the next breach comes from. The check is
static rather than a runtime resolver hook because a hook *executes* module top level, and these
files are CI gates that spawn `git`, read the whole tree and call `process.exit`.

**It is in its own population.** The step above runs a `scripts/` file before any install, in a job
that never installs, so the gate walks its own import graph on every run. A floor that exempted its
own enforcer would be the first thing to rot.

**If it fails:** it prints the offending chain — `scripts/some-gate.mjs -> scripts/invoked-as.mjs ->
typescript` — rather than a bare verdict, so the hop that introduced the package is named. Repairing
the import is deliberately *not* this gate's job: either drop the package, or move the step below
`pnpm install` in its workflow and accept the install cost. Run it locally with
`pnpm check:pre-install-import-graph`, `node scripts/check-pre-install-import-graph.mjs --list` to see
the derived population and every module walked, or `--self-test` to exercise the parser and the walk
against fixtures.

## Link Checking (`check-links.yml`)

**Trigger:** Weekly cron (`17 4 * * 0` — Sundays, off the top of the hour, when the scheduled-run
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@
"check:doc-fences": "node scripts/check-doc-fence-languages.mjs",
"check:eager-closure": "node scripts/check-eager-closure-budget.mjs",
"check:entry-guard": "node scripts/check-entry-guard.mjs",
"check:pre-install-import-graph": "node scripts/check-pre-install-import-graph.mjs",
"cli": "node packages/cli/dist/cli.js",
"objectui": "node packages/cli/dist/cli.js",
"create-plugin": "node packages/create-plugin/dist/index.js",
Expand Down
Loading
Loading