-
-
Notifications
You must be signed in to change notification settings - Fork 6
ci: check the packed package without a checkout #1767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
ff9f8f9
ci: check the packed package without a checkout
claude 837eda6
ci: the package check works for any package, and no regex
claude f8b224d
ci: module header, co-located proof, and refuse a compiler range
claude 389c443
ci: one step per stage in the package check
claude 9c1ee71
ci: install the artifact under a fixed alias
claude 26a6c2f
ci: pin the compiler in config, and drop the shell that was doing logic
claude 908bc0f
ci: quote the response-file paths
claude 5688ffa
ci: read the compiler pin instead of restating it
claude 9f6da87
ci: one command per step, and paths as arguments
claude 652ec49
changelog: the check needs an exact pin
claude 8d2de42
Merge branch 'main' into claude/private-ts-todo-partial-bydyc9
sergey-shandar cefa0d3
ci: validate the whole compiler pin, not its first character
claude 42bd069
Merge remote-tracking branch 'origin/claude/private-ts-todo-partial-b…
claude 5f90cda
ci: let tsc enumerate the packed declarations
claude f0364d3
docs: move the package-check review answers out of the threads
claude a194225
ci: reach declarations under dot-prefixed paths
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| - `fjs ci` now generates a `package-check` job for a project that pins | ||
| TypeScript exactly: it downloads the packed tarball uploaded by the Node job, | ||
| installs it as a dependency outside any checkout, and type-checks every | ||
| declaration the package ships with that pinned compiler. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| /** | ||
| * The packed-package check: a job that consumes the `npm pack` artifact the | ||
| * way an outside consumer would. | ||
| * | ||
| * @module | ||
| * | ||
| * @import { Job } from '../common/types.ts' | ||
| */ | ||
|
sergey-shandar marked this conversation as resolved.
sergey-shandar marked this conversation as resolved.
|
||
|
|
||
| import { images, node } from '../config/module.f.mjs' | ||
| import { uses } from '../common/module.f.mjs' | ||
| import { packageArtifact, packageJobId } from '../node/module.f.mjs' | ||
|
|
||
| export const packageCheckJobId = /** @type {const} */ ('package-check') | ||
|
|
||
| // A fixed alias, so every later command names the package literally. The | ||
| // artifact's own name would otherwise have to be derived and carried between | ||
| // steps. The narrow case an alias gives up: a package that imports itself by | ||
| // name — legal once `exports` is declared — does not resolve under a different | ||
| // directory name. Nothing here self-references; revisit if that changes. | ||
| const alias = /** @type {const} */ ('packed') | ||
|
|
||
| /** | ||
| * The whole check, as a file `tsc` reads for itself. | ||
| * | ||
| * `include` does the enumeration, so no shell walks the tree and no path is | ||
| * ever serialised: a space or a quote in a directory name is a JSON string | ||
| * here and a filename to `tsc`, with nothing in between to get it wrong. An | ||
| * empty match is `TS18003`, which names the pattern that found nothing — | ||
| * "checked nothing and passed" is the failure this job most needs to be | ||
| * legible about. | ||
| * | ||
| * An earlier revision walked the tree with `find`, guarded the result with | ||
| * `test -s`, and passed it through `xargs -0`. Do not go back: review found | ||
| * three defects in that mechanism, one of them silent. `find` omitted | ||
| * `.d.cts`; the paths needed escaping to survive the shell; and `xargs` fills | ||
| * a finite command buffer, so a package large enough to overflow it — about | ||
| * twenty times this one — would have been split across several `tsc` | ||
| * invocations, each a separate program, losing cross-file diagnostics and | ||
| * reporting another batch's globals as missing. One `include` has none of | ||
| * them, and root `AGENTS.md` §6 asks for the tool that parses what it checks | ||
| * rather than a pattern approximating one. | ||
| * | ||
| * `**` rather than a list of declaration extensions, for the same reason: | ||
| * a list is a thing that can be wrong, and that one already was. It does mean | ||
| * a package shipping `.ts` sources and no declarations has a nonempty root | ||
| * set, so `TS18003` would not fire — unreachable here, because root | ||
| * `package.json` `files` is an allowlist with no pattern matching a source | ||
| * file. Recorded in `../todo/package-check-unsupported-package-shapes.md`. | ||
| * | ||
| * The three patterns are one rule TypeScript and npm disagree about: npm's | ||
| * `**` walks into a dot-prefixed name and TypeScript's does not. So `files` | ||
| * publishes `.d/x.d.ts` and a lone `**` would leave it unchecked — silently, | ||
| * and even when it is the package's `types` entry point. The extra patterns | ||
| * name a dot segment explicitly, which does match: `**\/.*` for a dot-named | ||
| * file, `**\/.*\/**\/*` for anything under a dot-named directory at any | ||
| * depth. Two dot segments in a row (`.a/.b/x.d.ts`) still escape, because the | ||
| * inner `**` has to cross `.b` — see the todo. Enumerating the names instead | ||
| * would need a tool walking the tree, which root `AGENTS.md` §6 rules out. | ||
| * | ||
| * `exclude` is emptied because the default excludes `node_modules`, which is | ||
| * the only place the artifact exists. `skipLibCheck` is stated rather than | ||
| * left at its default: it is the one option whose flip would stop `tsc` | ||
| * opening these declarations at all, and the job would still pass. | ||
| */ | ||
| const tsconfig = /** @type {const} */ ({ | ||
| include: [ | ||
| `node_modules/${alias}/**/*`, | ||
| `node_modules/${alias}/**/.*`, | ||
| `node_modules/${alias}/**/.*/**/*`, | ||
| ], | ||
| exclude: [], | ||
| compilerOptions: { | ||
| module: 'nodenext', | ||
| target: 'esnext', | ||
| strict: true, | ||
| noEmit: true, | ||
| skipLibCheck: false, | ||
| }, | ||
| }) | ||
|
|
||
| /** | ||
| * One command per step, so a failure names what failed rather than arriving as | ||
| * an opaque script. | ||
| * | ||
| * The compiler is whatever the project pins, passed through untouched. With no | ||
| * checkout there is no lockfile, so a version chosen here instead would let the | ||
| * registry — or a constant that drifted from `package.json` — decide the | ||
| * verdict. | ||
| * | ||
| * `npm`, `npx` and `tsc` are the only external tools left, and root | ||
| * `AGENTS.md` §6 is why there are no others: `tsc` is the established tool | ||
| * that parses what it checks, and `npm` is the subject — a job proving the | ||
| * package installs for a consumer cannot avoid the consumer's package manager. | ||
| * | ||
| * @type {(pin: string) => readonly string[]} | ||
| */ | ||
| const commands = pin => [ | ||
| 'npm init -y > /dev/null', | ||
| // `echo` is the shell's own builtin expanding its own glob; `ls` would be | ||
| // a second process to learn what the shell already knew. | ||
| // | ||
| // No guard against a second `.tgz`: the glob would expand to two names | ||
| // inside one `file:` spec and npm fails ENOENT naming both, which is | ||
| // louder than anything a count check would print. | ||
| `npm install "${alias}@file:$(echo *.tgz)"`, | ||
| `npm install "typescript@${pin}"`, | ||
| `echo '${JSON.stringify(tsconfig)}' > tsconfig.json`, | ||
| 'npx tsc', | ||
| ] | ||
|
|
||
| /** | ||
| * Downloads the packed tarball, installs it as a real dependency, and | ||
| * type-checks every declaration it ships with the compiler the package pins. | ||
| * | ||
| * Deliberately not built through `toSteps`: that helper injects | ||
| * `actions/checkout`, and the missing checkout is this job's whole point. With | ||
| * no repository on the runner there is no `tsconfig.json` up the tree to | ||
| * inherit, no `node_modules` to resolve into, and no source file that could | ||
| * stand in for a declaration the tarball omits — so the job can only see what a | ||
| * real consumer sees. | ||
| * | ||
| * @type {(pin: string) => Job} | ||
| */ | ||
| export const packageCheckJob = pin => ({ | ||
| 'runs-on': images.ubuntu.arm, | ||
| // Without this the two jobs race and the download fails before the check | ||
| // has run — red for a reason unrelated to what it tests. | ||
| needs: [packageJobId], | ||
| steps: [ | ||
| uses('actions/download-artifact', { name: packageArtifact }), | ||
| uses('actions/setup-node', { 'node-version': node.default }), | ||
| ...commands(pin).map(run => ({ run })), | ||
| ], | ||
| }) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an external project has no string-valued
devDependencies.typescript,fjs cistill succeeds but silently emits nopackage-checkjob, despite the changelog promising that job; such a project's CI can therefore remain green without checking its packed declarations. Fresh evidence after the earlier compiler-version comment is this new conditional omission, so fail generation with an actionable error rather than quietly removing the validation.AGENTS.md reference: AGENTS.md:L112-L118
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping the omission, and I want to be clear that it is a decision rather than an unaddressed finding.
Your premise is right: silently dropping a check is worse than failing. Where I disagree is that it is silent. The generated workflow is committed to the repository, so losing the job appears as a
package-checkdeletion inci.yml— in the diff, in review — and node26's regeneration check makes an unregenerated workflow red. For this repository that is louder than an error at generation time, because it is visible to a reviewer rather than only to whoever ran the command.Failing generation instead has a cost I do not want to pay:
fjs ciwould stop working for any project without an exact TypeScript pin, including ones that do not want this job at all. That trades a real regression for a hypothetical one.On the changelog: fair point, it promised the job unconditionally. Since 9f6da87 the pin must also be exact, so I will make the entry say the job is generated when the project pins TypeScript exactly, rather than implying it is unconditional.
I should also flag the framing behind this and the
typesVersionsthread, since it applies to both: this generator exists to build our CI.fjs ciworking for other projects is a side effect, not a product commitment — so "an external project's CI can remain green" is not a cost we are carrying. Findings about our repo on this PR were right and got fixed; this one is about someone else's.Generated by Claude Code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed:
fjs/ci/todo/package-check-unsupported-package-shapes.md,`` in f0364d3 — issue 2.The todo is honest that this one is cheap to build — a
Resultfromci(setup)and an error path throughfjs ci— so the open question is whether refusing is right, not whether it is hard. My reason for not refusing is that the generator's other jobs do not depend on a pin, and a project without one still wants them.The contract it replaces is now written down where a consumer will find it, in
fjs/ci/README.md: an exactdevDependencies.typescriptgenerates the job, anything else generates none. Silently producing one fewer job was the real complaint, and documenting the rule answers it without a refusal.Generated by Claude Code