@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.
- package/.github/scripts/compute-disttag.sh +47 -0
- package/.github/workflows/release.yml +206 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +2 -0
- package/.prettierrc +8 -0
- package/CLAUDE.md +37 -0
- package/LICENSE +21 -0
- package/commitlint.config.cjs +3 -0
- package/dist/index.d.ts +1293 -0
- package/dist/index.js +1651 -0
- package/docs/superpowers/plans/2026-04-10-release-pipeline.md +798 -0
- package/docs/superpowers/specs/2026-04-10-release-pipeline-design.md +309 -0
- package/eslint.config.mjs +38 -0
- package/package.json +72 -0
- package/src/builders/event.ts +72 -0
- package/src/builders/rule.ts +143 -0
- package/src/client/errors.ts +171 -0
- package/src/client/http.ts +209 -0
- package/src/client/intelmesh.ts +57 -0
- package/src/client/pagination.ts +50 -0
- package/src/generated/types.ts +11 -0
- package/src/index.ts +106 -0
- package/src/provision/index.ts +6 -0
- package/src/provision/provisioner.ts +326 -0
- package/src/provision/rule-builder.ts +193 -0
- package/src/resources/apikeys.ts +63 -0
- package/src/resources/audit.ts +29 -0
- package/src/resources/evaluations.ts +38 -0
- package/src/resources/events.ts +61 -0
- package/src/resources/lists.ts +91 -0
- package/src/resources/phases.ts +71 -0
- package/src/resources/rules.ts +98 -0
- package/src/resources/scopes.ts +71 -0
- package/src/resources/scores.ts +63 -0
- package/src/testkit/assertion.ts +76 -0
- package/src/testkit/harness.ts +252 -0
- package/src/testkit/index.ts +7 -0
- package/src/types.ts +330 -0
- package/tests/client/errors.test.ts +159 -0
- package/tests/provision/provisioner.test.ts +311 -0
- package/tests/scripts/compute-disttag.test.ts +178 -0
- package/tests/testkit/harness.test.ts +291 -0
- package/tsconfig.eslint.json +8 -0
- package/tsconfig.json +29 -0
- 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.
|