-
Notifications
You must be signed in to change notification settings - Fork 137
325 lines (281 loc) · 13.9 KB
/
Copy pathrelease.yml
File metadata and controls
325 lines (281 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
name: Release
on:
push:
tags: ["v*"]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
# `contents: write` is required for `gh release create` to publish
# the GitHub Release at the end of the workflow. `id-token: write`
# stays for npm provenance.
contents: write
id-token: write
steps:
- name: Checkout source code
uses: actions/checkout@v7
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: 22.x
cache: "npm"
registry-url: "https://registry.npmjs.org"
- name: Verify release tag matches package version
run: |
VERSION=$(node -p "require('./package.json').version")
EXPECTED_TAG="v${VERSION}"
if [ "$GITHUB_REF_NAME" != "$EXPECTED_TAG" ]; then
echo "Release tag mismatch: expected $EXPECTED_TAG for package.json version $VERSION, got $GITHUB_REF_NAME"
exit 1
fi
echo "Verified release tag $EXPECTED_TAG"
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Check AI instruction mirror freshness
run: npm run check:ai-instructions
- name: Build library
run: npm run dist:prod
- name: Check public API surface
run: npm run check:api-surface
- name: Build MCP server
run: npm run build:mcp
- name: Run tests with coverage
run: npm run test:coverage
- name: Run type check
run: npm run typescript
- name: Run test type check
run: npm run typescript:tests
- name: Check chart spec registry round-trip
run: npm run check:chart-specs
- name: Check capability matrix freshness
run: npm run check:capabilities
- name: Check generated AI surface manifest freshness
run: npm run check:ai-surface
- name: Check blog metadata registry freshness
run: npm run check:blog-entries
- name: Build docs site and smoke-check generated routes
run: npm run check:website-build
- name: Verify TypeScript declarations
run: |
for f in dist/semiotic.d.ts dist/semiotic-xy.d.ts dist/semiotic-ordinal.d.ts dist/semiotic-network.d.ts dist/semiotic-geo.d.ts dist/semiotic-realtime.d.ts dist/semiotic-ai.d.ts dist/semiotic-data.d.ts dist/semiotic-server.d.ts dist/semiotic-themes.d.ts; do
if [ ! -f "$f" ]; then
echo "MISSING: $f — aborting release"
exit 1
fi
done
echo "All declaration files present"
- name: Determine npm dist-tag
id: dist-tag
run: |
VERSION=$(node -p "require('./package.json').version")
if echo "$VERSION" | grep -qE '[-](alpha|beta|rc)'; then
echo "tag=beta" >> "$GITHUB_OUTPUT"
else
echo "tag=latest" >> "$GITHUB_OUTPUT"
fi
# Construct one archive after every build/gate. All subsequent smoke,
# checksum/SBOM evidence, dry-run, and publication steps use this exact
# file; publishing the checkout would silently create a second archive.
- name: Create immutable release artifact
id: release-artifact
run: |
OUT_DIR="$RUNNER_TEMP/semiotic-release-artifact"
npm run release:artifact -- --out-dir "$OUT_DIR"
node scripts/create-release-artifact.mjs --out-dir "$OUT_DIR" --verify
MANIFEST=$(find "$OUT_DIR" -maxdepth 1 -name '*.provenance.json' -print -quit)
TARBALL=$(node -e "const fs=require('fs'); const path=require('path'); const manifest=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); process.stdout.write(path.join(path.dirname(process.argv[1]), manifest.tarball.file))" "$MANIFEST")
INTEGRITY=$(node -e "const fs=require('fs'); const manifest=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); process.stdout.write(manifest.tarball.integrity)" "$MANIFEST")
echo "directory=$OUT_DIR" >> "$GITHUB_OUTPUT"
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
echo "integrity=$INTEGRITY" >> "$GITHUB_OUTPUT"
- name: Upload immutable release evidence
uses: actions/upload-artifact@v4
with:
name: semiotic-${{ github.ref_name }}-release-artifact
path: ${{ steps.release-artifact.outputs.directory }}
if-no-files-found: error
- name: Pack-and-import smoke test (exact tarball)
run: npm run check:pack -- --tarball "${{ steps.release-artifact.outputs.tarball }}"
- name: Check packed cold-consumer baseline (exact tarball)
run: npm run check:cold-consumer -- --tarball "${{ steps.release-artifact.outputs.tarball }}"
- name: Reverify immutable release artifact before publication
run: node scripts/create-release-artifact.mjs --out-dir "${{ steps.release-artifact.outputs.directory }}" --verify
- name: "Release diagnostics: coverage-risk"
run: npm run check:coverage-risk
continue-on-error: true
- name: "Release diagnostics: visual baseline capabilities"
run: npm run check:visual-baseline-capabilities
continue-on-error: true
- name: "Release diagnostics: deployment lockfile"
run: npm run check:cloud-run-lock
continue-on-error: true
- name: "Release diagnostics: benchmark comparison"
run: npm run bench:compare
continue-on-error: true
- name: Check whether this exact version is already published
id: published-artifact
run: |
VERSION=$(node -p "require('./package.json').version")
PUBLISHED_INTEGRITY=$(npm view "semiotic@${VERSION}" dist.integrity 2>/dev/null || true)
if [ -z "$PUBLISHED_INTEGRITY" ]; then
echo "already-published=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$PUBLISHED_INTEGRITY" != "${{ steps.release-artifact.outputs.integrity }}" ]; then
echo "semiotic@${VERSION} already exists with different integrity"
echo "Expected: ${{ steps.release-artifact.outputs.integrity }}"
echo "Published: $PUBLISHED_INTEGRITY"
exit 1
fi
echo "semiotic@${VERSION} already has the expected integrity; treating this as an idempotent rerun"
echo "already-published=true" >> "$GITHUB_OUTPUT"
- name: Dry-run publish exact artifact
if: steps.published-artifact.outputs.already-published != 'true'
run: npm publish "${{ steps.release-artifact.outputs.tarball }}" --dry-run --access public --tag ${{ steps.dist-tag.outputs.tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish exact artifact to npm
if: steps.published-artifact.outputs.already-published != 'true'
run: npm publish "${{ steps.release-artifact.outputs.tarball }}" --provenance --access public --tag ${{ steps.dist-tag.outputs.tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Post-publish smoke test
run: |
VERSION=$(node -p "require('./package.json').version")
TAG=${{ steps.dist-tag.outputs.tag }}
echo "Waiting for npm to propagate semiotic@${VERSION}..."
for i in 1 2 3 4 5; do
PUBLISHED=$(npm view semiotic@${VERSION} version 2>/dev/null || echo "")
if [ "$PUBLISHED" = "$VERSION" ]; then
echo "Package available on npm"
break
fi
echo "Attempt $i: not yet available, waiting 15s..."
sleep 15
done
if [ "$PUBLISHED" != "$VERSION" ]; then
echo "Package was not visible on npm registry after 75s"
exit 1
fi
EXPECTED_INTEGRITY="${{ steps.release-artifact.outputs.integrity }}"
PUBLISHED_INTEGRITY=$(npm view "semiotic@${VERSION}" dist.integrity)
if [ "$PUBLISHED_INTEGRITY" != "$EXPECTED_INTEGRITY" ]; then
echo "Published package integrity does not match the immutable release artifact"
echo "Expected: $EXPECTED_INTEGRITY"
echo "Published: $PUBLISHED_INTEGRITY"
exit 1
fi
echo "Published npm integrity matches the immutable release artifact"
# Install in a temp project and verify imports
TMPDIR=$(mktemp -d)
cd "$TMPDIR"
npm init -y > /dev/null 2>&1
npm install semiotic@${VERSION} react react-dom > /dev/null 2>&1
node -e "
const assert = require('assert');
// Main entry
const s = require('semiotic');
assert(s.BarChart, 'BarChart not exported from semiotic');
assert(s.LineChart, 'LineChart not exported from semiotic');
assert(s.ForceDirectedGraph, 'ForceDirectedGraph not exported from semiotic');
// Sub-path entries
const xy = require('semiotic/xy');
assert(xy.LineChart, 'LineChart not exported from semiotic/xy');
const ord = require('semiotic/ordinal');
assert(ord.BarChart, 'BarChart not exported from semiotic/ordinal');
const net = require('semiotic/network');
assert(net.SankeyDiagram, 'SankeyDiagram not exported from semiotic/network');
const rt = require('semiotic/realtime');
assert(rt.RealtimeLineChart, 'RealtimeLineChart not exported from semiotic/realtime');
const srv = require('semiotic/server');
assert(srv.renderChart, 'renderChart not exported from semiotic/server');
const themes = require('semiotic/themes');
assert(themes.resolveThemePreset, 'resolveThemePreset not exported from semiotic/themes');
const utils = require('semiotic/utils');
assert(utils.validateProps, 'validateProps not exported from semiotic/utils');
console.log('All smoke tests passed — 8 entry points verified');
"
rm -rf "$TMPDIR"
- name: Verify published package registry provenance metadata
env:
EXPECTED_INTEGRITY: ${{ steps.release-artifact.outputs.integrity }}
run: |
export VERSION=$(node -p "require('./package.json').version")
node - <<'NODE'
const { execSync } = require("node:child_process")
const packageName = "semiotic"
const version = process.env.VERSION
const expectedIntegrity = process.env.EXPECTED_INTEGRITY
const payload = execSync(`npm view ${packageName}@${version} --json`, {
encoding: "utf8",
})
const manifest = JSON.parse(payload)
const dist = manifest.dist || {}
if (dist.integrity !== expectedIntegrity) {
throw new Error(`Published dist.integrity (${dist.integrity}) does not match expected integrity (${expectedIntegrity})`)
}
const asArray = (value) => (Array.isArray(value) ? value : value ? [value] : [])
const signatures = asArray(dist.signatures)
const attestations = asArray(dist.attestations)
if (signatures.length === 0) {
throw new Error("Published package is missing dist.signatures")
}
if (attestations.length === 0) {
throw new Error("Published package is missing dist.attestations")
}
console.log(`Verified registry provenance for ${packageName}@${version}: ${signatures.length} signature(s), ${attestations.length} attestation(s)`)
NODE
- name: Create or update GitHub Release
# Pulls the matching CHANGELOG section so the GH release page
# mirrors the version's entry. Falls back to `--generate-notes`
# (auto-built from PR titles) if the section is missing or empty
# so a release page is still created either way. Without this
# step a successful npm publish leaves the GitHub Releases page
# stuck on the previous version (the v3.2.3 → v3.4.2 backfill
# in 2026-04-28 fixed exactly that gap).
#
# Idempotent on workflow re-runs: if the release for this tag
# already exists, `gh release edit` updates the title/notes in
# place rather than failing on the conflict. Without that, a
# re-run for any reason (transient infra failure, manually
# re-triggered after a fix) would error out at this step.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
NOTES_FILE="$(mktemp)"
awk -v ver="$VERSION" '
BEGIN { flag = 0 }
$0 ~ "^## \\[" ver "\\]" { flag = 1; next }
flag && /^## \[/ { exit }
flag { print }
' CHANGELOG.md > "$NOTES_FILE"
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
echo "Release $GITHUB_REF_NAME already exists; updating in place"
if [ -s "$NOTES_FILE" ]; then
gh release edit "$GITHUB_REF_NAME" \
--title "Semiotic $GITHUB_REF_NAME" \
--notes-file "$NOTES_FILE"
else
echo "No CHANGELOG section for $VERSION; leaving existing notes unchanged"
gh release edit "$GITHUB_REF_NAME" \
--title "Semiotic $GITHUB_REF_NAME"
fi
else
if [ -s "$NOTES_FILE" ]; then
gh release create "$GITHUB_REF_NAME" \
--title "Semiotic $GITHUB_REF_NAME" \
--notes-file "$NOTES_FILE" \
--verify-tag
else
echo "No CHANGELOG section for $VERSION; falling back to --generate-notes"
gh release create "$GITHUB_REF_NAME" \
--title "Semiotic $GITHUB_REF_NAME" \
--generate-notes \
--verify-tag
fi
fi
gh release upload "$GITHUB_REF_NAME" \
"${{ steps.release-artifact.outputs.directory }}"/* \
--clobber
rm -f "$NOTES_FILE"