From 09f1289593ead04ba7d3b8439dec02106a5365fa Mon Sep 17 00:00:00 2001 From: kewang Date: Wed, 5 Aug 2026 10:45:19 +0800 Subject: [PATCH] chore(openspec): archive add-ci-and-npm-publish-automation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automatic sync succeeded — 14 requirements written into two new main specs, no manual delta application needed. openspec validate --specs: 50/50. - continuous-integration: 9 requirements, 22 scenarios - npm-package-cicd: 5 requirements, 14 scenarios Task 6.8 (end-to-end publish verification) was removed rather than left unchecked. Reaching the OIDC authentication step requires an actual npm publish, and neither package's src/ had changed since its last release, so under this change's own rules neither was due for one — publishing an identical tarball under a new number purely to exercise the pipeline is not a trade worth making against a registry that cannot be un-published. design.md records what is verified (version comparison, check job on a real runner, job-to-job outputs, the skip path running for real on master) and what is not, plus why finding out late is cheap: a failed publish leaves the registry untouched, and the tag is created only after a successful one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PgfFLCcVjPxZJmMspbY4Bu --- .../.openspec.yaml | 0 .../design.md | 29 +++ .../proposal.md | 0 .../specs/continuous-integration/spec.md | 0 .../specs/npm-package-cicd/spec.md | 0 .../tasks.md | 2 - openspec/specs/continuous-integration/spec.md | 219 ++++++++++++++++++ openspec/specs/npm-package-cicd/spec.md | 156 +++++++++++++ 8 files changed, 404 insertions(+), 2 deletions(-) rename openspec/changes/{add-ci-and-npm-publish-automation => archive/2026-08-05-add-ci-and-npm-publish-automation}/.openspec.yaml (100%) rename openspec/changes/{add-ci-and-npm-publish-automation => archive/2026-08-05-add-ci-and-npm-publish-automation}/design.md (88%) rename openspec/changes/{add-ci-and-npm-publish-automation => archive/2026-08-05-add-ci-and-npm-publish-automation}/proposal.md (100%) rename openspec/changes/{add-ci-and-npm-publish-automation => archive/2026-08-05-add-ci-and-npm-publish-automation}/specs/continuous-integration/spec.md (100%) rename openspec/changes/{add-ci-and-npm-publish-automation => archive/2026-08-05-add-ci-and-npm-publish-automation}/specs/npm-package-cicd/spec.md (100%) rename openspec/changes/{add-ci-and-npm-publish-automation => archive/2026-08-05-add-ci-and-npm-publish-automation}/tasks.md (97%) create mode 100644 openspec/specs/continuous-integration/spec.md create mode 100644 openspec/specs/npm-package-cicd/spec.md diff --git a/openspec/changes/add-ci-and-npm-publish-automation/.openspec.yaml b/openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/.openspec.yaml similarity index 100% rename from openspec/changes/add-ci-and-npm-publish-automation/.openspec.yaml rename to openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/.openspec.yaml diff --git a/openspec/changes/add-ci-and-npm-publish-automation/design.md b/openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/design.md similarity index 88% rename from openspec/changes/add-ci-and-npm-publish-automation/design.md rename to openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/design.md index 8b4ba3b..13a1f90 100644 --- a/openspec/changes/add-ci-and-npm-publish-automation/design.md +++ b/openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/design.md @@ -244,6 +244,35 @@ serializes runs; the version-diff check makes a second run a quiet skip rather t Rollback: each step is a file deletion or a tag that nothing reads. A failed publish leaves the registry untouched, since the tag is created only after a successful publish. +## Verified at implementation time + +- **The gates run and catch what they exist to catch.** PR #36's first run failed on this workflow's + own missing ui build; a throwaway PR (#37, closed unmerged) confirmed the original hole is closed — + `Test` passed 267 assertions and `Type check` then failed with the expected `TS2741`. +- **The smoke job's assertions** were exercised against five shapes (valid output, empty HTML, no + badge, missing file, empty output value), and the causal chain confirmed: with `ui/dist` removed, + `build-demo.ts` exits 1 and produces nothing. +- **The publish workflow's skip path ran for real** when #36 merged: both packages were detected as + already on the registry, the publish job was skipped, and the run was green. Nothing was published + and no tag was created. +- **`spek-version: ${{ github.sha }}` resolves** on a same-repository pull request. + +## Knowingly unverified + +**The OIDC authentication step has never executed.** Reaching it requires an actual `npm publish`, +and neither package's `src/` had changed since its last release — so under this change's own rules +neither was due for one, and publishing an identical tarball under a new number purely to test the +pipeline is not a trade worth making against a registry that cannot be un-published. + +What *is* verified is everything up to that point: the version comparison against the real registry +(across four cases), the check job on a real runner, the job-to-job output passing, and the skip +path. What remains unknown is whether the trusted-publisher registration and the npm upgrade are +correct. + +The cost of finding out late is bounded: a failed publish leaves the registry untouched, and the tag +is created only after a successful publish, so a first-release failure means re-running the workflow +rather than cleaning anything up. This is why the verification was dropped rather than forced. + ## Open Questions - Whether the smoke job's `spek-version: ${{ github.sha }}` resolves for a fork pull request. Decide diff --git a/openspec/changes/add-ci-and-npm-publish-automation/proposal.md b/openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/proposal.md similarity index 100% rename from openspec/changes/add-ci-and-npm-publish-automation/proposal.md rename to openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/proposal.md diff --git a/openspec/changes/add-ci-and-npm-publish-automation/specs/continuous-integration/spec.md b/openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/specs/continuous-integration/spec.md similarity index 100% rename from openspec/changes/add-ci-and-npm-publish-automation/specs/continuous-integration/spec.md rename to openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/specs/continuous-integration/spec.md diff --git a/openspec/changes/add-ci-and-npm-publish-automation/specs/npm-package-cicd/spec.md b/openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/specs/npm-package-cicd/spec.md similarity index 100% rename from openspec/changes/add-ci-and-npm-publish-automation/specs/npm-package-cicd/spec.md rename to openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/specs/npm-package-cicd/spec.md diff --git a/openspec/changes/add-ci-and-npm-publish-automation/tasks.md b/openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/tasks.md similarity index 97% rename from openspec/changes/add-ci-and-npm-publish-automation/tasks.md rename to openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/tasks.md index 04ed6d9..aececb9 100644 --- a/openspec/changes/add-ci-and-npm-publish-automation/tasks.md +++ b/openspec/changes/archive/2026-08-05-add-ci-and-npm-publish-automation/tasks.md @@ -86,8 +86,6 @@ annotated tag. Create no tag when the package was skipped. - [x] 6.7 Keep the workflow flat — no reusable-workflow indirection, which breaks npm's OIDC filename validation. -- [ ] 6.8 Verify end to end with a deliberate patch release of one package: the publish succeeds, - the registry shows a provenance attestation, and the tag lands on the right commit. ## 7. Extend the release skill to the package line diff --git a/openspec/specs/continuous-integration/spec.md b/openspec/specs/continuous-integration/spec.md new file mode 100644 index 0000000..cfe0de9 --- /dev/null +++ b/openspec/specs/continuous-integration/spec.md @@ -0,0 +1,219 @@ +# continuous-integration Specification + +## Purpose +TBD - created by archiving change add-ci-and-npm-publish-automation. Update Purpose after archive. +## Requirements +### Requirement: Quality gates run on every pull request and every push to master + +The repository SHALL provide a GitHub Actions workflow that runs the quality gates — tests, type +checking, lint, and builds — on every `pull_request` and on every `push` to `master`. + +The workflow SHALL NOT be limited to tag pushes. Every other workflow in this repository is a +publish pipeline gated on a tag; a gate that only runs at release time cannot prevent a defect from +reaching `master`, which is what `spek-version: master` (the composite action's default) exposes to +consumers. + +#### Scenario: Pull request opened + +- **WHEN** a pull request is opened or updated against `master` +- **THEN** the workflow runs and reports a status on the pull request + +#### Scenario: Push to master + +- **WHEN** a commit is pushed to `master` +- **THEN** the workflow runs against that commit + +#### Scenario: Failing gate blocks the report + +- **WHEN** any gate — test, type check, lint, or build — fails +- **THEN** the workflow is marked failed + +### Requirement: Every workspace package resolved through its dist is built before the gates + +The workflow SHALL build **both** `@spekjs/core` and `@spekjs/ui` after installing dependencies and +before running the type check or any build that imports them. It SHALL NOT assume that installing +dependencies produced either one. + +Both packages' entry points are `dist/`, so `@spekjs/web` type-checks against +`packages/core/dist/*.d.ts` and `packages/ui/dist/*.d.ts` rather than against their sources. Neither +`dist` survives a fresh `npm ci`: core's build is on `prepare`, and ui's is deliberately on +`prepublishOnly`, because a `prepare` hook would run before npm creates the workspace symlinks and +take the whole install down (see `ui-package`). + +The failure is invisible to local runs, where a `dist` from an earlier build is always present, and +it does not name its cause — it surfaces as `TS2307: Cannot find module '@spekjs/ui'` plus a spray +of implicit-`any` errors in the files that imported it. + +#### Scenario: Fresh runner checkout + +- **WHEN** the workflow runs on a runner with no pre-existing `packages/core/dist` or + `packages/ui/dist` +- **THEN** both packages are built before the type check runs +- **AND** the type check reports only errors attributable to the checked-out code + +#### Scenario: A newly added workspace package resolved through dist + +- **WHEN** a workspace package that resolves through a built `dist` is added and imported by another + package that the gates check +- **THEN** the workflow builds it before the gates, rather than relying on an install-time hook + +### Requirement: The type check covers every TypeScript source in the repository + +The root `type-check` script SHALL check every workspace that contains TypeScript — `@spekjs/core`, +`@spekjs/ui`, `@spekjs/web`, and `spek-vscode` — and each of those workspaces SHALL expose its own +`type-check` script. It SHALL also cover the repository-root `scripts/` directory. + +A root script that checks a single workspace reports success for the monorepo while three of four +packages are unchecked. `@spekjs/core` and `@spekjs/ui` are published to the public registry, so an +unchecked package is one whose type errors reach consumers. + +`scripts/build-demo.ts` and `scripts/generate-badges.ts` are named by no `tsconfig.json` in the +repository, so nothing checks them at all — and they are what the composite action and the published +demo page actually execute. + +#### Scenario: Root type check covers all workspaces + +- **WHEN** `npm run type-check` is run at the repository root +- **THEN** `@spekjs/core`, `@spekjs/ui`, `@spekjs/web`, and `spek-vscode` are each type-checked + +#### Scenario: Root scripts are type-checked + +- **WHEN** `npm run type-check` is run at the repository root +- **THEN** the TypeScript files under `scripts/` are type-checked + +#### Scenario: A type error in any workspace fails the check + +- **WHEN** a type error is introduced in any one of those workspaces, or under `scripts/` +- **THEN** `npm run type-check` exits non-zero + +### Requirement: Type-checking test files does not change what is published + +Including test files in type checking SHALL NOT cause them to be emitted into any package's build +output. + +`@spekjs/core` and `@spekjs/ui` build with the same `tsconfig.json` that carries the test `exclude`, +and both emit to `dist` while declaring `files: ["dist"]`. Removing the exclude outright would put +compiled test files into the published tarball — trading a silent gap for a silent regression in +what consumers download. + +#### Scenario: Published tarball carries no tests + +- **WHEN** either package's tarball is inspected after this change +- **THEN** it contains no compiled test file + +#### Scenario: Build output carries no tests + +- **WHEN** `npm run build` completes for either package +- **THEN** `dist/` contains no compiled test file + +### Requirement: Test files are type-checked + +Every package's TypeScript configuration SHALL include that package's test files in type checking. +No package SHALL exclude its own tests from the type check. + +The test suites run through `tsx`, which strips types without checking them, so a type-invalid test +file runs green. Combined with an exclude, a broken test fixture is invisible to both gates at once: +`packages/core/src/aggregate.test.ts` currently fails to type-check with two `TS2741`s while +`npm test` reports every test passing, and the same pair of holes hid the `defaultSchema` breakage +in `packages/ui`. + +#### Scenario: Type-invalid test fixture fails the check + +- **WHEN** a test file constructs a fixture that does not satisfy the type it is passed as +- **THEN** `npm run type-check` exits non-zero, naming that test file + +#### Scenario: No package excludes its own tests + +- **WHEN** each package's `tsconfig.json` (or the project the type check runs) is inspected +- **THEN** none of them excludes that package's test files from type checking + +### Requirement: Lint is real and enforced + +The repository SHALL provide a working lint setup — a configuration and the tools it needs — exposed +through a root `lint` script and run by the workflow. A package SHALL NOT declare a `lint` or +`format` script that cannot execute. + +`packages/web` declares both today while neither `eslint` nor `prettier` is installed and no +configuration exists anywhere in the repository. A script that exits with a "command not found" +class of error is worse than no script: it reads as a gate that exists. + +#### Scenario: Root lint runs + +- **WHEN** `npm run lint` is run at the repository root on a clean checkout after `npm ci` +- **THEN** the linter executes and reports results + +#### Scenario: Lint failure fails the workflow + +- **WHEN** the linter reports an error +- **THEN** the workflow is marked failed + +#### Scenario: No unrunnable script is declared + +- **WHEN** any workspace's `package.json` declares a `lint` or `format` script +- **THEN** that script executes successfully on a clean checkout after `npm ci` + +### Requirement: The Kotlin suite runs in CI + +The workflow SHALL run `packages/intellij`'s Gradle test suite on JDK 17, in a job separate from the +Node gates. + +`packages/intellij` re-implements the core scanning rules in Kotlin (`ArtifactDiscovery.kt`, +`SchemaOrder.kt`, `TaskParser.kt`), with its own tests under `src/test/kotlin`. Those rules are +aligned with the TypeScript ones by convention only — nothing links them — so the Kotlin suite is +the only thing that observes a divergence. + +#### Scenario: Gradle tests run + +- **WHEN** the workflow runs +- **THEN** `./gradlew test` executes in `packages/intellij` on JDK 17 + +#### Scenario: Kotlin test failure fails the workflow + +- **WHEN** a Kotlin test fails +- **THEN** the workflow is marked failed + +### Requirement: The composite action is smoke-tested + +The workflow SHALL invoke the composite action defined by `action.yml` against this repository, with +badge generation enabled, and SHALL assert that the files named by the `html-path` and `badges-path` +outputs exist and are non-empty. + +`action.yml` is the only shipped artifact with no test coverage, and it fails silently: moving +`@spekjs/ui`'s build from `prepare` to `prepublishOnly` removed the action's only source of ui +`dist` and the Marketplace action was broken for a full day with nothing raising an alarm. Asserting +that the outputs merely have values is not sufficient — a step output is set whether or not the +build produced anything. + +#### Scenario: Action produces the HTML + +- **WHEN** the smoke job runs the action against this repository +- **THEN** the file at the `html-path` output exists and is non-empty + +#### Scenario: Action produces badges + +- **WHEN** the smoke job runs the action with `generate-badges: "true"` +- **THEN** the directory at the `badges-path` output exists and contains at least one badge file + +#### Scenario: Broken build chain fails the job + +- **WHEN** the action's build chain stops producing a workspace package's `dist` +- **THEN** the smoke job fails rather than reporting success with an empty output + +### Requirement: The gates are runnable locally by the same commands + +Every gate the workflow runs SHALL be invocable locally through a documented root npm script, and +the workflow SHALL invoke those same scripts rather than reimplementing a gate inline. + +A gate that only exists inside a workflow file cannot be reproduced by a contributor before pushing, +and drifts from what the scripts do. + +#### Scenario: Contributor reproduces CI locally + +- **WHEN** a contributor runs the documented root scripts for test, type check, lint, and build +- **THEN** they exercise the same gates the workflow runs + +#### Scenario: Gates are documented + +- **WHEN** a contributor reads `CONTRIBUTING.md` +- **THEN** it names the commands that must pass before opening a pull request + diff --git a/openspec/specs/npm-package-cicd/spec.md b/openspec/specs/npm-package-cicd/spec.md new file mode 100644 index 0000000..a506436 --- /dev/null +++ b/openspec/specs/npm-package-cicd/spec.md @@ -0,0 +1,156 @@ +# npm-package-cicd Specification + +## Purpose +TBD - created by archiving change add-ci-and-npm-publish-automation. Update Purpose after archive. +## Requirements +### Requirement: Publishing is triggered by a version difference against the registry + +The repository SHALL provide a GitHub Actions workflow that, on every `push` to `master`, compares +each publishable package's declared `version` against the version published on the npm registry, and +publishes that package when the two differ. + +`@spekjs/core` and `@spekjs/ui` SHALL be evaluated independently: one may publish while the other +does not. + +The trigger is the version field rather than a dedicated tag because the version bump is already a +deliberate, reviewed act performed by the release flow; requiring a second signal adds a step that +can be forgotten without adding information. + +#### Scenario: Version differs from the registry + +- **WHEN** a commit lands on `master` in which `packages/core/package.json` declares a version not + present on the registry +- **THEN** `@spekjs/core` is published at that version + +#### Scenario: Packages evaluated independently + +- **WHEN** only `@spekjs/ui`'s version differs from the registry +- **THEN** `@spekjs/ui` is published and `@spekjs/core` is not + +#### Scenario: Already-published version is skipped quietly + +- **WHEN** a commit lands on `master` in which a package's declared version is already on the + registry +- **THEN** that package is not published +- **AND** the workflow succeeds rather than failing + +The skip must not fail. Most pushes to `master` change neither version, so a workflow that errors on +"already published" would report a red `master` as its normal state, and a genuine failure would be +indistinguishable from the noise. + +### Requirement: Publishing authenticates through OIDC, not a stored token + +The workflow SHALL authenticate to the npm registry using npm Trusted Publishing (OIDC). It SHALL +NOT read a long-lived npm token from repository secrets. + +The job SHALL request the `id-token: write` permission, and SHALL ensure an npm CLI new enough to +perform a trusted publish before invoking `npm publish` — the Node version pinned by `.nvmrc` ships +an npm older than the minimum, so relying on the bundled npm fails. + +Trusted publishing attaches a provenance attestation automatically. Both packages already declare +the `repository` URL and `directory` that provenance requires. + +#### Scenario: No npm token in the repository + +- **WHEN** the repository's secrets and workflow files are inspected +- **THEN** no npm authentication token is stored or referenced + +#### Scenario: Publish carries provenance + +- **WHEN** a package is published by the workflow +- **THEN** the published version carries a provenance attestation linking it to the workflow run and + commit that produced it + +#### Scenario: npm CLI too old to publish + +- **WHEN** the workflow runs on a Node version whose bundled npm predates trusted publishing support +- **THEN** the workflow installs a new enough npm before publishing, rather than failing at the + publish step + +### Requirement: The publishing workflow's filename is part of its configuration + +The publishing workflow's filename SHALL be treated as configuration shared with the npm registry +and SHALL NOT be renamed without re-registering it. The repository SHALL record this constraint +where someone renaming the file would encounter it. + +A trusted publisher is registered on npmjs.com against a specific repository *and workflow +filename*, matched exactly. Renaming the file leaves a workflow that still runs, still resolves the +version difference, and fails only at the authentication step — with no indication that a rename +caused it. + +#### Scenario: Constraint is discoverable at the file + +- **WHEN** a maintainer opens the publishing workflow +- **THEN** the file states that its name is registered with the npm registry and must not be changed + without re-registering + +### Requirement: A successful publish creates a release tag + +On a successful publish, the workflow SHALL create and push an annotated tag naming the package and +the published version, distinct from the product `v*` tag namespace — `core-vX.Y.Z` for +`@spekjs/core` and `ui-vX.Y.Z` for `@spekjs/ui`. + +Neither package line has any tag today, so "what changed between core 1.3.0 and 1.4.0" cannot be +answered from the repository; the only available anchor is a `chore(npm): publish …` commit-message +convention, which nothing enforces. + +The repository SHALL also carry tags for the versions published before this workflow existed, so the +tags describe the entire published history rather than starting mid-stream. + +#### Scenario: Tag created on publish + +- **WHEN** `@spekjs/core` is published at 1.4.1 by the workflow +- **THEN** an annotated tag `core-v1.4.1` is created on the published commit and pushed to origin + +#### Scenario: No tag when nothing is published + +- **WHEN** the workflow skips a package because its version is already on the registry +- **THEN** no tag is created for that package + +#### Scenario: Historical versions are tagged + +- **WHEN** the tags are listed after this change is implemented +- **THEN** every previously published version of both packages has a corresponding tag on the commit + that published it + +#### Scenario: Product tags are unaffected + +- **WHEN** package tags are created +- **THEN** the product `v*` tag namespace is untouched, and no existing tag is moved or deleted + +### Requirement: The version bump is decided by the release flow, not derived from commit messages + +The choice of version increment SHALL be made by the release flow — which reads the archived +changes' stated impact — and SHALL NOT be inferred from commit message prefixes. + +This repository's own history shows the inference is unreliable. `@spekjs/core` 1.3.0 came from a +single `fix(core,ui):` commit that added the `./graph-node-id` export subpath, and 1.4.0 from three +`fix:` / `test:` commits that changed `TaskItem.text`'s semantics for consumers. A +conventional-commit rule would have published 1.2.1 and 1.3.1 — under-bumping two of the last four +core releases. A published version cannot be withdrawn, so an under-bump is corrected only by +publishing again. + +This is why the project convention requires a change that affects registry consumers — a new public +export, a behavior change, additive-so-minor-not-patch — to record that in its proposal or design +for whoever cuts the release. + +#### Scenario: Release flow covers the package line + +- **WHEN** the release flow runs +- **THEN** it checks, for each publishable package, whether that package's sources changed since its + last release tag +- **AND** for each package that changed, it decides the version increment, updates that package's + CHANGELOG, and bumps the version + +#### Scenario: Additive change is not published as a patch + +- **WHEN** a change adds a new public export or alters behavior observable by a registry consumer, + under a commit whose prefix is `fix:` +- **THEN** the release flow selects a minor increment, on the evidence in the change's artifacts + rather than the commit prefix + +#### Scenario: No package changes since the last release + +- **WHEN** neither package's sources changed since its last release tag +- **THEN** the release flow bumps neither package and the publish workflow has nothing to do +