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
6 changes: 4 additions & 2 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -1471,8 +1471,10 @@ below for the half of that mechanism which is still live.
workflow; manual. **It carries no `pull_request` trigger, on purpose.**

No published package's build output may contain tooling material — `__tests__/`, `__mocks__/`,
`__benchmarks__/`, `*.test.*`, `*.spec.*`, `*.bench.*`, `*.stories.*`. The gate is
`scripts/check-published-dist-tooling.mjs` (`pnpm check:published-dist`); it builds every
`__benchmarks__/`, `*.test.*`, `*.spec.*`, `*.bench.*`, `*.stories.*`, or a `*.tsbuildinfo` build
record ([#7003](https://github.com/objectstack-ai/objectui/issues/7003): a record has no tooling
source to be traced back to, and it names every input path on the machine that produced it). The
gate is `scripts/check-published-dist-tooling.mjs` (`pnpm check:published-dist`); it builds every
published package itself, then reads each one's tarball file list from `npm pack --dry-run`.

Three things about it are easy to get wrong and are written down in the script's own header
Expand Down
94 changes: 94 additions & 0 deletions scripts/__tests__/check-published-dist-tooling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url';
import { TOOLING_FILE } from '../check-phantom-dependencies.mjs';
import {
BUILD_OUTPUT_DIRS,
BUILD_RECORD,
MIN_PACKAGES,
PUBLISHED_TOOLING_FILE,
analyze,
Expand Down Expand Up @@ -47,6 +48,13 @@ import {
* 7. **The gate is wired into the publish path**, which is where the ruling put
* it (comment 5307574139), and into a nightly workflow — and NOT into a
* per-PR job.
* 8. **A build record is refused even though it has no tooling SOURCE.**
* objectui#7003 measured the blind spot: every criterion above traces an
* artifact back to a source the convention names, and a `*.tsbuildinfo` is
* a compiler by-product with no such source, so `PUBLISHED_TOOLING_FILE`
* matches nothing about it. `BUILD_RECORD` is the second, artifact-only
* term; the cases below read it from the gate rather than retyping the
* suffix, so the two cannot disagree about what a build record IS.
*/
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const GATE = 'scripts/check-published-dist-tooling.mjs';
Expand Down Expand Up @@ -161,6 +169,58 @@ describe('the artifact criterion vs. the source criterion', () => {
});
});

// ── 2b. build records, which have no tooling source at all ───────────────────

describe('build records — objectui#7003', () => {
/** Spellings `tsc` writes, built from the gate's own output-directory list. */
const records = BUILD_OUTPUT_DIRS.flatMap((dir) => [
`${dir}/tsconfig.tsbuildinfo`,
`${dir}/tsconfig.build.tsbuildinfo`,
`${dir}/.tsbuildinfo`,
`${dir}/nested/deep/tsconfig.tsbuildinfo`,
]);

it('is exactly the blind spot the card measured: invisible to the derived convention', () => {
// The whole finding of objectui#7003 in one assertion. `PUBLISHED_TOOLING_FILE`
// matches a tooling MARKER — a directory name or a stem — because every
// artifact it grades was emitted from a file somebody wrote. A build record
// was written by the compiler about the build, so it carries no marker, and
// the gate built for tooling material in published output saw nothing.
for (const record of records) {
expect(PUBLISHED_TOOLING_FILE.test(record), `${record} via PUBLISHED_TOOLING_FILE`).toBe(false);
expect(TOOLING_FILE.test(record), `${record} via TOOLING_FILE`).toBe(false);
expect(isToolingArtifact(record), `${record} via isToolingArtifact`).toBe(true);
}
});

it('reads the pattern off the gate instead of retyping the suffix', () => {
// The derived-never-retyped property, applied to the new term: the predicate
// is the union of the two exported patterns and nothing else, so a third
// spelling cannot appear in the gate without appearing here.
for (const path of [
...records,
'tsconfig.tsbuildinfo',
'dist/index.js',
'dist/a.test.d.ts',
'src/a.test.ts',
'dist/__tests__/a.d.ts',
]) {
expect(isToolingArtifact(path), path).toBe(PUBLISHED_TOOLING_FILE.test(path) || BUILD_RECORD.test(path));
}
});

it('does not fire on names that merely contain the word', () => {
for (const clean of [
'dist/index.js',
'dist/tsbuildinfo.js',
'dist/tsconfig.tsbuildinfo.js',
'dist/a.tsbuildinfo/index.js', // a DIRECTORY so named — anchored to the last segment
]) {
expect(isToolingArtifact(clean), clean).toBe(false);
}
});
});

// ── 3. the verdicts, including the vacuous one ───────────────────────────────

describe('outputDirOf', () => {
Expand Down Expand Up @@ -201,6 +261,40 @@ describe('auditPackedFiles', () => {
expect(findings[0].dir).toBe('packages/core');
});

it('names a build record inside the build output — objectui#7003', () => {
const { findings, counters } = auditPackedFiles(pkg('@object-ui/core'), [
'dist/index.js',
'dist/tsconfig.tsbuildinfo',
'dist/chunks/tsconfig.build.tsbuildinfo',
]);
expect(findings.map((f: Finding) => f.reason)).toEqual([
'tooling-in-published-output',
'tooling-in-published-output',
]);
expect(findings.map((f: Finding) => f.file)).toEqual([
'dist/tsconfig.tsbuildinfo',
'dist/chunks/tsconfig.build.tsbuildinfo',
]);
expect(findings[0].pkg).toBe('@object-ui/core');
expect(findings[0].dir).toBe('packages/core');
expect(counters.tooling).toBe(2);
});

it('leaves the record at its REAL location alone — the package root', () => {
// Where every one of this repository's 30 composite packages writes its
// record today, and the state option B would change. It is not in any
// package's `files` list, so it does not reach a tarball at all — but this
// function must not flag it even when handed one, or the gate would red a
// tree that ships nothing wrong.
const { findings, counters } = auditPackedFiles(pkg('@object-ui/core'), [
'tsconfig.tsbuildinfo',
'dist/index.js',
'dist/index.d.ts',
]);
expect(findings).toEqual([]);
expect(counters.tooling).toBe(0);
});

it('reports a tarball with NO build output instead of passing it — the vacuous verdict', () => {
// The whole reason this gate is not "check dist/ if it exists": with no
// per-PR full-repo build, `dist/` is usually absent, and a gate that skips
Expand Down
75 changes: 70 additions & 5 deletions scripts/check-published-dist-tooling.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@
* per-package (a tsconfig `exclude` that names `*.test.ts` but not
* `__tests__/`, a bundler entry that reaches a mock) and the fix belongs in
* that package's build config, as PR #4845 did for four of them.
*
* ## Build RECORDS are refused too, and they have no tooling source (objectui#7003)
*
* Everything above traces an ARTIFACT back to a tooling SOURCE: `dist/a.test.d.ts`
* is refused because `src/a.test.ts` exists and the convention names that stem.
* An incremental build record (`*.tsbuildinfo`) has no such source — it is a
* by-product of the compiler, not the emit of a file anyone wrote — so it
* matches nothing `PUBLISHED_TOOLING_FILE` describes, and objectui#7003 measured
* that blind spot on this gate before anything had shipped through it. It is
* material a consumer must not receive for the same reason the rest of this
* gate exists, and a sharper one: the record NAMES EVERY INPUT PATH on the
* machine that produced it. Every affected package publishes by directory
* (`files: ["dist", …]`), so a record written inside the build output ships
* whole. `BUILD_RECORD` below is therefore a SECOND, artifact-only term rather
* than an addition to `TOOLING_FILE`: see its own docblock for why the shared
* convention is the wrong home for it.
*/

import { execFileSync } from 'node:child_process';
Expand Down Expand Up @@ -186,8 +202,51 @@ export const PUBLISHED_TOOLING_FILE = new RegExp(
`(^|/)(${CONVENTION.directories})/|\\.(${CONVENTION.stems})\\.[^/]*$`,
);

/** Whether a tarball entry is tooling material by this repository's convention. */
export const isToolingArtifact = (path) => PUBLISHED_TOOLING_FILE.test(path);
/**
* An incremental build RECORD, wherever a build wrote one.
*
* ## Why this is a separate term and not a third alternation in `TOOLING_FILE`
*
* Three reasons, and the first is mechanical (objectui#7003):
*
* 1. It would not arrive. `toolingConventionFrom` extracts exactly TWO halves
* out of `TOOLING_FILE.source` — the directory alternation and the stem
* alternation — and `PUBLISHED_TOOLING_FILE` is rebuilt from those two. A
* third alternation added over there is dropped here silently: the sibling
* gate would change behaviour and this one would not, which is the exact
* drift the derivation exists to prevent.
* 2. `TOOLING_FILE` grades SOURCE files, and a build record is not one. Its
* five other readers walk source trees filtered by `SOURCE_FILE`
* (`\.[cm]?[jt]sx?$`), so the term would be inert in all of them — a rule
* declared in a place that never honours it.
* 3. `check-published-tsconfig-tooling-exclude.mjs` turns that convention into
* tsconfig `exclude` patterns. Excluding a `.tsbuildinfo` from a program is
* meaningless: `tsc` writes the record, it never reads one as an input.
*
* ## What it recognises, and what it cannot
*
* Any basename, at any depth, ending `.tsbuildinfo` — `tsconfig.tsbuildinfo`,
* `tsconfig.build.tsbuildinfo`, a bare `.tsbuildinfo`, and the same names in a
* nested directory. That suffix IS this repository's spelling for the artifact:
* `turbo.json`'s build `outputs` name a recursive glob over `*.tsbuildinfo` and
* `.gitignore` ignores the same one, so a record renamed away from that suffix
* would already be uncached and untracked.
*
* It cannot recognise a record whose `tsBuildInfoFile` points at an arbitrary
* name with another extension; nothing readable from the tarball distinguishes
* that file from an emitted one. Stated rather than papered over with a list of
* guessed artifact names — a wrong guess would red a clean package, and the
* repository-wide convention above is the thing actually worth enforcing.
*/
export const BUILD_RECORD = /(^|\/)[^/]*\.tsbuildinfo$/;

/**
* Whether a tarball entry is tooling material by this repository's convention.
*
* The union of the two terms: material traced back to a tooling SOURCE, and
* build records, which have none (objectui#7003).
*/
export const isToolingArtifact = (path) => PUBLISHED_TOOLING_FILE.test(path) || BUILD_RECORD.test(path);

/** The build output directory an entry belongs to, or `null`. */
export function outputDirOf(path) {
Expand Down Expand Up @@ -390,7 +449,12 @@ const HINTS = {
`(${CONVENTION.directories.split('|').join(', ')}) from the EMITTING program, not just the ` +
'`*.test.*` name — that name-vs-directory mismatch is what shipped in objectui#4006 and again ' +
'in objectui#4836. If the file loses its type coverage with the emit, name it in the package\'s ' +
'`tsconfig.test.json` (PR #4845 did exactly this for `core.bench.ts`).',
'`tsconfig.test.json` (PR #4845 did exactly this for `core.bench.ts`). If the file is a ' +
'`*.tsbuildinfo` BUILD RECORD the remedy is a different one, because it has no tooling source ' +
'and no `exclude` can stop it: point that package\'s `tsBuildInfoFile` outside the published ' +
'build output, or leave it at its default (the package root), since `files: ["dist", …]` ' +
'publishes that directory whole and the record names every input path on the machine that ' +
'produced it (objectui#7003).',
'no-build-output':
'A published package produced nothing this gate could inspect. Either its build did not run ' +
'(re-run without `--no-build`), or it now emits outside BUILD_OUTPUT_DIRS, or its `files` field ' +
Expand Down Expand Up @@ -461,8 +525,9 @@ if (invokedDirectly) {
console.log(
`src-tier (reported, not enforced — see the scope note in this gate): ${counters.srcTierTooling} ` +
`tooling file(s) ship OUTSIDE the build output of ${srcTierPackages.join(', ')}, because those ` +
'packages list `src` in `files`. That is objectui#4851, a different card — see the scope note ' +
'in scripts/check-published-dist-tooling.mjs.',
'packages list `src` in `files` (objectui#4851) or publish a build record from outside a ' +
'build output directory (objectui#7003). Both are reported rather than enforced — see the ' +
'scope note in scripts/check-published-dist-tooling.mjs.',
);
}

Expand Down
Loading