ci: make every sub-project buildable from its lockfile - #66
Conversation
pg-dotnet was the only sub-project without a lockfile, so a CI job that builds every sub-project from its lockfile had nothing to lock against. RestorePackagesWithLockFile keeps packages.lock.json in step with the csproj on a normal restore; CI adds --locked-mode, which fails with NU1004 when a PackageReference moves without the lockfile being regenerated.
pg-node has no bundler, so a build job for it would only prove that npm ci succeeds. The check runs node --check on the CLI entry point and imports src/encryption.mjs, which resolves the named imports against the installed @e4a/pg-js and so fails when an export is renamed or removed.
Adds the per-sub-project install-and-build commands, the packages.lock.json regeneration rule, and a note that the dobby-coder App cannot push files under .github/workflows/ so workflow changes arrive as a patch to apply.
Workflow YAML for a maintainer to applyThese are the two jobs #65 asks for. I cannot commit them: Both files pass
|
There was a problem hiding this comment.
VERDICT: request-changes
Rules + review gate, cycle 1. I re-ran the two load-bearing claims in this container against 6d10682 instead of reasoning about them.
Blocker: the canary's pg-manual leg cannot fail
pg-manual is the only consumer of @e4a/pg-wasm in the matrix, and both examples load the SDK through a dynamic import('@e4a/pg-wasm') and destructure at runtime (examples/string.js:16, examples/file.js:17). Webpack does no static export analysis across that boundary. I renamed the destructured seal binding to a name the SDK does not export, then ran the exact commands the workflows run:
npm run build->compiled successfully, exit 0, zero warningsnpx webpack --fail-on-warnings->compiled successfully, exit 0
A breaking @e4a/pg-wasm release therefore leaves both ci.yml and sdk-canary.yml green, so @e4a/pg-wasm has no coverage at all. That is the failure mode the canary exists for.
It also makes the comment's "Not covered" paragraph wrong where it says webpack --fail-on-warnings "would close that hole". It does not, for this case. It only catches statically imported deps, like the web-streams-polyfill rename recorded at CLAUDE.md:44.
The fix has the same shape as the one already applied to pg-node: give pg-manual a check script that imports the bundler entry and asserts the named exports the examples use (seal, sealStream, plus whatever utils.js pulls). Both workflows already run npm run check --if-present, so no YAML change is needed. Correct the "Not covered" paragraph in the same pass.
Non-blocking
Two smaller points are inline on pg-node/package.json and CLAUDE.md.
Cheap while a maintainer is pasting the YAML anyway: actions/checkout@v5 and actions/setup-node@v5 are two majors behind (v7.0.1 and v7.0.0 are current) and actions/setup-dotnet@v5 is one behind (v6.0.0). Nothing breaks on v5 today. Same pins appear in both files.
Verified green
dotnet restore --locked-mode + dotnet build --no-restore on net8.0 and net10.0, npm ci + build/check/lint across the three node sub-projects, and the NU1004 negative case. The lockfile plumbing itself is sound.
The examples load the SDK through a dynamic import('@e4a/pg-wasm') and
destructure at runtime, so webpack never analyses those bindings. Renaming
the destructured `seal` binding leaves `npm run build` at exit 0 with zero
warnings, and `webpack --fail-on-warnings` at exit 0 too, so both the PR
gate and the weekly canary would stay green through a breaking pg-wasm
release. pg-wasm is only used here, so it had no coverage at all.
`npm run check` builds check/sdk-exports.js, which imports the same names
statically, with --fail-on-warnings. Dropping `seal` from the SDK now exits
1 while `npm run build` still succeeds.
Both workflows already run `npm run check --if-present`, so the proposed
YAML is unchanged.
Also correct CLAUDE.md's claim about what pg-node's check covers: it never
links index.mjs's own imports, only src/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Cycle 1 addressed in fcde281. Blocker, the canary's pg-manual leg cannot fail: confirmed and fixed. Measured by dropping
Both workflows already run The "Not covered" paragraph: you were right that it was wrong, and the middle row above is why. That section of the workflow comment is rewritten as "What covers Action versions: bumped in the comment, Two inline threads: both replied to and resolved. CLAUDE.md's workflows paragraph is split into the standing permission rule and a paragraph marked pending that links the YAML to this PR; the pg-node line now says what the check actually reaches, and names the Re-ran locally after the change: |
There was a problem hiding this comment.
VERDICT: request-changes
Rules + review gate, cycle 2 on fcde281. Cycle 1's blocker is fixed and I re-verified it: with seal dropped from the installed SDK, npm run check in pg-manual exits 1 while npm run build still exits 0, so @e4a/pg-wasm now has real coverage. The lockfile plumbing stays green.
One blocker left, and it is the mirror image of cycle 1's.
Blocker: the static-import half of the same hole is still open
pg-manual's build script is still bare webpack, so a broken static import compiles with a warning and exit 0. Measured on this branch by re-applying the web-streams-polyfill rename from CLAUDE.md:49: npm run build exit 0 (with the export ... was not found warning), npm run check exit 0, npx webpack --fail-on-warnings exit 1. Both ci.yml steps pass on a broken example.
That is not hypothetical — it is two of the three breakages this repo has already hit (CLAUDE.md:49 and :50), and CLAUDE.md:53, a line this PR edits, asks CI to enforce it. On a clean tree --fail-on-warnings exits 0, so the one-flag fix is safe to turn on today. Details and both measurements are inline on pg-manual/package.json.
Non-blocking
Two inline notes: the sdk-exports.js comment tells a maintainer to update one place where coverage needs two (verified — an unreferenced import keeps the check at exit 0), and the CLAUDE.md sentence about --fail-on-warnings should split the dynamic case from the static one.
Rule sweep
Clean. Checked the CI/canary YAML in the comment against the Actions default-shell pipefail trap (the report job sets set -euo pipefail and has no unguarded pipeline) and the empty-tag-resolves-to-latest trap (no version is read from a file); checked PR hygiene against the conventional-commit title check (pr-title.yml is present, ci: is valid), reviewer assignment (rubenhensen requested), the closing-keyword rule (Part of #65 is the correct deliberate form here, since a human must apply the YAML), and the draft/no-CI rules (draft is correct mid-cycle). Prose and scope sweep found nothing: the new webpack config stays on mode: 'development', so it does not stray into production-build territory.
Verified green on fcde281
npm ci from the lockfile in pg-manual, then plain webpack and webpack --fail-on-warnings both exit 0 with zero warnings on the untouched tree. The negative cases above were produced by editing the tree, then reverted — the branch is unmodified.
`build` ran bare `webpack`, which reports a missing static import as a warning and still exits 0. Both breakages recorded in CLAUDE.md's pg-manual notes (web-streams-polyfill v3->v4, yivi 0.2->1.x) are that shape, so the CI gate would have gone green on either. Measured in pg-manual: re-applying the web-streams-polyfill rename to examples/file.js leaves `npm run build` at exit 0 without the flag and exits 1 with it, while the untouched tree compiles with zero warnings either way. The flag on `build` only reaches static imports, so `check` still carries the dynamic `@e4a/pg-wasm` half. CLAUDE.md now splits the two claims instead of implying the flag is not useful here, and the sdk-exports.js comment names both edits a new probe name needs: an import-only addition is elided and keeps the check at exit 0.
|
Cycle 2 addressed in 7963a9b. Blocker, the static-import half: confirmed and fixed. The one thing worth adding to your table is the row that shows why
One script per half, no overlap. The two nits: both applied. All three inline threads are replied to and resolved. Re-ran after the change: The workflow-YAML comment's closing section is rewritten to match, since it carried the same framing the |
The bullet said the flag turns "both breakages recorded under pg-manual
build notes" into a failed build. Only one of the two is a webpack
warning. Measured on this branch by reintroducing each breakage and
running npm run build:
- web-streams-polyfill rename, `import { PolyfilledWritableStream }`:
exit 1, "export 'PolyfilledWritableStream' ... was not found". Caught.
- yivi, `import * as YiviCore from '@privacybydesign/yivi-core'`:
exit 0, zero warnings, "compiled successfully". npm run check also
exit 0. Not caught.
`import * as` is valid ESM regardless of what the module exports, so
webpack emits nothing; yivi-core 1.0.0 is type: module, and the
namespace object throws TypeError: not a constructor at
`new YiviCore(...)` in examples/utils.js:63. That is a runtime failure
no build gate sees, and check/sdk-exports.js cannot close it either
because the mistake is in this repo's import form rather than in the
SDK's export list.
Scopes the sentence to what was measured. No code change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
VERDICT: approve
Rule sweep is clean. The one finding from the review pass was real, and I fixed it in b2f9cff rather than sending the PR back, since it was a one-paragraph docs correction and this is the third cycle.
The finding, confirmed then fixed
CLAUDE.md:33 said --fail-on-warnings turns "both breakages recorded under pg-manual build notes" into a failed build. I reintroduced each breakage on the branch and ran the gates. Only one of the two is a webpack warning:
| Reintroduced breakage | npm run build |
npm run check |
|---|---|---|
web-streams-polyfill rename, import { PolyfilledWritableStream } |
exit 1, export 'PolyfilledWritableStream' ... was not found |
exit 0 |
yivi, import * as YiviCore from '@privacybydesign/yivi-core' |
exit 0, zero warnings, "compiled successfully" | exit 0 |
import * as is valid ESM whatever the module exports, so webpack has nothing to warn about. @privacybydesign/yivi-core 1.0.0 is type: module, and its namespace object is not callable:
typeof NS = object
keys = StateMachineImpl,YiviCore,transitions
new NS() threw: TypeError: NS is not a constructor
That is exactly what examples/utils.js:63 does with new YiviCore({...}). So the yivi class of breakage fails at runtime, and no build gate in this repo covers it. check/sdk-exports.js can't close it either, because the mistake is in this repo's own import form rather than in the SDK's export list. Only running the example would catch it, which is out of scope here.
b2f9cff scopes the sentence to the class the flag does catch and says plainly that the yivi class is uncovered. No code changed, and build plus check still exit 0 in pg-manual and pg-node after the edit.
This mattered because CLAUDE.md is what the next agent trusts. As written it claimed CI covers a breakage class it does not.
Rule sweep
Checked the rules that apply to a CI/lockfile diff on an example repo: PR-title conventional commits, closing-keyword usage, reviewer assignment, cross-repo link format, draft policy, the workflows-permission constraint, the no-build-CI repo caveat, Actions default-shell pipefail, the gh empty-tag trap, example-apps-no-production-build, the writing rules, and no-justification-paragraphs. No violations.
Two worth spelling out, because both look like violations at a glance and are not:
Part of #65instead ofCloses #65is correct here. The issue is not finished until a maintainer applies the YAML, and the PR body says so.- Shipping the workflow YAML in a comment rather than the diff is the required pattern, not missing work.
dobby-coder[bot]has noworkflowspermission.
I also spot-checked CLAUDE.md's new install-and-build table against the actual package.json files rather than trusting it: pg-manual has build and check, pg-node has check, pg-sveltekit has build, check and lint, and pg-dotnet carries RestorePackagesWithLockFile. The table is accurate.
The workflow YAML in the comment
GitHub cannot anchor an inline comment to a path outside the diff, so the YAML review goes here. It is also the half a diff review would normally miss, since it lands via a maintainer's commit.
Both files read clean. The report job is the only run: block with real shell in it, and it opens with set -euo pipefail, so the Actions default-shell gap (bash -e without pipefail) does not apply. It also contains no pipe, so there was nothing for that gap to swallow.
I ran that job's shell body against a stubbed gh to check the parts static reading cannot settle. The heredoc de-indents correctly out of the YAML block scalar, the escaped backticks survive as literal backticks, $RUN_URL expands, the no-existing-issue branch reaches gh issue create, and the block exits 0. No version-from-file feeds into gh, so the empty-tag-resolves-to-latest trap does not apply either.
One note for whoever applies it, not a defect: permissions: issues: write is scoped to the report job only, which is right.
State
Flipping out of draft. The repo has no build CI yet, which is the point of #65, so the verification in the PR body is local and a maintainer still needs to paste the two YAML files.
Part of #65
The repo had no build CI, so every PR said "reviewer must verify manually". The two jobs the issue asks for are written and tested, but they live in a PR comment rather than in this diff, because
dobby-coder[bot]has noworkflowspermission and the remote rejects any push touching.github/workflows/:So this PR is the part I can land: the lockfile plumbing and the per-sub-project checks both jobs depend on. A maintainer has to paste the two YAML files from the comment below for #65 to be done, hence
Part ofrather thanCloses.Committed here
pg-dotnet/packages.lock.json+RestorePackagesWithLockFile: pg-dotnet was the only sub-project without a lockfile, so "build every sub-project from its lockfile" had nothing to lock against there.RestorePackagesWithLockFilekeeps the lockfile in step on a normal restore; CI adds--locked-mode.npm run checkscript forpg-node: it has no bundler, so its CI job would otherwise only prove thatnpm cisucceeds. The check importssrc/encryption.mjs, which resolvessrc/'s named imports against the installed@e4a/pg-js.--fail-on-warningsonpg-manual'sbuild, and a newnpm run check: neither SDK breakage this repo has already hit would have failed CI without them. See below.packages.lock.jsonregeneration rule, what each check does and does not cover, and the workflows-permission block so the next agent does not rediscover it.Why pg-manual needed both
pg-manual can break in two ways, and until this PR neither one failed a build.
A static import going stale is the shape of both breakages already recorded in CLAUDE.md's pg-manual notes:
web-streams-polyfillv3 renamingPolyfilledWritableStream, and yivi 0.2 to 1.x moving to named exports. Webpack reports these as a warning and exits 0, sonpm run buildcompiled a broken example green.A dynamic import going stale is the pg-wasm case. Both examples reach the SDK through
import('@e4a/pg-wasm')and destructure at runtime, and webpack analyses no exports across that boundary, so it does not even warn.One flag and one small build cover one half each. Measured on this branch:
npm run build(before)npm run build(now,--fail-on-warnings)npm run checkfile.js:2static rename re-appliedsealdropped from the installed SDKThe bottom-right of that table is why the flag alone is not enough.
check/sdk-exports.jsimports the four names the examples destructure (seal,sealStream,Unsealer,StreamUnsealer) statically, which puts them back under webpack's analysis;check/webpack.config.jskeeps that build out ofdist/. Both workflows already runnpm run buildandnpm run check --if-present, so the YAML needs no change for either half.The repo has no test suite (it is example code), so the check script and the flag are themselves the regression guard rather than a test file. Both negative cases above were produced by editing the tree, then reverted.
Verification
Everything below was run in this container against the branch, not reasoned about.
npm ci+build/check/lint --if-presentin pg-manual, pg-node, pg-sveltekitdotnet restore --locked-mode+dotnet build --no-restoreE4A.PostGuardto 0.5.0 without regenerating the lockfile--locked-modefails,NU1004web-streams-polyfillrename inexamples/file.jsnpm run buildfails,npm run checkstill passessealfrom the installed@e4a/pg-wasmnpm run checkfails,npm run buildstill passessdk-exports.js's import but not its object literalPostGuardimport in pg-node'ssrc/encryption.mjsnpm run checkfails, "does not provide an export named"npm install @e4a/pg-wasm@latest+ build + checkactionlint1.7.7 on both workflow filesgh, both branchesThe only check that runs on this PR is
Conventional Commit, which passes. There is no build CI yet, which is what #65 is about, so the table above is the stand-in: it runs the proposed workflows' own commands locally.One thing the canary will not tell you
I ran the canary steps by hand against what is published today. Every example still builds against
@e4a/pg-js2.3.3,@e4a/pg-wasm0.6.1 andE4A.PostGuard0.5.0, so the canary is green as of this PR.The issue's framing points the other way, so it is worth being precise about the gap. pg-node and pg-sveltekit pin
^1.10.0while 2.3.3 is current, and pg-dotnet pins 0.4.1 while 0.5.0 is current. Being pinned behind is not a build failure. The canary catches an SDK release that breaks the examples; it does not catch the examples drifting behind a current SDK, which is what happened here. I am flagging that rather than also building a version-drift check, since the issue asked for two specific jobs.Notes for whoever applies the YAML
sdk-canary.ymlneedsissues: write, scoped to the report job only.workflow_dispatchfailures too, not only scheduled ones. Manually dispatching a run that fails will open or update the issue.