@intelmesh/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.github/scripts/compute-disttag.sh +47 -0
  2. package/.github/workflows/release.yml +206 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +2 -0
  5. package/.prettierrc +8 -0
  6. package/CLAUDE.md +37 -0
  7. package/LICENSE +21 -0
  8. package/commitlint.config.cjs +3 -0
  9. package/dist/index.d.ts +1293 -0
  10. package/dist/index.js +1651 -0
  11. package/docs/superpowers/plans/2026-04-10-release-pipeline.md +798 -0
  12. package/docs/superpowers/specs/2026-04-10-release-pipeline-design.md +309 -0
  13. package/eslint.config.mjs +38 -0
  14. package/package.json +72 -0
  15. package/src/builders/event.ts +72 -0
  16. package/src/builders/rule.ts +143 -0
  17. package/src/client/errors.ts +171 -0
  18. package/src/client/http.ts +209 -0
  19. package/src/client/intelmesh.ts +57 -0
  20. package/src/client/pagination.ts +50 -0
  21. package/src/generated/types.ts +11 -0
  22. package/src/index.ts +106 -0
  23. package/src/provision/index.ts +6 -0
  24. package/src/provision/provisioner.ts +326 -0
  25. package/src/provision/rule-builder.ts +193 -0
  26. package/src/resources/apikeys.ts +63 -0
  27. package/src/resources/audit.ts +29 -0
  28. package/src/resources/evaluations.ts +38 -0
  29. package/src/resources/events.ts +61 -0
  30. package/src/resources/lists.ts +91 -0
  31. package/src/resources/phases.ts +71 -0
  32. package/src/resources/rules.ts +98 -0
  33. package/src/resources/scopes.ts +71 -0
  34. package/src/resources/scores.ts +63 -0
  35. package/src/testkit/assertion.ts +76 -0
  36. package/src/testkit/harness.ts +252 -0
  37. package/src/testkit/index.ts +7 -0
  38. package/src/types.ts +330 -0
  39. package/tests/client/errors.test.ts +159 -0
  40. package/tests/provision/provisioner.test.ts +311 -0
  41. package/tests/scripts/compute-disttag.test.ts +178 -0
  42. package/tests/testkit/harness.test.ts +291 -0
  43. package/tsconfig.eslint.json +8 -0
  44. package/tsconfig.json +29 -0
  45. package/vitest.config.ts +14 -0
@@ -0,0 +1,798 @@
1
+ # Release Pipeline Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Ship a GitHub Actions workflow that publishes `@intelmesh/sdk` to npm (with provenance) and creates a GitHub Release on every signed semver tag, with signature verification of both the commit and the annotated tag object as a hard gate.
6
+
7
+ **Architecture:** Single workflow file (`.github/workflows/release.yml`) with four sequential jobs chained via `needs:` — `verify-signature` → `test` (Node 20/22/24 matrix) → `publish-npm` → `github-release`. The npm dist-tag logic is extracted into a standalone bash script (`.github/scripts/compute-disttag.sh`) with vitest unit tests, so the trickiest piece of the pipeline is deterministically covered before any tag is pushed.
8
+
9
+ **Tech Stack:** GitHub Actions, `gh` CLI (pre-installed on runners), `npm publish --provenance`, actionlint (binary, downloaded ad-hoc for local validation), vitest (already in the repo), bash.
10
+
11
+ **Reference spec:** `docs/superpowers/specs/2026-04-10-release-pipeline-design.md` (all design decisions live there; this plan implements them literally).
12
+
13
+ **User preference — important:** Do NOT `git add` or commit anything under `docs/`. The plan document itself is not committed. Code changes outside `docs/` are committed normally.
14
+
15
+ ---
16
+
17
+ ## File Structure
18
+
19
+ | File | State | Responsibility |
20
+ |---|---|---|
21
+ | `package.json` | modify | Add missing `repository` field (required for npm provenance). |
22
+ | `.gitignore` | modify | Ignore `.tools/` directory where local actionlint binary is downloaded. |
23
+ | `.github/scripts/compute-disttag.sh` | create | Pure bash: read `GITHUB_REF_NAME`, emit `version`, `disttag`, `is_prerelease` to `GITHUB_OUTPUT`. No dependencies. Testable. |
24
+ | `tests/scripts/compute-disttag.test.ts` | create | Vitest test that execs the bash script in a temp dir and asserts outputs for stable/beta/rc/alpha/next and no-dot-suffix cases. |
25
+ | `.github/workflows/release.yml` | create | Single-file workflow: trigger, permissions, concurrency, and four jobs. |
26
+
27
+ ---
28
+
29
+ ## Task 1: Prerequisites — `repository` field and `.gitignore` entry
30
+
31
+ **Context:** `npm publish --provenance` requires `package.json` to have a `repository` field pointing to the GitHub repo — the published package's provenance attestation links back to this URL. We also prep `.gitignore` for the actionlint binary we'll download in Task 3.
32
+
33
+ **Files:**
34
+ - Modify: `package.json`
35
+ - Modify: `.gitignore`
36
+
37
+ - [ ] **Step 1.1: Read current `package.json`**
38
+
39
+ Run: `cat package.json` (using Read tool in agent).
40
+
41
+ Confirm: no `"repository"` key exists at the top level.
42
+
43
+ - [ ] **Step 1.2: Add `repository` field to `package.json`**
44
+
45
+ Add this block right after the `"license": "MIT",` line:
46
+
47
+ ```json
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/intelmesh/intelmesh-sdk-ts.git"
51
+ },
52
+ "homepage": "https://github.com/intelmesh/intelmesh-sdk-ts#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/intelmesh/intelmesh-sdk-ts/issues"
55
+ },
56
+ ```
57
+
58
+ The final ordering should place `repository`, `homepage`, `bugs` right after `license` and before `devDependencies`. Preserve existing formatting (2-space indent, trailing comma style of surrounding code).
59
+
60
+ - [ ] **Step 1.3: Validate JSON is still parseable**
61
+
62
+ Run: `node -e "JSON.parse(require('fs').readFileSync('package.json','utf8')); console.log('ok')"`
63
+ Expected output: `ok`
64
+
65
+ - [ ] **Step 1.4: Append `.tools/` to `.gitignore`**
66
+
67
+ Edit `.gitignore`, add a new block at the end:
68
+
69
+ ```
70
+ # Local tooling binaries (actionlint, etc)
71
+ .tools/
72
+ ```
73
+
74
+ - [ ] **Step 1.5: Commit**
75
+
76
+ ```bash
77
+ git add package.json .gitignore
78
+ git commit -m "chore: add repository metadata for npm provenance
79
+
80
+ Adds repository, homepage, and bugs fields required by
81
+ npm publish --provenance to attach Sigstore attestation,
82
+ and ignores .tools/ where actionlint will be downloaded
83
+ for local workflow validation."
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Task 2: Extract `compute-disttag.sh` with TDD
89
+
90
+ **Context:** The dist-tag computation (`v1.2.3` → `latest`, `v1.2.3-beta.1` → `beta`, etc.) is the most logic-heavy piece of the workflow. Extract it to a standalone bash script so it can be unit-tested with vitest before being wired into the YAML. The script reads `GITHUB_REF_NAME` and writes to `GITHUB_OUTPUT` — identical ABI to an inline workflow step, just testable.
91
+
92
+ **Files:**
93
+ - Create: `.github/scripts/compute-disttag.sh`
94
+ - Create: `tests/scripts/compute-disttag.test.ts`
95
+
96
+ - [ ] **Step 2.1: Write the failing test**
97
+
98
+ Create `tests/scripts/compute-disttag.test.ts`:
99
+
100
+ ```typescript
101
+ import { describe, expect, it } from 'vitest';
102
+ // eslint-disable-next-line security/detect-child-process
103
+ import { spawnSync } from 'node:child_process';
104
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
105
+ import { tmpdir } from 'node:os';
106
+ import { join } from 'node:path';
107
+
108
+ interface ScriptOutput {
109
+ version: string;
110
+ disttag: string;
111
+ is_prerelease: string;
112
+ }
113
+
114
+ function parseOutput(raw: string): ScriptOutput {
115
+ const map: Record<string, string> = {};
116
+ for (const line of raw.split('\n')) {
117
+ const idx = line.indexOf('=');
118
+ if (idx === -1) continue;
119
+ map[line.slice(0, idx)] = line.slice(idx + 1);
120
+ }
121
+ return {
122
+ version: map.version ?? '',
123
+ disttag: map.disttag ?? '',
124
+ is_prerelease: map.is_prerelease ?? '',
125
+ };
126
+ }
127
+
128
+ function runScript(refName: string): ScriptOutput {
129
+ const dir = mkdtempSync(join(tmpdir(), 'disttag-'));
130
+ const outputFile = join(dir, 'github_output');
131
+ writeFileSync(outputFile, '');
132
+ const result = spawnSync('bash', ['.github/scripts/compute-disttag.sh'], {
133
+ env: {
134
+ ...process.env,
135
+ GITHUB_REF_NAME: refName,
136
+ GITHUB_OUTPUT: outputFile,
137
+ },
138
+ encoding: 'utf-8',
139
+ });
140
+ if (result.status !== 0) {
141
+ throw new Error(
142
+ `Script failed (status=${String(result.status)}): ${result.stderr}`,
143
+ );
144
+ }
145
+ return parseOutput(readFileSync(outputFile, 'utf-8'));
146
+ }
147
+
148
+ describe('compute-disttag.sh', () => {
149
+ it('stable v1.2.3 resolves to latest', () => {
150
+ const out = runScript('v1.2.3');
151
+ expect(out.version).toBe('1.2.3');
152
+ expect(out.disttag).toBe('latest');
153
+ expect(out.is_prerelease).toBe('false');
154
+ });
155
+
156
+ it('v1.2.3-beta.1 resolves to beta dist-tag', () => {
157
+ const out = runScript('v1.2.3-beta.1');
158
+ expect(out.version).toBe('1.2.3-beta.1');
159
+ expect(out.disttag).toBe('beta');
160
+ expect(out.is_prerelease).toBe('true');
161
+ });
162
+
163
+ it('v1.2.3-rc.2 resolves to rc dist-tag', () => {
164
+ const out = runScript('v1.2.3-rc.2');
165
+ expect(out.disttag).toBe('rc');
166
+ expect(out.is_prerelease).toBe('true');
167
+ });
168
+
169
+ it('v2.0.0-alpha.7 resolves to alpha dist-tag', () => {
170
+ const out = runScript('v2.0.0-alpha.7');
171
+ expect(out.disttag).toBe('alpha');
172
+ expect(out.is_prerelease).toBe('true');
173
+ });
174
+
175
+ it('v2.0.0-next.0 resolves to next dist-tag', () => {
176
+ const out = runScript('v2.0.0-next.0');
177
+ expect(out.disttag).toBe('next');
178
+ expect(out.is_prerelease).toBe('true');
179
+ });
180
+
181
+ it('v1.0.0-beta without dot suffix resolves to beta', () => {
182
+ const out = runScript('v1.0.0-beta');
183
+ expect(out.version).toBe('1.0.0-beta');
184
+ expect(out.disttag).toBe('beta');
185
+ expect(out.is_prerelease).toBe('true');
186
+ });
187
+ });
188
+ ```
189
+
190
+ - [ ] **Step 2.2: Run the test — verify it fails because the script does not exist**
191
+
192
+ Run: `npm test -- tests/scripts/compute-disttag.test.ts`
193
+
194
+ Expected: all 6 tests fail with an error like `Script failed (status=127): bash: .github/scripts/compute-disttag.sh: No such file or directory`.
195
+
196
+ - [ ] **Step 2.3: Create the script**
197
+
198
+ Create `.github/scripts/compute-disttag.sh`:
199
+
200
+ ```bash
201
+ #!/usr/bin/env bash
202
+ # Compute npm dist-tag and prerelease flag from a git tag name.
203
+ #
204
+ # Reads (env):
205
+ # GITHUB_REF_NAME git tag name, e.g. "v1.2.3" or "v1.2.3-beta.1"
206
+ # GITHUB_OUTPUT path to GitHub Actions step output file
207
+ #
208
+ # Writes to $GITHUB_OUTPUT:
209
+ # version semver without leading "v"
210
+ # disttag npm dist-tag ("latest" for stable, suffix prefix otherwise)
211
+ # is_prerelease "true" for any tag with a "-" suffix, else "false"
212
+
213
+ set -euo pipefail
214
+
215
+ tag="${GITHUB_REF_NAME:?GITHUB_REF_NAME is required}"
216
+ : "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}"
217
+
218
+ version="${tag#v}"
219
+
220
+ if [[ "$version" == *-* ]]; then
221
+ suffix="${version#*-}"
222
+ disttag="${suffix%%.*}"
223
+ is_prerelease=true
224
+ else
225
+ disttag="latest"
226
+ is_prerelease=false
227
+ fi
228
+
229
+ {
230
+ echo "version=$version"
231
+ echo "disttag=$disttag"
232
+ echo "is_prerelease=$is_prerelease"
233
+ } >> "$GITHUB_OUTPUT"
234
+
235
+ echo "Resolved: tag=$tag version=$version disttag=$disttag prerelease=$is_prerelease"
236
+ ```
237
+
238
+ Make it executable: `chmod +x .github/scripts/compute-disttag.sh`
239
+
240
+ - [ ] **Step 2.4: Run the test — verify it passes**
241
+
242
+ Run: `npm test -- tests/scripts/compute-disttag.test.ts`
243
+
244
+ Expected: all 6 tests pass.
245
+
246
+ - [ ] **Step 2.5: Run full test suite — verify nothing else broke**
247
+
248
+ Run: `npm test`
249
+
250
+ Expected: all tests across the repo pass.
251
+
252
+ - [ ] **Step 2.6: Run lint — verify the test file passes ESLint strict**
253
+
254
+ Run: `npm run lint`
255
+
256
+ Expected: no errors. If ESLint flags anything in the new test file (e.g., security plugin or jsdoc), fix inline with targeted disable comments scoped to the minimum line, re-run, confirm clean.
257
+
258
+ - [ ] **Step 2.7: Commit**
259
+
260
+ ```bash
261
+ git add .github/scripts/compute-disttag.sh tests/scripts/compute-disttag.test.ts
262
+ git commit -m "feat(ci): add compute-disttag script with tests
263
+
264
+ Extracts the tag-to-disttag mapping logic into a
265
+ standalone bash script so it can be unit-tested before
266
+ being wired into the release workflow. Handles stable
267
+ releases (-> latest), prereleases with .N suffix
268
+ (beta.1 -> beta), and prereleases without a dot
269
+ (beta -> beta)."
270
+ ```
271
+
272
+ ---
273
+
274
+ ## Task 3: Workflow skeleton and `verify-signature` job
275
+
276
+ **Context:** Create the workflow file with trigger, workflow-level permissions, concurrency, and the first job (`verify-signature`). After this task the file is a syntactically valid workflow that *only* verifies signatures — the remaining jobs are added in subsequent tasks. Also download actionlint for local validation.
277
+
278
+ **Files:**
279
+ - Create: `.github/workflows/release.yml`
280
+ - Create: `.tools/actionlint` (gitignored, not committed)
281
+
282
+ - [ ] **Step 3.1: Download actionlint binary into `.tools/`**
283
+
284
+ Run:
285
+
286
+ ```bash
287
+ mkdir -p .tools
288
+ cd .tools
289
+ bash <(curl -sSf https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 1.7.7 .
290
+ cd ..
291
+ ls -la .tools/actionlint
292
+ ```
293
+
294
+ Expected: `.tools/actionlint` exists and is executable. If the download script fails, fall back to:
295
+
296
+ ```bash
297
+ curl -sSL -o /tmp/actionlint.tgz \
298
+ https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz
299
+ tar -xzf /tmp/actionlint.tgz -C .tools actionlint
300
+ chmod +x .tools/actionlint
301
+ ```
302
+
303
+ Confirm: `.tools/actionlint --version` prints `1.7.7`.
304
+
305
+ - [ ] **Step 3.2: Create `.github/workflows/release.yml` with skeleton and verify-signature job**
306
+
307
+ Create the file with exactly this content:
308
+
309
+ ```yaml
310
+ name: release
311
+
312
+ on:
313
+ push:
314
+ tags:
315
+ - 'v[0-9]+.[0-9]+.[0-9]+'
316
+ - 'v[0-9]+.[0-9]+.[0-9]+-*'
317
+
318
+ permissions:
319
+ contents: write
320
+ id-token: write
321
+
322
+ concurrency:
323
+ group: release-${{ github.ref }}
324
+ cancel-in-progress: false
325
+
326
+ jobs:
327
+ verify-signature:
328
+ name: Verify commit and tag are signed
329
+ runs-on: ubuntu-24.04
330
+ steps:
331
+ - name: Verify commit signature
332
+ env:
333
+ GH_TOKEN: ${{ github.token }}
334
+ run: |
335
+ set -euo pipefail
336
+ commit_sha="${GITHUB_SHA}"
337
+ repo="${GITHUB_REPOSITORY}"
338
+
339
+ verified=$(gh api "repos/${repo}/commits/${commit_sha}" \
340
+ --jq '.commit.verification.verified')
341
+ reason=$(gh api "repos/${repo}/commits/${commit_sha}" \
342
+ --jq '.commit.verification.reason')
343
+
344
+ echo "commit=${commit_sha} verified=${verified} reason=${reason}"
345
+
346
+ if [ "${verified}" != "true" ]; then
347
+ echo "::error title=Unsigned commit::Commit ${commit_sha} is NOT verified by GitHub. Reason: ${reason}. Refusing to release."
348
+ exit 1
349
+ fi
350
+
351
+ - name: Verify tag is a signed annotated tag
352
+ env:
353
+ GH_TOKEN: ${{ github.token }}
354
+ run: |
355
+ set -euo pipefail
356
+ tag="${GITHUB_REF_NAME}"
357
+ repo="${GITHUB_REPOSITORY}"
358
+
359
+ ref_json=$(gh api "repos/${repo}/git/refs/tags/${tag}")
360
+ obj_type=$(echo "${ref_json}" | jq -r '.object.type')
361
+ obj_sha=$(echo "${ref_json}" | jq -r '.object.sha')
362
+
363
+ if [ "${obj_type}" != "tag" ]; then
364
+ echo "::error title=Lightweight tag::Tag ${tag} is a lightweight tag (object.type=${obj_type}). Use 'git tag -s ${tag}' to create a signed annotated tag. Refusing to release."
365
+ exit 1
366
+ fi
367
+
368
+ verified=$(gh api "repos/${repo}/git/tags/${obj_sha}" \
369
+ --jq '.verification.verified')
370
+ reason=$(gh api "repos/${repo}/git/tags/${obj_sha}" \
371
+ --jq '.verification.reason')
372
+
373
+ echo "tag=${tag} object_sha=${obj_sha} verified=${verified} reason=${reason}"
374
+
375
+ if [ "${verified}" != "true" ]; then
376
+ echo "::error title=Unsigned tag::Tag ${tag} signature is NOT verified by GitHub. Reason: ${reason}. Refusing to release."
377
+ exit 1
378
+ fi
379
+ ```
380
+
381
+ - [ ] **Step 3.3: Validate with actionlint**
382
+
383
+ Run: `./.tools/actionlint .github/workflows/release.yml`
384
+
385
+ Expected: no output (success). If actionlint reports issues, fix them inline and re-run until clean. Common issues:
386
+ - `shellcheck` warnings on the `run:` blocks — fix the bash per shellcheck's guidance.
387
+ - `github.token` should work; if flagged use `${{ secrets.GITHUB_TOKEN }}` instead.
388
+
389
+ - [ ] **Step 3.4: Verify YAML is parseable with Node**
390
+
391
+ Run:
392
+
393
+ ```bash
394
+ node -e "
395
+ const fs = require('node:fs');
396
+ const content = fs.readFileSync('.github/workflows/release.yml', 'utf8');
397
+ // naive sanity: starts with 'name:' and contains 'jobs:'
398
+ if (!content.startsWith('name: release')) throw new Error('bad header');
399
+ if (!content.includes('jobs:\n verify-signature:')) throw new Error('missing job');
400
+ console.log('ok');
401
+ "
402
+ ```
403
+
404
+ Expected output: `ok`.
405
+
406
+ - [ ] **Step 3.5: Commit**
407
+
408
+ ```bash
409
+ git add .github/workflows/release.yml
410
+ git commit -m "feat(ci): add release workflow skeleton with verify-signature job
411
+
412
+ Adds the release workflow entry point triggered by semver
413
+ tag pushes (v*.*.* and v*.*.*-*) with workflow-level
414
+ permissions (contents:write, id-token:write) and a
415
+ concurrency group per tag ref with cancel-in-progress
416
+ disabled.
417
+
418
+ The first job, verify-signature, queries the GitHub API
419
+ to confirm both the commit the tag points to and the
420
+ annotated tag object itself are cryptographically
421
+ verified. Lightweight tags, unsigned commits, and any
422
+ non-verified tag objects cause the job to fail hard with
423
+ a ::error:: annotation, blocking the rest of the
424
+ pipeline via needs: in subsequent tasks."
425
+ ```
426
+
427
+ Note: `.tools/actionlint` is NOT staged — it is in `.gitignore` (Task 1.4).
428
+
429
+ ---
430
+
431
+ ## Task 4: Add `test` job with Node version matrix
432
+
433
+ **Context:** Add the quality gate: lint, typecheck, tests, and build, running in parallel across Node 20, 22, 24. This job `needs: verify-signature` so it only runs if the signature check passed. All three matrix cells must succeed for downstream jobs to unblock.
434
+
435
+ **Files:**
436
+ - Modify: `.github/workflows/release.yml`
437
+
438
+ - [ ] **Step 4.1: Append the `test` job to `.github/workflows/release.yml`**
439
+
440
+ Add this block after the `verify-signature` job, inside the `jobs:` mapping, maintaining 2-space YAML indent:
441
+
442
+ ```yaml
443
+ test:
444
+ name: Test on Node ${{ matrix.node-version }}
445
+ needs: verify-signature
446
+ runs-on: ubuntu-24.04
447
+ strategy:
448
+ fail-fast: true
449
+ matrix:
450
+ node-version: ['20', '22', '24']
451
+ steps:
452
+ - name: Checkout
453
+ uses: actions/checkout@v5
454
+
455
+ - name: Setup Node ${{ matrix.node-version }}
456
+ uses: actions/setup-node@v5
457
+ with:
458
+ node-version: ${{ matrix.node-version }}
459
+ cache: 'npm'
460
+
461
+ - name: Install dependencies
462
+ run: npm ci
463
+
464
+ - name: Lint
465
+ run: npm run lint
466
+
467
+ - name: Typecheck
468
+ run: npm run typecheck
469
+
470
+ - name: Test
471
+ run: npm test
472
+
473
+ - name: Build
474
+ run: npm run build
475
+ ```
476
+
477
+ - [ ] **Step 4.2: Validate with actionlint**
478
+
479
+ Run: `./.tools/actionlint .github/workflows/release.yml`
480
+
481
+ Expected: no output. Fix any reported issues before continuing.
482
+
483
+ - [ ] **Step 4.3: Commit**
484
+
485
+ ```bash
486
+ git add .github/workflows/release.yml
487
+ git commit -m "feat(ci): add test matrix job to release workflow
488
+
489
+ Adds a test job that runs lint, typecheck, tests, and
490
+ build across Node 20, 22, and 24 in parallel. Chained
491
+ after verify-signature via needs: so unsigned tags never
492
+ burn CI minutes on tests. fail-fast is enabled: the
493
+ first matrix cell to fail aborts the others."
494
+ ```
495
+
496
+ ---
497
+
498
+ ## Task 5: Add `publish-npm` job
499
+
500
+ **Context:** Add the npm publish job. Uses the bash script from Task 2 to compute the dist-tag, includes the version-mismatch guard, and publishes with `--provenance --access public`. This job `needs: test` so it only runs if all three Node matrix cells passed.
501
+
502
+ **Files:**
503
+ - Modify: `.github/workflows/release.yml`
504
+
505
+ - [ ] **Step 5.1: Append the `publish-npm` job**
506
+
507
+ Add this block after the `test` job, inside the `jobs:` mapping:
508
+
509
+ ```yaml
510
+ publish-npm:
511
+ name: Publish to npm
512
+ needs: test
513
+ runs-on: ubuntu-24.04
514
+ outputs:
515
+ version: ${{ steps.disttag.outputs.version }}
516
+ disttag: ${{ steps.disttag.outputs.disttag }}
517
+ is_prerelease: ${{ steps.disttag.outputs.is_prerelease }}
518
+ steps:
519
+ - name: Checkout
520
+ uses: actions/checkout@v5
521
+
522
+ - name: Setup Node
523
+ uses: actions/setup-node@v5
524
+ with:
525
+ node-version: '22'
526
+ registry-url: 'https://registry.npmjs.org'
527
+ cache: 'npm'
528
+
529
+ - name: Install dependencies
530
+ run: npm ci
531
+
532
+ - name: Build
533
+ run: npm run build
534
+
535
+ - name: Compute dist-tag from git tag
536
+ id: disttag
537
+ run: bash .github/scripts/compute-disttag.sh
538
+
539
+ - name: Verify package.json version matches git tag
540
+ run: |
541
+ set -euo pipefail
542
+ pkg_version=$(node -p "require('./package.json').version")
543
+ git_version="${{ steps.disttag.outputs.version }}"
544
+ if [ "${pkg_version}" != "${git_version}" ]; then
545
+ echo "::error title=Version mismatch::package.json version (${pkg_version}) does not match git tag (${git_version}). Bump package.json before tagging."
546
+ exit 1
547
+ fi
548
+ echo "package.json and git tag both at ${pkg_version}"
549
+
550
+ - name: Publish to npm
551
+ env:
552
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
553
+ run: |
554
+ set -euo pipefail
555
+ npm publish \
556
+ --provenance \
557
+ --access public \
558
+ --tag "${{ steps.disttag.outputs.disttag }}"
559
+ ```
560
+
561
+ - [ ] **Step 5.2: Validate with actionlint**
562
+
563
+ Run: `./.tools/actionlint .github/workflows/release.yml`
564
+
565
+ Expected: no output. Fix any issues before continuing.
566
+
567
+ - [ ] **Step 5.3: Verify the script is referenced correctly**
568
+
569
+ Run:
570
+
571
+ ```bash
572
+ test -x .github/scripts/compute-disttag.sh && echo "script exists and is executable"
573
+ grep -q 'bash .github/scripts/compute-disttag.sh' .github/workflows/release.yml && echo "workflow references script"
574
+ ```
575
+
576
+ Expected:
577
+ ```
578
+ script exists and is executable
579
+ workflow references script
580
+ ```
581
+
582
+ - [ ] **Step 5.4: Commit**
583
+
584
+ ```bash
585
+ git add .github/workflows/release.yml
586
+ git commit -m "feat(ci): add publish-npm job with provenance
587
+
588
+ Adds the npm publish job chained after the test matrix.
589
+ Computes the npm dist-tag from the git tag via the
590
+ compute-disttag script, verifies package.json version
591
+ matches the git tag (fails hard on mismatch), then
592
+ publishes with --provenance --access public. Exposes
593
+ version, disttag, and is_prerelease as job outputs for
594
+ the github-release job to consume."
595
+ ```
596
+
597
+ ---
598
+
599
+ ## Task 6: Add `github-release` job
600
+
601
+ **Context:** Final job: creates a GitHub Release with the built tarball attached. Split into two mutually exclusive steps gated by `if:` on `needs.publish-npm.outputs.is_prerelease` — one for stable releases, one for prereleases (adds `--prerelease` flag). Auto-generates release notes from commits since the last tag.
602
+
603
+ **Files:**
604
+ - Modify: `.github/workflows/release.yml`
605
+
606
+ - [ ] **Step 6.1: Append the `github-release` job**
607
+
608
+ Add this block after the `publish-npm` job, inside the `jobs:` mapping:
609
+
610
+ ```yaml
611
+ github-release:
612
+ name: Create GitHub Release
613
+ needs: publish-npm
614
+ runs-on: ubuntu-24.04
615
+ steps:
616
+ - name: Checkout
617
+ uses: actions/checkout@v5
618
+ with:
619
+ fetch-depth: 0
620
+
621
+ - name: Setup Node
622
+ uses: actions/setup-node@v5
623
+ with:
624
+ node-version: '22'
625
+ cache: 'npm'
626
+
627
+ - name: Install dependencies
628
+ run: npm ci
629
+
630
+ - name: Build
631
+ run: npm run build
632
+
633
+ - name: Pack tarball
634
+ run: npm pack
635
+
636
+ - name: Create GitHub Release (stable)
637
+ if: needs.publish-npm.outputs.is_prerelease == 'false'
638
+ env:
639
+ GH_TOKEN: ${{ github.token }}
640
+ run: |
641
+ set -euo pipefail
642
+ gh release create "${GITHUB_REF_NAME}" \
643
+ ./intelmesh-sdk-*.tgz \
644
+ --title "${GITHUB_REF_NAME}" \
645
+ --generate-notes
646
+
647
+ - name: Create GitHub Release (prerelease)
648
+ if: needs.publish-npm.outputs.is_prerelease == 'true'
649
+ env:
650
+ GH_TOKEN: ${{ github.token }}
651
+ run: |
652
+ set -euo pipefail
653
+ gh release create "${GITHUB_REF_NAME}" \
654
+ ./intelmesh-sdk-*.tgz \
655
+ --title "${GITHUB_REF_NAME}" \
656
+ --generate-notes \
657
+ --prerelease
658
+ ```
659
+
660
+ - [ ] **Step 6.2: Validate with actionlint**
661
+
662
+ Run: `./.tools/actionlint .github/workflows/release.yml`
663
+
664
+ Expected: no output. Fix any issues before continuing.
665
+
666
+ - [ ] **Step 6.3: Sanity check — count jobs in final file**
667
+
668
+ Run:
669
+
670
+ ```bash
671
+ grep -cE '^ [a-z][a-z-]+:$' .github/workflows/release.yml
672
+ ```
673
+
674
+ Expected: `4` (four jobs: verify-signature, test, publish-npm, github-release).
675
+
676
+ Also verify the needs chain:
677
+
678
+ ```bash
679
+ grep -n 'needs:' .github/workflows/release.yml
680
+ ```
681
+
682
+ Expected: three `needs:` lines — `verify-signature` (on test), `test` (on publish-npm), `publish-npm` (on github-release).
683
+
684
+ - [ ] **Step 6.4: Commit**
685
+
686
+ ```bash
687
+ git add .github/workflows/release.yml
688
+ git commit -m "feat(ci): add github-release job completing the pipeline
689
+
690
+ Adds the final job that npm-packs the built SDK and
691
+ creates a GitHub Release with the .tgz attached. Split
692
+ into two mutually exclusive steps gated by if: on the
693
+ publish-npm.is_prerelease output — stable releases use
694
+ default flags, prereleases add --prerelease so they
695
+ don't pollute /releases/latest. Release notes are
696
+ auto-generated from commits since the previous tag via
697
+ gh release --generate-notes, which needs fetch-depth: 0
698
+ on checkout."
699
+ ```
700
+
701
+ ---
702
+
703
+ ## Task 7: Final full-workflow actionlint + manual review
704
+
705
+ **Context:** The workflow is complete. Run the linter one last time on the full file, then do a manual sanity sweep against the spec.
706
+
707
+ **Files:**
708
+ - Read only: `.github/workflows/release.yml`, `docs/superpowers/specs/2026-04-10-release-pipeline-design.md`
709
+
710
+ - [ ] **Step 7.1: Run actionlint with verbose output**
711
+
712
+ Run: `./.tools/actionlint -verbose .github/workflows/release.yml`
713
+
714
+ Expected: no output (success). If anything is flagged, fix it and re-run until clean.
715
+
716
+ - [ ] **Step 7.2: Spec-to-implementation mapping check**
717
+
718
+ Manually walk through the spec sections and confirm each corresponds to something in the final workflow. Create a checklist in your head:
719
+
720
+ | Spec section | Where implemented |
721
+ |---|---|
722
+ | §3 Trigger (`v*.*.*` + `v*.*.*-*`) | `on.push.tags` in workflow |
723
+ | §4 Permissions + concurrency | workflow-level `permissions:` and `concurrency:` |
724
+ | §5.1 verify-signature (commit + tag object checks) | `verify-signature` job |
725
+ | §5.2 test matrix (Node 20/22/24, lint+typecheck+test+build) | `test` job with strategy.matrix |
726
+ | §5.3 publish-npm (dist-tag, version guard, provenance) | `publish-npm` job + `compute-disttag.sh` |
727
+ | §5.4 github-release (two steps, prerelease flag, generate-notes) | `github-release` job |
728
+ | §6 Secrets (NPM_TOKEN usage) | referenced in publish-npm step |
729
+
730
+ If any row has a gap, STOP and fix the workflow before the smoke test task.
731
+
732
+ - [ ] **Step 7.3: Dry-run the dist-tag script against a few tag names to build confidence**
733
+
734
+ Run:
735
+
736
+ ```bash
737
+ mkdir -p /tmp/disttag-manual
738
+ for tag in v1.0.0 v1.0.0-beta.1 v2.0.0-rc.3 v3.0.0-alpha.0; do
739
+ out=/tmp/disttag-manual/$tag.out
740
+ : > "$out"
741
+ GITHUB_REF_NAME="$tag" GITHUB_OUTPUT="$out" bash .github/scripts/compute-disttag.sh
742
+ echo "--- $tag ---"
743
+ cat "$out"
744
+ done
745
+ ```
746
+
747
+ Expected output groups like:
748
+ ```
749
+ --- v1.0.0 ---
750
+ version=1.0.0
751
+ disttag=latest
752
+ is_prerelease=false
753
+ --- v1.0.0-beta.1 ---
754
+ version=1.0.0-beta.1
755
+ disttag=beta
756
+ is_prerelease=true
757
+ ...
758
+ ```
759
+
760
+ - [ ] **Step 7.4: Final full test suite run**
761
+
762
+ Run: `npm test`
763
+
764
+ Expected: all tests pass, including `tests/scripts/compute-disttag.test.ts`.
765
+
766
+ - [ ] **Step 7.5: No commit needed for this task**
767
+
768
+ Task 7 is verification only. No files were modified. If anything needed fixing in Step 7.1, those fixes were already committed as targeted "fix(ci): ..." commits during the fix loop.
769
+
770
+ ---
771
+
772
+ ## Post-implementation: smoke test checklist (manual, operator-run)
773
+
774
+ **Not part of the automated plan — this is a runbook for the human operator once the code is merged.**
775
+
776
+ Before the first real release:
777
+
778
+ 1. Ensure `NPM_TOKEN` secret is set in the repo (Settings → Secrets → Actions → `NPM_TOKEN`, Granular Access Token with `Read and write` on `@intelmesh` packages).
779
+ 2. Ensure the npm account has the package linked to this repo (required for `--provenance`).
780
+ 3. On a throwaway branch, bump `package.json` to a prerelease like `0.1.0-alpha.0`.
781
+ 4. Sign-tag: `git tag -s v0.1.0-alpha.0 -m "pipeline smoke test"`.
782
+ 5. Push: `git push origin v0.1.0-alpha.0`.
783
+ 6. Watch the Actions run. Expected outcomes:
784
+ - `verify-signature` passes (if it fails, fix local GPG/SSH signing config before anything else).
785
+ - `test` passes on all three Node versions.
786
+ - `publish-npm` publishes to npm under dist-tag `alpha`.
787
+ - `github-release` creates a prerelease on GitHub with the `.tgz` attached.
788
+ 7. Cleanup (optional): `npm dist-tag rm @intelmesh/sdk alpha` and delete the GitHub release.
789
+
790
+ ---
791
+
792
+ ## Self-review notes (author's own)
793
+
794
+ - **Spec coverage:** every requirement in the spec maps to a specific task or file created. §9 (smoke test) is represented in the post-implementation runbook above since it's a manual step.
795
+ - **No placeholders:** every step has actual content — code blocks, commands, and expected output.
796
+ - **Type consistency:** the bash script writes `version`, `disttag`, `is_prerelease` to `$GITHUB_OUTPUT`; the vitest test parses those same three keys; the workflow step `id: disttag` consumes `steps.disttag.outputs.{version,disttag,is_prerelease}`; the `publish-npm` job exposes the same three names as job outputs; `github-release` reads `needs.publish-npm.outputs.is_prerelease`. Names are consistent end-to-end.
797
+ - **Commits are frequent:** Task 1 = 1 commit, Task 2 = 1 commit, Tasks 3-6 = 1 commit each, Task 7 = 0 commits. Total: 6 commits, each self-contained and reversible.
798
+ - **User feedback honored:** nothing under `docs/` is ever staged or committed. The plan document itself is not committed.