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
44 changes: 37 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,10 @@ jobs:
crosspkg: ${{ github.event_name != 'merge_group' && steps.changes.outputs.crosspkg || 'true' }}
# The objectui tree under `ui/` (objectstack#25677). Three outputs, read
# together by the four `* UI` jobs at the bottom of this file:
# ui — a path under ui/, or a root file the ui jobs build
# from (the lockfile, the workspace, turbo.json, ci.yml);
# ui — a path under ui/, a root file the ui jobs build from
# (the lockfile, the workspace, turbo.json), or a root
# the ui gates read (every root workflow, the prompts,
# the objectui skill bundle; see the filter below);
# ui_consumers — one of the seven packages objectui installs from this
# tree changed, so the Console is compiled (Type Check
# UI) and built (Build & E2E UI) against the new shape in
Expand Down Expand Up @@ -446,14 +448,29 @@ jobs:
# narrower audits entry is left as the package that declared it
# spelled it, exactly as the two narrower `skills/` entries were.
- 'docs/**'
# The objectui tree and the root files the ui jobs build from. A
# change to the guard runs the guard, so ci.yml itself is listed.
# The objectui tree, the root files the ui jobs build from, and the
# three roots outside ui/ that the ui jobs' gates and tests read
# (objectstack#25712):
# - every root workflow, not ci.yml alone: Lint UI's pre-install
# import graph check scans all of `.github/workflows/` for ui
# scripts run before an install. ci.yml, the guard itself, is
# one of them. Measured over the 211 merges of the 7 days before
# this entry: 3 changed another workflow and no ui path;
# - `.github/prompts/**`, where objectui's prompts moved in
# objectstack#25957 and which Lint UI's prompt-keys check reads;
# - `skills/objectui/**`, objectui's skill bundle since
# objectstack#26097. Lint UI's skill gates (skills-paths,
# skill-examples, skill-eval-tokens, doc-json,
# shell-escape-residue) and Test UI's skill readers find it
# through `ui/scripts/skill-bundle-root.mjs`.
ui:
- 'ui/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- '.github/workflows/ci.yml'
- '.github/workflows/**'
- '.github/prompts/**'
- 'skills/objectui/**'
# The seven packages objectui installs from this tree (the set its
# `.objectstack-pin` used to pack). A change here compiles and builds
# the Console in the same PR — the check the retired Spec Shape Gate
Expand Down Expand Up @@ -3797,10 +3814,11 @@ jobs:
# when the tree moved under `ui/` (it reads only this repository's
# `.github/workflows/`), and which objectstack#25712 deleted with the
# rest of `ui/.github/workflows/`. They judge what objectui ships
# — `ui/content/docs/**`, `ui/skills/**`, the package READMEs and the
# — `ui/content/docs/**`, the skill bundle (`skills/objectui/**` at the
# repository root since objectstack#26097), the package READMEs and the
# documented component types — so the change most likely to trip them is
# a docs-only or skills-only PR, which the `filter` job's `ui` globs
# already include.
# include.
#
# Ported as that workflow ran them, in its order: the install-free gates
# here, before the install; the gates that read built `dist/*.d.ts` after
Expand Down Expand Up @@ -3883,6 +3901,18 @@ jobs:
if: ${{ !cancelled() }}
run: pnpm install --frozen-lockfile

# The spec liveness ledger cites the renderer in-repo, under ui/
# (objectstack#26150), so a ui/ change can delete a file a ledger row
# cites, move a cited line past the end of its file, drop an anchored
# symbol or stop naming the cited key. Spec Liveness Check does not run on
# a ui/-only pull request (its `paths:` are the spec side) and is not a
# required context, so the gate runs here too, where every ui/ change
# starts it (objectstack#25712 F26). It runs tsx over spec's sources and
# needs the install alone; about 3 s.
- name: Spec Liveness Check — Check the liveness ledger's citations, ui/ included
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
run: pnpm --filter @objectstack/spec check:liveness

- name: Build the @objectstack packages objectui consumes
run: |
pnpm exec turbo run build --concurrency=4 \
Expand Down
8 changes: 5 additions & 3 deletions ui/scripts/__tests__/check-pre-install-import-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,13 +410,15 @@ describe('the gate is wired, not merely present', () => {
// Deliberate: its input is the workflows themselves, and a workflow edit is
// the change most likely to break it. A filter would also make the check
// unrequirable (objectui#3523). The root trigger filters no path; the job
// runs on the `filter` job's `ui` globs, which must match the workflow
// whose steps are the population today and the gate itself.
// runs on the `filter` job's `ui` globs, which must match EVERY root
// workflow, not only ci.yml, whose steps are the population today: the scan
// reads them all, so a ui step added before an install in any one of them
// is this gate's subject (objectstack#25712) — and the gate itself.
const on = withoutComments(triggerBlock(ci));
expect(on).not.toMatch(/paths-ignore:/);
expect(on).not.toMatch(/^\s+paths:/m);
const globs = uiFilterGlobs(ci);
const missed = ['.github/workflows/ci.yml', repoPath(SCRIPT)]
const missed = [...rootWorkflowFiles().map((file) => `.github/workflows/${file}`), repoPath(SCRIPT)]
.filter((file) => !globs.some((glob) => globMatches(glob, file)));
expect(missed, `a change to these would skip ${WORKFLOW}`).toEqual([]);
});
Expand Down
20 changes: 19 additions & 1 deletion ui/scripts/__tests__/check-prompt-component-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
scanPromptSections,
} from '../check-prompt-component-keys.mjs';
import { INDIRECT_REGISTRATIONS, deriveRegistryKeys } from '../check-doc-component-types.mjs';
import { globMatches, jobBlock, rootCi, uiFilterGlobs, uiRun, UI_JOBS } from './helpers/root-ci';

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
/** The repository root, whose `.github/prompts/` the live prompts sit in (objectstack#25712). */
Expand Down Expand Up @@ -312,7 +313,8 @@ describe('wiring', () => {
// this gate. objectstack#25880 ports the `repo-gates.yml` gates (row 24 of
// the objectstack#25697 workflow roster) into the root `Lint UI` job and
// brings these pins back against that job, by path. That case's import-graph
// walk reads no workflow and stays below.
// walk reads no workflow and stays below. The Lint UI home and the filter that
// starts it on a prompts-only change are pinned by the second case below.

it('is exposed as a root package script', () => {
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as {
Expand All @@ -321,6 +323,22 @@ describe('wiring', () => {
expect(pkg.scripts['check:prompt-keys']).toBe(`node ${SCRIPT}`);
});

it('runs in Lint UI, which a prompts-only change starts: the root `ui` filter covers the prompt surface', () => {
// The prompts left `ui/` for the repository root's `PROMPT_DIR`
// (objectstack#25957), where `ui/**` does not reach them, so a prompts-only
// pull request started no ui job until the filter named that root
// (objectstack#25712).
const ci = rootCi();
const lint = jobBlock(ci, UI_JOBS.lint);
expect(lint).toContain(uiRun('check:prompt-keys'));
expect(lint).toContain("needs.filter.outputs.ui != 'false'");
const probe = `${PROMPT_DIR}/any.prompt`;
expect(
uiFilterGlobs(ci).some((glob) => globMatches(glob, probe)),
`no \`ui:\` filter glob covers ${PROMPT_DIR}/`,
).toBe(true);
});

it('needs no install: its import graph reaches no package', () => {
// Walk the WHOLE static import graph, not just this gate's first line: it
// imports a sibling gate, and "needs no node_modules" has to stay true of
Expand Down
22 changes: 21 additions & 1 deletion ui/scripts/__tests__/check-skills-paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
readBaseline,
BASELINE_FILE,
} from '../check-skills-paths.mjs';
import { SKILL_BUNDLE } from '../skill-bundle-root.mjs';
import { globMatches, jobBlock, rootCi, uiFilterGlobs, uiRun, UI_JOBS } from './helpers/root-ci';

/**
* objectui#3735 — the test for `scripts/check-skills-paths.mjs`.
Expand Down Expand Up @@ -516,7 +518,9 @@ describe('wiring — the gate is reachable by name, beside its baseline', () =>
// the monorepo: GitHub runs the root `.github/workflows/` alone, and no
// workflow there runs this gate. objectstack#25880 ports the `repo-gates.yml`
// gates (row 24 of the objectstack#25697 workflow roster) into the root `Lint
// UI` job and brings these pins back against that job, by path.
// UI` job and brings these pins back against that job, by path. The Lint UI
// home and the filter that starts it on a skills-only change are pinned by
// the last case below.

it('is exposed as a root package script', () => {
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
Expand All @@ -526,4 +530,20 @@ describe('wiring — the gate is reachable by name, beside its baseline', () =>
it('ships its baseline next to the script', () => {
expect(fs.existsSync(path.join(repoRoot, BASELINE_FILE))).toBe(true);
});

it('runs in Lint UI, which a skills-only change starts: the root `ui` filter covers the bundle', () => {
// The bundle left `ui/` for the repository root's `SKILL_BUNDLE`
// (objectstack#26097), where `ui/**` does not reach it, so a skills-only
// pull request started no ui job until the filter named that root
// (objectstack#25712).
const ci = rootCi();
const lint = jobBlock(ci, UI_JOBS.lint);
expect(lint).toContain(uiRun('check:skills-paths'));
expect(lint).toContain("needs.filter.outputs.ui != 'false'");
const probe = `${SKILL_BUNDLE}/any`;
expect(
uiFilterGlobs(ci).some((glob) => globMatches(glob, probe)),
`no \`ui:\` filter glob covers ${SKILL_BUNDLE}/`,
).toBe(true);
});
});
Loading