@intelmesh/sdk 0.1.0 → 0.1.1

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.
@@ -108,6 +108,7 @@ jobs:
108
108
  name: Publish to npm
109
109
  needs: test
110
110
  runs-on: ubuntu-24.04
111
+ environment: npm-publish
111
112
  permissions:
112
113
  contents: read
113
114
  id-token: write
@@ -122,7 +123,7 @@ jobs:
122
123
  - name: Setup Node
123
124
  uses: actions/setup-node@v5
124
125
  with:
125
- node-version: '22'
126
+ node-version: '24'
126
127
  registry-url: 'https://registry.npmjs.org'
127
128
  cache: 'npm'
128
129
 
@@ -170,7 +171,7 @@ jobs:
170
171
  - name: Setup Node
171
172
  uses: actions/setup-node@v5
172
173
  with:
173
- node-version: '22'
174
+ node-version: '24'
174
175
  cache: 'npm'
175
176
 
176
177
  - name: Install dependencies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intelmesh/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Official Node.js client for the IntelMesh Risk Intelligence Engine API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,798 +0,0 @@
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.
@@ -1,309 +0,0 @@
1
- # Release Pipeline Design — `@intelmesh/sdk`
2
-
3
- **Date:** 2026-04-10
4
- **Status:** Draft (pending user review)
5
- **Scope:** GitHub Actions workflow to publish `@intelmesh/sdk` to the public npm registry and create a GitHub Release on every signed semver tag.
6
-
7
- ---
8
-
9
- ## 1. Goals
10
-
11
- Create a single, auditable GitHub Actions workflow that:
12
-
13
- 1. Triggers on semver tag push (`v*.*.*` and `v*.*.*-*`).
14
- 2. Refuses to publish unless **both** the commit the tag points to **and** the tag object itself are cryptographically signed and verified by GitHub.
15
- 3. Runs the full quality gate (lint, typecheck, tests, build) on a Node version matrix (20, 22, 24) before any publish action.
16
- 4. Publishes to the public npm registry with Sigstore provenance.
17
- 5. Automatically derives the npm `dist-tag` from the tag suffix (stable → `latest`, prerelease → `beta`/`rc`/`alpha`/etc).
18
- 6. Creates a GitHub Release with the built tarball attached and auto-generated changelog, marked as `prerelease` when appropriate.
19
- 7. Fails hard at the earliest failing step — no partial state (no published version without a release, no release without a successful publish).
20
-
21
- ## 2. Non-goals
22
-
23
- - PR CI workflow (only release-on-tag is in scope).
24
- - `workflow_dispatch` with `dry_run` flag (deferred, YAGNI).
25
- - Integration tests hitting a live API (no such tests exist today).
26
- - Publishing to GitHub Packages or any registry other than public npm.
27
- - Automatic version bumping, changelog generation outside `gh release --generate-notes`, or release-please style automation.
28
- - Unpublish / rollback automation (stays manual).
29
-
30
- ## 3. Trigger
31
-
32
- ```yaml
33
- on:
34
- push:
35
- tags:
36
- - 'v[0-9]+.[0-9]+.[0-9]+'
37
- - 'v[0-9]+.[0-9]+.[0-9]+-*'
38
- ```
39
-
40
- Two explicit glob patterns. GitHub filters these *before* scheduling runners, so malformed tags like `v-test` or `release-1` never spend CI minutes. The first pattern matches stable releases (`v1.2.3`); the second matches prereleases (`v1.2.3-beta.1`, `v1.2.3-rc.2`, `v2.0.0-alpha.0`, etc).
41
-
42
- ## 4. Workflow-level permissions and concurrency
43
-
44
- ```yaml
45
- permissions:
46
- contents: write # github-release job creates releases
47
- id-token: write # publish-npm job generates OIDC token for --provenance
48
-
49
- concurrency:
50
- group: release-${{ github.ref }}
51
- cancel-in-progress: false
52
- ```
53
-
54
- **Permissions** are declared at the workflow level (not per job) because both are needed and `contents: read` alone is insufficient for the release step. No other permissions granted — principle of least privilege.
55
-
56
- **Concurrency** groups runs by tag ref with `cancel-in-progress: false`. Same tag cannot run twice in parallel (defense against accidental double-push), but distinct tags run independently. `cancel-in-progress` is explicitly `false` because a publish-in-flight must never be cancelled mid-flight (would leave npm and GitHub in inconsistent states).
57
-
58
- ## 5. Job graph
59
-
60
- Four jobs, chained strictly via `needs:`. Any failure aborts the entire chain.
61
-
62
- ```
63
- verify-signature → test (matrix 20/22/24) → publish-npm → github-release
64
- ```
65
-
66
- ### 5.1 Job: `verify-signature`
67
-
68
- **Runs-on:** `ubuntu-24.04`
69
- **Responsibility:** Refuse to proceed unless both the commit and the annotated tag object are verified.
70
-
71
- **Algorithm:**
72
-
73
- 1. **Commit check**
74
- - `GET /repos/{owner}/{repo}/commits/{github.sha}`
75
- - Read `.commit.verification.verified`
76
- - If not `true`: log `.commit.verification.reason` (examples: `unsigned`, `unknown_key`, `bad_email`, `unverified_email`, `not_signing_key`, `expired_key`) and `exit 1`.
77
-
78
- 2. **Resolve tag object**
79
- - `GET /repos/{owner}/{repo}/git/refs/tags/{github.ref_name}`
80
- - Read `.object.type`
81
- - If `"commit"`: the tag is a lightweight tag (no signature). Emit error telling the user to use `git tag -s` and `exit 1`.
82
- - If `"tag"`: read `.object.sha` (the SHA of the tag object itself).
83
-
84
- 3. **Tag signature check**
85
- - `GET /repos/{owner}/{repo}/git/tags/{tag_object_sha}`
86
- - Read `.verification.verified`
87
- - If not `true`: log `.verification.reason` and `exit 1`.
88
-
89
- **Implementation tool:** `gh api` (pre-installed on GitHub-hosted runners, authenticated via `GH_TOKEN=${{ github.token }}`). JSON parsing via `--jq`.
90
-
91
- **Error output:** uses `::error::` workflow command so failures surface as annotations on the tag's commit in the Actions UI.
92
-
93
- **Why both checks?**
94
- - Commit-only: misses a lightweight tag created on top of a verified commit — you'd publish without ever signing the release itself.
95
- - Tag-only: misses the case where the tag is signed but the underlying commit came from an unverified push.
96
- - Both: guarantees a complete cryptographic chain from commit → tag → release.
97
-
98
- ### 5.2 Job: `test`
99
-
100
- **Runs-on:** `ubuntu-24.04`
101
- **Needs:** `verify-signature`
102
- **Strategy:** Matrix on `node-version: [20, 22, 24]`, `fail-fast: true`.
103
-
104
- **Steps:**
105
-
106
- 1. `actions/checkout@v5`
107
- 2. `actions/setup-node@v5` with `node-version: ${{ matrix.node-version }}` and `cache: 'npm'`
108
- 3. `npm ci`
109
- 4. `npm run lint`
110
- 5. `npm run typecheck`
111
- 6. `npm run test` (vitest)
112
- 7. `npm run build`
113
-
114
- All three matrix cells must succeed for `needs: test` downstream to unblock.
115
-
116
- **Rationale for matrix:** `package.json` declares `engines.node: ">=20"`. The release must prove the SDK works on every LTS-or-current supported runtime before going to npm. Runtime is cheap (three parallel jobs) and it catches runtime-specific regressions that only manifest on newer Node versions (e.g., changes to `fetch`, `URL`, or `AbortController` semantics).
117
-
118
- ### 5.3 Job: `publish-npm`
119
-
120
- **Runs-on:** `ubuntu-24.04`
121
- **Needs:** `test`
122
- **Outputs:** `is_prerelease`, `disttag`, `version` (consumed by `github-release`).
123
-
124
- **Steps:**
125
-
126
- 1. `actions/checkout@v5`
127
- 2. `actions/setup-node@v5` with `node-version: 22`, `registry-url: 'https://registry.npmjs.org'`, `cache: 'npm'`
128
- 3. `npm ci`
129
- 4. `npm run build`
130
- 5. **Compute dist-tag and version (step id: `disttag`)**
131
-
132
- ```bash
133
- set -euo pipefail
134
- tag="${GITHUB_REF_NAME}" # e.g. "v1.2.3-beta.1"
135
- version="${tag#v}" # strip leading v → "1.2.3-beta.1"
136
- if [[ "$version" == *-* ]]; then
137
- suffix="${version#*-}" # "beta.1"
138
- disttag="${suffix%%.*}" # "beta"
139
- is_prerelease=true
140
- else
141
- disttag="latest"
142
- is_prerelease=false
143
- fi
144
- echo "version=$version" >> "$GITHUB_OUTPUT"
145
- echo "disttag=$disttag" >> "$GITHUB_OUTPUT"
146
- echo "is_prerelease=$is_prerelease" >> "$GITHUB_OUTPUT"
147
- ```
148
-
149
- 6. **Version mismatch guard**
150
-
151
- ```bash
152
- pkg_version=$(node -p "require('./package.json').version")
153
- if [ "$pkg_version" != "${{ steps.disttag.outputs.version }}" ]; then
154
- echo "::error::package.json version ($pkg_version) does not match git tag (${{ steps.disttag.outputs.version }})"
155
- exit 1
156
- fi
157
- ```
158
-
159
- 7. **Publish**
160
-
161
- ```bash
162
- npm publish --provenance --access public --tag "${{ steps.disttag.outputs.disttag }}"
163
- ```
164
-
165
- With `env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}`.
166
-
167
- **Tag-to-dist-tag mapping (reference):**
168
-
169
- | Tag pushed | `version` | `disttag` | `is_prerelease` |
170
- |-----------------------|-------------------|-----------|-----------------|
171
- | `v1.2.3` | `1.2.3` | `latest` | `false` |
172
- | `v1.2.3-beta.1` | `1.2.3-beta.1` | `beta` | `true` |
173
- | `v1.2.3-beta.4` | `1.2.3-beta.4` | `beta` | `true` |
174
- | `v1.2.3-rc.1` | `1.2.3-rc.1` | `rc` | `true` |
175
- | `v2.0.0-alpha.7` | `2.0.0-alpha.7` | `alpha` | `true` |
176
- | `v2.0.0-next.0` | `2.0.0-next.0` | `next` | `true` |
177
-
178
- **Why `--access public`?** `@intelmesh/sdk` is a scoped package; npm defaults scoped packages to private. The flag makes it explicit and prevents accidental private publishes.
179
-
180
- **Why `--provenance`?** Generates a Sigstore attestation linking the published tarball to the GitHub Actions run that built it. Consumers see a "provenance" badge on `npmjs.com/package/@intelmesh/sdk`. Requires `id-token: write` permission (already declared) and the npm account to have the package linked to this repo (via `package.json` `repository` field — must be verified present during implementation).
181
-
182
- **Why use the tag as the source of truth, not `package.json`?** If the developer tags `v1.2.3-beta.1` but forgets to bump `package.json` from `1.2.3`, we want a loud failure, not silent guessing. The version mismatch guard enforces this.
183
-
184
- ### 5.4 Job: `github-release`
185
-
186
- **Runs-on:** `ubuntu-24.04`
187
- **Needs:** `publish-npm`
188
-
189
- **Steps:**
190
-
191
- 1. `actions/checkout@v5` with `fetch-depth: 0` (needed for `gh release --generate-notes` to compute since-last-tag)
192
- 2. `actions/setup-node@v5` with `node-version: 22`, `cache: 'npm'`
193
- 3. `npm ci`
194
- 4. `npm run build`
195
- 5. `npm pack` (produces `intelmesh-sdk-<version>.tgz`)
196
- 6. **Create release** — split into two steps, gated by `if:` on the `is_prerelease` output, to keep each command readable:
197
-
198
- ```yaml
199
- - name: Create GitHub Release (stable)
200
- if: needs.publish-npm.outputs.is_prerelease == 'false'
201
- env:
202
- GH_TOKEN: ${{ github.token }}
203
- run: |
204
- gh release create "$GITHUB_REF_NAME" \
205
- ./intelmesh-sdk-*.tgz \
206
- --title "$GITHUB_REF_NAME" \
207
- --generate-notes
208
-
209
- - name: Create GitHub Release (prerelease)
210
- if: needs.publish-npm.outputs.is_prerelease == 'true'
211
- env:
212
- GH_TOKEN: ${{ github.token }}
213
- run: |
214
- gh release create "$GITHUB_REF_NAME" \
215
- ./intelmesh-sdk-*.tgz \
216
- --title "$GITHUB_REF_NAME" \
217
- --generate-notes \
218
- --prerelease
219
- ```
220
-
221
- Two mutually exclusive steps are clearer than one step with a dynamic flag inlined via GHA expression syntax.
222
-
223
- **Why rebuild here instead of reusing `publish-npm` artifacts?** GitHub Actions jobs have isolated file systems. The alternatives are (a) `actions/upload-artifact` + `actions/download-artifact` between jobs, or (b) rebuild. Rebuild is simpler, deterministic, and the build is fast (<30s for this SDK). If build time grows, switch to upload/download. YAGNI for now.
224
-
225
- **Why `--generate-notes`?** GitHub auto-generates release notes from commits and PRs since the previous tag. No need to maintain a manual CHANGELOG for the MVP — the git log and PR titles are the source of truth. Manual CHANGELOG can be added later if needed.
226
-
227
- **Why create release *after* npm publish (not before)?** If npm publish fails, we don't want a dangling GitHub release pointing at a version that isn't in the registry. Publish first, release second.
228
-
229
- ## 6. Secrets
230
-
231
- | Secret | Source | Used in | Required |
232
- |----------------|---------------------------------|---------------------------------|----------|
233
- | `NPM_TOKEN` | Repo Settings → Secrets → Actions | `publish-npm` | Yes |
234
- | `GITHUB_TOKEN` | Automatic (injected) | `verify-signature`, `github-release` | Auto |
235
-
236
- **`NPM_TOKEN` provisioning (one-time, manual):**
237
-
238
- 1. On `npmjs.com`, log in with the owner account for `@intelmesh`.
239
- 2. Settings → Access Tokens → Generate New Token → Granular Access Token.
240
- 3. Scope: `Read and write` on packages under `@intelmesh`.
241
- 4. Expiration: 1 year (document renewal in team runbook).
242
- 5. Copy the token.
243
- 6. In the GitHub repo: Settings → Secrets and variables → Actions → New repository secret, name `NPM_TOKEN`.
244
-
245
- **Provenance prerequisite check:** During implementation, verify `package.json` has a `repository` field pointing to `github.com/intelmesh/intelmesh-sdk-ts`. If missing, add it — `npm publish --provenance` uses it to link the package to the source repo.
246
-
247
- ## 7. Failure modes
248
-
249
- | Scenario | Failing job | User-visible message |
250
- |-----------------------------------------|-------------------|---------------------------------------------------------------------------------------|
251
- | Push of lightweight tag | `verify-signature` | `Tag vX.Y.Z is a lightweight tag. Use 'git tag -s' to create a signed annotated tag.` |
252
- | Commit not signed | `verify-signature` | `Commit <sha> is NOT verified. Reason: unsigned` |
253
- | Signing key not registered on GitHub | `verify-signature` | `Reason: unknown_key` |
254
- | Expired signing key | `verify-signature` | `Reason: expired_key` |
255
- | Signer email not verified on GitHub | `verify-signature` | `Reason: unverified_email` |
256
- | Lint / typecheck / test / build failure | `test` | ESLint / tsc / vitest annotations on the run |
257
- | `package.json` version ≠ tag | `publish-npm` | `package.json version (X) does not match git tag (Y)` |
258
- | Version already published | `publish-npm` | npm 403; step fails; no release created |
259
- | `NPM_TOKEN` missing/expired | `publish-npm` | npm 401 |
260
- | Provenance OIDC fails | `publish-npm` | npm publish error mentioning `id-token` |
261
-
262
- All failures propagate via `needs:` so no downstream job runs. There is no partial state: a failed pipeline leaves the npm registry and GitHub Releases untouched by downstream jobs.
263
-
264
- ## 8. Re-running a failed release
265
-
266
- **Supported:** GitHub Actions "Re-run failed jobs" works because the trigger is `push: tags` and the tag continues to point to the same commit. Use this for flaky failures (npm registry 502, runner network blip, etc).
267
-
268
- **Not supported by the workflow (manual process):**
269
-
270
- - Unpublishing a broken release from npm: `npm unpublish @intelmesh/sdk@X.Y.Z` within the 72-hour window, or contact npm support after.
271
- - Removing a release from GitHub: `gh release delete vX.Y.Z`.
272
- - Deleting/moving a tag: `git tag -d vX.Y.Z && git push --delete origin vX.Y.Z && git tag -s vX.Y.Z <new-sha> && git push origin vX.Y.Z`.
273
-
274
- These stay manual because they are operational rollbacks, not part of a normal release flow, and automating them would be footgun territory.
275
-
276
- ## 9. Testing the workflow itself
277
-
278
- Before the first real release, validate the pipeline end-to-end on a throwaway version:
279
-
280
- 1. Create a branch with a signed commit.
281
- 2. Bump `package.json` to a harmless prerelease version like `0.1.0-alpha.0`.
282
- 3. Sign-tag: `git tag -s v0.1.0-alpha.0 -m "pipeline smoke test"`.
283
- 4. Push tag: `git push origin v0.1.0-alpha.0`.
284
- 5. Watch the Actions run:
285
- - `verify-signature` must pass (if not: fix GPG/SSH setup first).
286
- - `test` must pass on all three Node versions.
287
- - `publish-npm` will actually publish to npm under dist-tag `alpha`. This is fine — `alpha` dist-tag doesn't affect default installs.
288
- - `github-release` creates a prerelease on GitHub.
289
- 6. After success: `npm dist-tag rm @intelmesh/sdk alpha` (cleanup, optional) and delete the GitHub release if desired.
290
-
291
- Alternatively, if contaminating the npm registry with a test version is undesirable, a future `workflow_dispatch` with `dry_run` input can be added (explicitly deferred — see Non-goals).
292
-
293
- ## 10. File layout
294
-
295
- ```
296
- .github/
297
- workflows/
298
- release.yml ← this design's output
299
- ```
300
-
301
- Single file. All four jobs live in one workflow so the full release pipeline is visible in one place.
302
-
303
- ## 11. Open implementation questions
304
-
305
- None at spec time. Items to verify during implementation:
306
-
307
- - `package.json` `repository` field is present and points to the correct GitHub URL (required for `--provenance`).
308
- - The npm account owning `@intelmesh` exists and `NPM_TOKEN` has been provisioned before merging the workflow (otherwise the first tag push will fail at `publish-npm`).
309
- - GitHub org `intelmesh` has `Actions` enabled with `write` permissions for workflow tokens (Settings → Actions → Workflow permissions).