Skip to content

feat(ci): add quality gates and automate the npm package release line - #36

Merged
kewang merged 5 commits into
masterfrom
feat/ci-and-npm-publish-automation
Aug 4, 2026
Merged

feat(ci): add quality gates and automate the npm package release line#36
kewang merged 5 commits into
masterfrom
feat/ci-and-npm-publish-automation

Conversation

@kewang

@kewang kewang commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Closes #16.

Why

Nothing ran tests, type checks, or lint automatically — all three existing workflows publish or deploy on a tag, none run on pull_request. Every gate was a command a human remembered to type.

That was hiding a live defect: packages/core/src/aggregate.test.ts failed to type-check with two TS2741s while npm test reported 60/60 passing. Two holes lined up — a root type-check covering only @spekjs/web, and package tsconfigs excluding their own tests while the suites run through tsx, which strips types without checking them.

Separately, @spekjs/core / @spekjs/ui were published entirely by hand, and neither version line had a single git tag.

What's here

Gatesci.yml on pull_request + push:[master]: Node gates (test, type check, lint, build, build:demo under NODE_ENV=production), the Kotlin suite on JDK 17, and a smoke test of the composite action.

Type check now covers core / ui / web / vscode and scripts/build-demo.ts and generate-badges.ts were named by no tsconfig at all, despite being what the action and the demo page execute. Test files are included via a per-package tsconfig.test.json rather than by deleting the exclude: those configs also drive the build, which emits into the published dist, so deleting it would ship compiled tests to consumers.

Lint is real for the first time. packages/web declared lint and format scripts while neither eslint nor prettier was installed and no config existed anywhere. Prettier is deliberately not added — it would rewrite nearly every file for no defect caught.

Publishingnpm-publish.yml publishes each package when its declared version differs from the registry, via Trusted Publishing (OIDC, no stored token, provenance automatic), then tags core-vX.Y.Z / ui-vX.Y.Z. An already-published version is a quiet skip, not a failure. Twelve backfill tags cover every version already on the registry.

The version decision stays in the release skill, read from the archived changes' Impact — never from commit prefixes, which get it wrong about half the time here (core 1.3.0 and 1.4.0 were both fix: commits that warranted a minor).

Bugs the new gates caught

  • vscode/panel.tsgetHtml() read index.webview.html on every render and discarded it
  • web/SearchDialog.tsx — the flatResults memo recomputed every render, its deps being freshly-built arrays
  • web/useOpenSpec.ts — an unused ref and an unused type import
  • web/useScrollspy.ts — effect now keyed on the joined ids instead of the unstable array (behavior identical; slugs cannot contain the separator)
  • vscode/extension.ts — two require() calls in a file already using ESM imports

Notes for review

  • The smoke job pins spek-version: ${{ github.sha }}. The action checks out spekhq/spek at that ref and builds from that copy, so the default master would test master's implementation and go green on a PR that breaks the action. Whether this resolves for a fork PR is the one open question — if it doesn't, the fallback is restricting the job to push: master.
  • npm-publish.yml's filename is registered with npm and matched exactly. Renaming it leaves a workflow that runs, resolves the version difference, and fails only at authentication, naming no cause.
  • Gates run inside the publish job rather than chaining on ci.yml via workflow_run — that trigger evaluates the workflow file from the default branch rather than the triggering commit, and getting its coupling wrong publishes on a red build, which cannot be undone.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PgfFLCcVjPxZJmMspbY4Bu

kewang and others added 3 commits August 4, 2026 23:48
Nothing ran tests, type checks, or lint automatically: all three existing workflows
publish or deploy on a tag, none run on pull_request (issue #16).

Add .github/workflows/ci.yml on pull_request + push:[master] with three jobs — the
Node gates (test, type check, lint, build, build:demo under NODE_ENV=production),
the Kotlin suite, and a smoke test of the composite action.

The type check previously covered only @spekjs/web, and core/ui excluded their own
tests from checking while the suites run through tsx, which strips types without
checking them. That pair of holes was hiding two live TS2741s in core's
aggregate.test.ts while npm test reported every test passing. Test files are now
checked through a separate tsconfig.test.json per package rather than by deleting
the exclude — those configs also drive the build, which emits into the published
dist, so deleting it would ship compiled tests to consumers.

scripts/build-demo.ts and scripts/generate-badges.ts were named by no tsconfig at
all, despite being what the composite action and the demo page execute. They now
have one. The root tsconfig.json — project references listing 2 of 4 packages,
invoked by nothing — is removed.

Lint is real for the first time: packages/web declared lint and format scripts
while neither eslint nor prettier was installed and no config existed. Prettier is
deliberately not added; it would rewrite nearly every file for no defect caught.

The smoke job pins spek-version to the commit under test — the action checks out
spekhq/spek at that ref and builds from that copy, so the default of "master" would
test master's action implementation and report green on a change that breaks it. It
asserts on file contents, not on outputs being set: an output is populated whether
or not the build produced anything, which is how the ui-dist break shipped.

Fixes found by the newly-enabled gates:
- core/aggregate.test.ts: missing vcs on two WorktreeInfo fixtures
- vscode/panel.ts: getHtml read index.webview.html on every render and discarded it
- vscode/extension.ts: two require() calls in a file already using ESM imports
- web/useOpenSpec.ts: an unused ref and an unused type import
- web/useScrollspy.ts: effect now keyed on the joined ids, no longer references the
  unstable array (behavior identical — slugs cannot contain the separator)
- web/SearchDialog.tsx: the filtered lists are memoized, so the flatResults memo
  they feed actually memoizes instead of recomputing every render

Refs #16

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PgfFLCcVjPxZJmMspbY4Bu
…ease tags

@spekjs/core and @spekjs/ui were published entirely by hand — bump, npm publish,
then a chore(npm) commit — and neither version line had a single git tag, so "what
changed between core 1.3.0 and 1.4.0" could not be answered from the repository.

Add .github/workflows/npm-publish.yml: on push to master it compares each package's
declared version against the registry and publishes the ones that differ, then
creates a core-vX.Y.Z / ui-vX.Y.Z tag. An already-published version is a quiet skip,
not a failure — nearly every push to master changes no version, and a workflow that
errors there would make red the normal state of master.

Authentication is npm Trusted Publishing (OIDC), so no npm token is stored in this
repository, and provenance is attached automatically. Three constraints are load-
bearing and documented in the file: the workflow's filename is registered with npm
and matched exactly (a rename fails only at authentication, naming no cause), the
publish steps cannot be factored into a reusable workflow (npm validates the calling
workflow's name), and npm must be upgraded first because the Node in .nvmrc bundles
one older than trusted publishing requires.

The gates run inside the publish job rather than chaining on ci.yml via workflow_run:
that trigger evaluates the workflow file from the default branch rather than the
triggering commit, and getting its coupling wrong publishes on a red build, which
cannot be undone.

Version detection checks membership of the full version list rather than
`npm view <pkg>@<version>`, whose E404 exit conflates "not published" with network,
auth and registry failures — and a failure read as "not published" is a failure read
as "publish it".

Backfill the twelve tags for every version already on the registry, each verified
against the version its commit actually declares. Three of them predate the
chore(npm) message convention, which is why the convention cannot be the anchor.

The release skill gains the step CI cannot do: deciding the increment. It reads the
archived changes' Impact rather than commit prefixes, which get this wrong about half
the time here — core 1.3.0 and 1.4.0 were both `fix:` commits that warranted a minor.

Refs #16

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PgfFLCcVjPxZJmMspbY4Bu
The first run of this workflow failed with "TS2307: Cannot find module
'@spekjs/ui'" plus a spray of implicit-any errors in the files importing it.

@spekjs/web type-checks against ui's dist/, not its sources, and `npm ci` does not
produce that dist: ui's build is deliberately on prepublishOnly, because a prepare
hook would run before npm creates the workspace symlinks and take the whole install
down. The job built core and stopped there.

Invisible locally — a dist from an earlier build is always lying around — and this
is the same trap that shipped the composite action broken, which the github-action
capability already carries a requirement for. design.md stated the constraint
correctly; the spec requirement and both workflows only implemented half of it.

Reproduced by deleting both dists: 50 type errors before, 0 after.

Widen the requirement from "build core" to "build every workspace package resolved
through its dist", with a scenario for the next package that acquires one, and fix
the same omission in npm-publish.yml.

Refs #16

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PgfFLCcVjPxZJmMspbY4Bu
kewang and others added 2 commits August 5, 2026 00:08
…po PR

spek-version pinned to github.sha resolves and the smoke job passes (PR #36). The
fork case is still unobserved and stays an open question, with the push:master
fallback recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PgfFLCcVjPxZJmMspbY4Bu
All three jobs green on #36. #37 confirmed the closed hole: Test passed 267
assertions, Type check then failed with the expected TS2741.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PgfFLCcVjPxZJmMspbY4Bu
@kewang
kewang merged commit 9d54ab4 into master Aug 4, 2026
3 checks passed
@kewang
kewang deleted the feat/ci-and-npm-publish-automation branch August 4, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No CI: nothing runs tests or type-checks; type-check only covers packages/web

1 participant