Repository navigation
613 lines (602 loc) · 33.7 KB
/
Copy pathpublish.yml
File metadata and controls
613 lines (602 loc) · 33.7 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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
# Published-package provenance (GX-10 / sq-toze.24).
#
# Complements release.yml (#367 / sq-toze.23), which attests the GitHub-Release ARCHIVES +
# the ghcr.io container. This workflow covers the bytes a CONSUMER actually installs:
# - the `@sparq-org/sparq` npm client (crates/sparq-wasm wrapper in js/)
# - the packaged crates.io `.crate` artifacts
# - the `sparq-rdf` PyPI wheels (crates/sparq-py, maturin-built) — PEP-740 attestations
#
# HONEST SCOPE BOUNDARY (see compliance/slsa/):
# * npm — native provenance via OIDC TRUSTED PUBLISHING. sq-v286.11: the npm
# job authenticates with the GitHub Actions OIDC token (id-token: write) — NO
# long-lived NPM_TOKEN — exchanging it for a short-lived publish credential, and
# npm's registry records a Sigstore-signed in-toto SLSA provenance statement bound
# to this workflow run. A consumer verifies with `npm audit signatures`. THIS IS A
# PROVENANCE-EMITTING PATH. ONE-TIME needs:user: register the npm Trusted Publisher
# for @sparq-org/sparq (owner `sparq-org`, repo `sparq`, workflow file `publish.yml`); the
# package must already exist, so one bootstrap publish precedes it (see npm job).
# * crates.io — crates.io has NO published-package provenance mechanism upstream as of this
# writing (no equivalent of npm `--provenance`). The tractable equivalent we CAN
# do is attest the packaged `.crate` artifact (the exact bytes `cargo publish`
# uploads) with the same `actions/attest-build-provenance` mechanism release.yml
# uses, and publish that attestation as a release-adjacent artifact + via Sigstore.
# This does NOT put a provenance link on the crates.io page (that needs upstream
# support) — it gives consumers an out-of-band attestation they can `gh attestation
# verify` against the downloaded `.crate`. The crates.io-native sub-gap stays OPEN
# and is tracked as an EXTERNAL sub-gap of GX-10 / sq-toze.24.
# * PyPI — sq-toze.37. native PEP-740 attestations. `pypa/gh-action-pypi-publish`
# runs in this GitHub Actions OIDC context (id-token: write) and — when uploading
# via PyPI Trusted Publishing — generates + uploads a PEP-740 digital attestation
# (an in-toto/Sigstore-signed statement bound to this workflow run) ALONGSIDE each
# wheel/sdist. PyPI records it and shows a "Provenance" panel on the release-files
# page; a consumer can verify with `pypi-attestations verify` (or `gh attestation
# verify`). THIS IS A PROVENANCE-EMITTING PATH — the strongest of the three.
# BUT there is ONE maintainer prerequisite that CANNOT be wired from this repo:
# the PyPI project `sparq-rdf` must have a Trusted Publisher registered (PyPI →
# project → Publishing → "Add a new pending/published publisher": owner `sparq-org`,
# repo `sparq`, workflow `publish.yml`, environment `pypi`). Until a maintainer
# does that one-time PyPI-side config, the `twine`/Trusted-Publishing upload step
# here will (correctly) fail to mint a token — the CI is fully wired, the PyPI
# account config is the only open item. See compliance/slsa/ + docs/release.md.
#
# TRIGGER: this is a MAINTAINER-DRIVEN publish. `release: published` covers Releases published
# by a human or external token; GitHub does not emit a new workflow-triggering event when
# `release.yml` creates its Release with `GITHUB_TOKEN`. The normal release-plz path
# therefore uses `workflow_dispatch` after the GitHub Release is green. Dispatch uploads are
# separately gated by explicit boolean inputs; a tag alone never force-publishes. A Release
# published directly by a maintainer remains an explicit authorization for the primary npm and
# PyPI jobs, while the optional npm packages still require their dispatch inputs.
# Dispatch at the immutable release tag, never a branch. setup verifies the
# tag/checkout identity and the versions of every selected registry package first.
name: publish
on:
release:
types: [published]
workflow_dispatch:
inputs:
publish_npm:
description: "Publish @sparq-org/sparq to npm with provenance"
type: boolean
default: false
publish_eyereasoner_compat:
description: "Publish @sparq-org/eyereasoner-compat to npm (needs the one-time npm-side bootstrap — see job)"
type: boolean
default: false
publish_solid_server:
description: "Publish @sparq-org/solid-server to npm (needs the one-time npm-side bootstrap — see job)"
type: boolean
default: false
attest_crates:
description: "Package + attest publishable crates (crates.io bytes)"
type: boolean
default: true
publish_pypi:
description: "Build sparq-rdf wheels + publish to PyPI with PEP-740 attestations (Trusted Publishing)"
type: boolean
default: false
# Default read-only; jobs opt into the OIDC token (id-token: write) + attestation write below.
permissions:
contents: read
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false
jobs:
setup:
name: verify immutable package source
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Verify immutable release source
env:
RELEASE_TAG: ${{ github.event.release.tag_name || github.ref_name }}
PUBLISH_NPM: ${{ github.event_name == 'release' || inputs.publish_npm }}
PUBLISH_SOLID_SERVER: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_solid_server }}
PUBLISH_EYEREASONER_COMPAT: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_eyereasoner_compat }}
run: python3 scripts/check-release-source.py --tag "$RELEASE_TAG" --publish
# ---- npm: @sparq-org/sparq via OIDC TRUSTED PUBLISHING (no NPM_TOKEN) ----
# sq-v286.11 (maintainer #758): switched from a long-lived NPM_TOKEN to npm's
# OIDC Trusted Publishing (GA 2025-07-31, github.blog/changelog/2025-07-31). The npm CLI
# detects the GitHub Actions OIDC environment (granted by `id-token: write` below), exchanges
# the OIDC token with the registry for a short-lived publish credential, and records the
# Sigstore-signed SLSA provenance statement automatically — NO static token is read or stored.
# Runs only when a maintainer asks for it (manual input) OR on a published release.
#
# ONE-TIME needs:user step (cannot be a repo file — it is registry-side config): on npmjs.com,
# @sparq-org/sparq must have a Trusted Publisher registered (npmjs.com/package/@sparq-org/sparq/access
# -> "Trusted Publisher"): organization/user `sparq-org`, repository `sparq`, workflow FILENAME
# `publish.yml` (filename only, with extension — NOT a path), environment left blank (this job
# uses none). npm trusted publishing requires the package to ALREADY EXIST, so the maintainer
# does ONE bootstrap `npm publish` with a granular token, then registers the trusted publisher
# and removes the token — every CI publish thereafter is tokenless. See docs/release.md.
npm:
name: npm publish (OIDC trusted publishing)
needs: setup
runs-on: ubuntu-latest
if: >-
github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && inputs.publish_npm)
permissions:
contents: read
id-token: write # OIDC token npm exchanges for a short-lived publish credential + provenance signing
defaults:
run:
working-directory: js
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: wasm32-unknown-unknown
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
# registry-url is still REQUIRED: it writes the .npmrc `//registry.npmjs.org/:...`
# line the OIDC flow keys off (and selects the public registry the token is minted
# for). Under trusted publishing no NODE_AUTH_TOKEN is read — the npm CLI mints a
# short-lived credential from the OIDC token at publish time.
registry-url: "https://registry.npmjs.org"
# sq-v286.11: OIDC trusted publishing requires npm CLI >= 11.5.1. Node 22's
# BUNDLED npm can be older (verified locally: Node 20/22 images have shipped npm 10.x),
# so pin an npm that supports trusted publishing rather than relying on the bundled one.
# `npm@^11.5.1` is the documented floor (docs.npmjs.com/trusted-publishers).
- name: Pin npm CLI >= 11.5.1 (trusted-publishing floor)
run: |
npm install -g 'npm@^11.5.1'
npm --version
# Prebuilt, SHA-pinned, exactly-versioned wasm-pack (sq-khm3f, #5776): a
# `cargo install` recompiles it from crates.io and flakes on a registry blip.
- name: Install wasm-pack
uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
with:
version: v0.15.0
- name: Install npm dependencies
run: npm ci
# Publish guardrail (sq-bkag): refuse to publish a binding that would ship without its
# wasm engine, that drops dist/wasm from the `files` allowlist, or that lost the `prepare`
# git-pin build hook. Runs BEFORE publish so a broken package fails the job, not the consumer.
- name: Package guardrail check
run: npm run check:package
# `prepack` (build:wasm + build:ts + bundle skills) runs automatically on `npm publish`,
# producing the same tree `npm pack --dry-run` validates in js.yml.
- name: Reverify remote release tag immediately before publish
env:
RELEASE_TAG: ${{ github.event.release.tag_name || github.ref_name }}
run: >-
python3 "$GITHUB_WORKSPACE/scripts/check-release-source.py"
--repo-root "$GITHUB_WORKSPACE" --tag "$RELEASE_TAG" --remote-tag origin
- name: Publish to npm (OIDC trusted publishing + provenance)
# NO `env: NODE_AUTH_TOKEN` — trusted publishing authenticates via the GitHub Actions
# OIDC token (id-token: write). The npm CLI detects the OIDC environment and exchanges
# it for a short-lived publish credential; no long-lived NPM_TOKEN is stored anywhere.
# --provenance: provenance is emitted by default under trusted publishing, but the flag
# is kept as an explicit belt-and-suspenders (a practitioner-reported edge case where
# it was not auto-attached); it is a no-op when already on.
# --access public: @sparq-org/sparq is a scoped package; scoped packages default to restricted.
run: npm publish --provenance --access public
# Verify the just-published version actually carries a registry provenance attestation, so a
# misconfigured publish (missing OIDC / old npm) fails the job instead of silently shipping an
# unattested package. `npm view <pkg>@<ver> dist.attestations` returns the registry-recorded
# attestation bundle URL + provenance predicate type when provenance was attached.
- name: Verify published provenance attestation
run: |
set -euo pipefail
ver=$(node -p "require('./package.json').version")
echo "Inspecting registry attestations for @sparq-org/sparq@${ver}"
# The registry can take a few minutes to serve a just-published version (v0.1.5:
# about two), so poll before calling the publish unattested.
att=""
for _ in $(seq 1 20); do
att=$(npm view "@sparq-org/sparq@${ver}" dist.attestations 2>/dev/null || true)
if [ -n "$att" ] && [ "$att" != "undefined" ]; then break; fi
sleep 30
done
echo "$att"
if [ -z "$att" ] || [ "$att" = "undefined" ]; then
echo "::error::no provenance attestation recorded for @sparq-org/sparq@${ver} — publish was not provenance-bearing"
exit 1
fi
echo "Provenance attestation present. Consumers verify with: npm audit signatures"
# ---- npm: @sparq-org/eyereasoner-compat via OIDC TRUSTED PUBLISHING (no NPM_TOKEN) ----
# sq-ohnj1 (GH #1499). The eye-js migration package (packages/eyereasoner-compat) —
# the eye-js `n3reasoner` API backed by the sparq-reason wasm bundle, which ships INSIDE the
# tarball (never the site-hosted wasm URL). Same tokenless OIDC trusted-publishing pattern as
# the @sparq-org/sparq job above: `id-token: write` lets the npm CLI mint a short-lived publish
# credential + record a Sigstore-signed SLSA provenance statement; no NPM_TOKEN is stored.
#
# GRACEFUL-SKIP: this job runs ONLY on an explicit manual `workflow_dispatch` with
# `publish_eyereasoner_compat=true` — NOT on `release: published`. So a release never fails
# because the npm-side bootstrap below is not yet done; the maintainer triggers it once ready.
#
# ONE-TIME needs:user step (registry-side config — cannot be a repo file). npm trusted
# publishing can only be configured on a package that ALREADY EXISTS, and this is a NEW package
# under a NEW scope, so the maintainer does exactly this ONCE, then every CI publish is tokenless:
# 1. Create the npm organisation `@sparq-org` (npmjs.com → "Add Organization") if it does not
# exist, and ensure the publishing identity is a member with publish rights.
# 2. Bootstrap-publish the first version with a granular token (or `npm login`):
# cd packages/eyereasoner-compat && npm run build && npm publish --access public
# 3. Register the Trusted Publisher on
# npmjs.com/package/@sparq-org/eyereasoner-compat/access → "Trusted Publisher":
# GitHub organisation/user `sparq-org`, repository `sparq`, workflow FILENAME
# `publish.yml` (filename only, with extension — NOT a path), environment left blank.
# Then remove the granular token. Every publish this job triggers thereafter is tokenless.
# Until step 1–3 are done, DO NOT trigger this job (the publish would fail to mint a credential
# by design — no static token is stored). See docs/release.md.
npm-eyereasoner-compat:
name: npm publish @sparq-org/eyereasoner-compat (OIDC trusted publishing)
needs: setup
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' && inputs.publish_eyereasoner_compat
permissions:
contents: read
id-token: write # OIDC token npm exchanges for a short-lived publish credential + provenance signing
defaults:
run:
working-directory: packages/eyereasoner-compat
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: wasm32-unknown-unknown
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
registry-url: "https://registry.npmjs.org"
# OIDC trusted publishing requires npm CLI >= 11.5.1 (Node 22 bundles an older npm).
- name: Pin npm CLI >= 11.5.1 (trusted-publishing floor)
run: |
npm install -g 'npm@^11.5.1'
npm --version
# Prebuilt, SHA-pinned, exactly-versioned wasm-pack (sq-khm3f, #5776): a
# `cargo install` recompiles it from crates.io and flakes on a registry blip.
- name: Install wasm-pack
uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
with:
version: v0.15.0
# Build tooling only (typescript, @rdfjs/types). --workspaces=false installs just this
# package's devDeps without pulling the whole monorepo graph; the package has no lockfile.
- name: Install build dependencies
run: npm install --workspaces=false --no-package-lock --no-audit --no-fund
# Build (wasm-pack + tsc) then verify the tarball actually ships dist/ + the wasm engine.
- name: Build + package guardrail check
run: |
npm run build
npm run check:package
# `prepack` rebuilds the same tree; provenance is emitted by default under trusted
# publishing (--provenance is belt-and-suspenders). --access public: scoped package.
- name: Reverify remote release tag immediately before publish
env:
RELEASE_TAG: ${{ github.event.release.tag_name || github.ref_name }}
run: >-
python3 "$GITHUB_WORKSPACE/scripts/check-release-source.py"
--repo-root "$GITHUB_WORKSPACE" --tag "$RELEASE_TAG" --remote-tag origin
- name: Publish to npm (OIDC trusted publishing + provenance)
run: npm publish --provenance --access public
- name: Verify published provenance attestation
run: |
set -euo pipefail
ver=$(node -p "require('./package.json').version")
# The registry can take a few minutes to serve a just-published version (v0.1.5:
# about two), so poll before calling the publish unattested.
att=""
for _ in $(seq 1 20); do
att=$(npm view "@sparq-org/eyereasoner-compat@${ver}" dist.attestations 2>/dev/null || true)
if [ -n "$att" ] && [ "$att" != "undefined" ]; then break; fi
sleep 30
done
echo "$att"
if [ -z "$att" ] || [ "$att" = "undefined" ]; then
echo "::error::no provenance attestation recorded for @sparq-org/eyereasoner-compat@${ver}"
exit 1
fi
echo "Provenance attestation present. Consumers verify with: npm audit signatures"
# ---- npm: @sparq-org/solid-server via OIDC TRUSTED PUBLISHING (no NPM_TOKEN) ----
# sq-6xasp.11 (epic sq-6xasp). The WebAssembly Solid server package
# (packages/solid-server) — a local-dev Solid/LDP pod backed by the sparq-lws wasm handler,
# which ships INSIDE the tarball (the `files` allowlist packs bin/ + src/ + the built wasm/ +
# README). Same tokenless OIDC trusted-publishing pattern as the @sparq-org/sparq job above:
# `id-token: write` lets the npm CLI mint a short-lived publish credential + record a
# Sigstore-signed SLSA provenance statement; no NPM_TOKEN is stored.
#
# GRACEFUL-SKIP: this job runs ONLY on an explicit manual `workflow_dispatch` with
# `publish_solid_server=true` — NOT on `release: published`. So a release never fails because
# the npm-side bootstrap below is not yet done; the maintainer triggers it once ready.
#
# ONE-TIME needs:user step (registry-side config — cannot be a repo file). npm trusted
# publishing can only be configured on a package that ALREADY EXISTS, and this is a NEW package,
# so the maintainer does exactly this ONCE, then every CI publish is tokenless:
# 1. Bootstrap-publish the first version with a granular token (or `npm login`) — build the
# wasm first so the tarball ships wasm/ (the `files` allowlist expects it):
# cd packages/solid-server && npm run build:lws-wasm && npm publish --access public
# 2. Register the Trusted Publisher on
# npmjs.com/package/@sparq-org/solid-server/access → "Trusted Publisher":
# GitHub organisation/user `sparq-org`, repository `sparq`, workflow FILENAME `publish.yml`
# (filename only, with extension — NOT a path), environment left blank.
# Then remove the granular token. Every publish this job triggers thereafter is tokenless.
# Until step 1–2 are done, DO NOT trigger this job (the publish would fail to mint a credential
# by design — no static token is stored). See docs/release.md.
npm-solid-server:
name: npm publish @sparq-org/solid-server (OIDC trusted publishing)
needs: setup
runs-on: ubuntu-latest
if: >-
github.event_name == 'workflow_dispatch' && inputs.publish_solid_server
permissions:
contents: read
id-token: write # OIDC token npm exchanges for a short-lived publish credential + provenance signing
defaults:
run:
working-directory: packages/solid-server
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: wasm32-unknown-unknown
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
registry-url: "https://registry.npmjs.org"
# OIDC trusted publishing requires npm CLI >= 11.5.1 (Node 22 bundles an older npm).
- name: Pin npm CLI >= 11.5.1 (trusted-publishing floor)
run: |
npm install -g 'npm@^11.5.1'
npm --version
# Prebuilt, SHA-pinned, exactly-versioned wasm-pack (sq-khm3f, #5776): a
# `cargo install` recompiles it from crates.io and flakes on a registry blip.
- name: Install wasm-pack
uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
with:
version: v0.15.0
# Build the wasm bundle so the `files` allowlist packs a populated wasm/ tree. This package
# has NO `prepack`/`build` script (unlike @sparq-org/sparq / eyereasoner-compat), so the wasm is
# NOT rebuilt automatically on `npm publish` — build it explicitly here, before publish.
- name: Build wasm bundle
run: npm run build:lws-wasm
# `npm publish` packs ONLY the `files` allowlist (bin/ + src/ + wasm/ + README); runtime
# dependencies are resolved by the consumer at install time and are not bundled, so no npm
# install is needed for a correct tarball. Provenance is emitted by default under trusted
# publishing (--provenance is belt-and-suspenders). --access public: scoped package.
- name: Reverify remote release tag immediately before publish
env:
RELEASE_TAG: ${{ github.event.release.tag_name || github.ref_name }}
run: >-
python3 "$GITHUB_WORKSPACE/scripts/check-release-source.py"
--repo-root "$GITHUB_WORKSPACE" --tag "$RELEASE_TAG" --remote-tag origin
- name: Publish to npm (OIDC trusted publishing + provenance)
run: npm publish --provenance --access public
- name: Verify published provenance attestation
run: |
set -euo pipefail
ver=$(node -p "require('./package.json').version")
# The registry can take a few minutes to serve a just-published version (v0.1.5:
# about two), so poll before calling the publish unattested.
att=""
for _ in $(seq 1 20); do
att=$(npm view "@sparq-org/solid-server@${ver}" dist.attestations 2>/dev/null || true)
if [ -n "$att" ] && [ "$att" != "undefined" ]; then break; fi
sleep 30
done
echo "$att"
if [ -z "$att" ] || [ "$att" = "undefined" ]; then
echo "::error::no provenance attestation recorded for @sparq-org/solid-server@${ver}"
exit 1
fi
echo "Provenance attestation present. Consumers verify with: npm audit signatures"
# ---- crates.io: attest the packaged .crate artifacts (provenance over the publish bytes) ----
# crates.io has no native provenance; we attest the `cargo package` output (identical bytes to
# what `cargo publish` uploads) with the SAME mechanism release.yml uses. This does NOT publish
# to crates.io (the maintainer does that manually, in dependency order) — it produces a
# verifiable attestation of the artifacts and uploads them for the maintainer to verify
# before/after publishing. crates.io-native provenance stays an OPEN external sub-gap.
crates:
name: attest crate packages
needs: setup
runs-on: ubuntu-latest
if: >-
github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && inputs.attest_crates)
permissions:
contents: read
id-token: write # OIDC token for the Sigstore attestation
attestations: write # write the attestation to the repo's attestation store
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
- name: Rust (preinstalled stable)
# CI-economy (2026-07-18 directive): the ubuntu runner image
# ships a rustup stable toolchain; the SHA-pinned action's `rustup
# toolchain install stable` re-downloads the whole toolchain whenever
# the image lags the newest release. Use the preinstalled one —
# `rustup default`/`component add` never trigger a toolchain download.
run: |
rustup default stable
rustup show active-toolchain
# Package the crates.io publish set exactly as `cargo publish` would upload it:
# scripts/publish-strip.py first removes the research-only optional feature edges
# (and makes the vendored `sparq-spargebra` fork a member, so the set resolves before
# anything exists on crates.io); then one `cargo package --workspace` packages every
# publishable member in dependency order against a local overlay of the others.
# --no-verify skips the rebuild-from-tarball (publish runs verify itself); the bytes are
# identical to `cargo publish`'s upload. --allow-dirty because the strip edits manifests.
- name: Package publishable crates
run: |
set -euo pipefail
python3 -m pip install --quiet --require-hashes -r .github/requirements/publish-strip.txt
python3 scripts/publish-strip.py --with-vendored
# Ask Cargo which workspace packages are publishable. In metadata,
# `publish = false` is represented as an empty registry list.
mapfile -t packages < <(
cargo metadata --no-deps --format-version 1 |
jq -r '.packages[] | select(.publish != []) | .name' |
sort
)
expected=${#packages[@]}
if [ "$expected" -eq 0 ]; then
echo "::error::Cargo metadata reported no publishable crates"
exit 1
fi
cargo package --workspace --no-verify --allow-dirty
mkdir -p crate-pkgs
for pkg in "${packages[@]}"; do
find target/package -maxdepth 1 -name "${pkg}-[0-9]*.crate" -exec cp {} crate-pkgs/ \;
done
echo "Packaged crates:"; ls -l crate-pkgs/
packaged=$(find crate-pkgs -name '*.crate' | wc -l)
if [ "$packaged" -ne "$expected" ]; then
echo "::error::packaged count ${packaged} does not match expected count ${expected}"
exit 1
fi
- name: Reverify remote release tag immediately before attestation handoff
env:
RELEASE_TAG: ${{ github.event.release.tag_name || github.ref_name }}
run: python3 scripts/check-release-source.py --tag "$RELEASE_TAG" --remote-tag origin
# Same SHA-pinned attest action + version as release.yml — one signed SLSA provenance
# predicate per .crate, binding its digest to this workflow run.
- name: Attest crate-package provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: crate-pkgs/*.crate
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: crate-packages
path: crate-pkgs/*.crate
if-no-files-found: error
# ---- PyPI: build the `sparq-rdf` wheels + publish with native PEP-740 attestations ----
# sq-toze.37 / GX-10. Two-stage: a build matrix produces the abi3 wheels + sdist
# with maturin (mirroring the docs/release.md "Python wheels" plan + python.yml's build args),
# then a single publish job uploads them via PyPI Trusted Publishing. Trusted Publishing runs
# in this workflow's GitHub OIDC context (id-token: write) so PyPI mints a short-lived token
# AND records a PEP-740 digital attestation (an in-toto/Sigstore-signed provenance statement)
# for each uploaded file — the PyPI-native equivalent of npm `--provenance`.
#
# MAINTAINER PREREQUISITE (cannot be wired from this repo — see header + docs/release.md):
# The `sparq-rdf` PyPI project must have a Trusted Publisher registered (owner `sparq-org`,
# repo `sparq`, workflow `publish.yml`, environment `pypi`). Until that one-time PyPI-side
# config exists, the upload step below fails to mint an OIDC token (by design — no static
# API token is stored). The CI is fully wired; that PyPI account step is the only open item.
pypi-build:
name: build sparq-rdf wheels (${{ matrix.platform.target }})
needs: setup
if: >-
github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && inputs.publish_pypi)
strategy:
fail-fast: false
matrix:
platform:
- runner: ubuntu-latest
target: x86_64-unknown-linux-gnu
manylinux: auto
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
manylinux: auto
- runner: macos-latest
target: aarch64-apple-darwin
manylinux: ""
# sq-v286.12: macos-13 was retired by GitHub. macos-15-intel is the
# supported Intel-Mac runner line (matches dist.yml + release.yml), so the
# x86_64-apple-darwin wheel arch is preserved — NOT macos-14/macos-latest, which
# are Apple silicon and would clash with the aarch64-apple-darwin line above.
- runner: macos-15-intel
target: x86_64-apple-darwin
manylinux: ""
- runner: windows-latest
target: x86_64-pc-windows-msvc
manylinux: ""
runs-on: ${{ matrix.platform.runner }}
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
# maturin-action provisions the Rust toolchain + (on Linux) the manylinux container.
# `python-release` profile keeps unwinding panics — the plain release profile is
# panic=abort, which would turn a Rust panic into a hard interpreter abort (pyproject.toml).
# No `--release`: maturin >= 1.15 rejects it alongside `--profile` (v0.1.5's wheels failed on
# it), and python-release already inherits release.
- name: Build abi3 wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
target: ${{ matrix.platform.target }}
# auto on Linux builds inside the manylinux container; empty elsewhere.
manylinux: ${{ matrix.platform.manylinux }}
args: --profile python-release --manifest-path crates/sparq-py/Cargo.toml --out dist
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pypi-wheel-${{ matrix.platform.target }}
path: dist/*.whl
if-no-files-found: error
pypi-sdist:
name: build sparq-rdf sdist
needs: setup
if: >-
github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && inputs.publish_pypi)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
- name: Build sdist
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
command: sdist
args: --manifest-path crates/sparq-py/Cargo.toml --out dist
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pypi-sdist
path: dist/*.tar.gz
if-no-files-found: error
pypi-publish:
name: publish sparq-rdf to PyPI (PEP-740 attestations)
needs: [pypi-build, pypi-sdist]
runs-on: ubuntu-latest
# A GitHub Environment is part of the Trusted-Publisher binding (PyPI checks owner+repo+
# workflow+environment). It also lets a maintainer add a required reviewer / deployment
# protection so a publish cannot fire unattended even on a release event.
environment: pypi
permissions:
contents: read
id-token: write # OIDC token PyPI Trusted Publishing exchanges + PEP-740 attestation signing
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
persist-credentials: false
# Collect every wheel + the sdist into one dist/ tree (download-artifact merges patterns).
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: dist
pattern: pypi-*
merge-multiple: true
- name: List collected distributions
run: ls -l dist/
# PyPI Trusted Publishing + PEP-740: with id-token: write and a registered Trusted
# Publisher, this action mints a short-lived PyPI token via OIDC and (attestations: true,
# the default) generates + uploads a PEP-740 attestation per file. No static API token.
# `print-hash` echoes the uploaded digests into the log for audit.
- name: Reverify remote release tag immediately before publish
env:
RELEASE_TAG: ${{ github.event.release.tag_name || github.ref_name }}
run: python3 scripts/check-release-source.py --tag "$RELEASE_TAG" --remote-tag origin
- name: Publish to PyPI with provenance attestations
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
attestations: true
print-hash: true
packages-dir: dist/