Skip to content

Commit 51d3c75

Browse files
authored
Merge pull request #2 from ushironoko/npm-publish-supplychain
ci: add hardened npm publish pipeline (OIDC, provenance, pkg.pr.new)
2 parents 5140ecb + 7e3b6a0 commit 51d3c75

20 files changed

Lines changed: 1116 additions & 21 deletions

File tree

.changeset/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changesets
2+
3+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4+
with multi-package repos, or single-package repos to help you version and publish your code. You can
5+
find the full documentation for it [in our repository](https://github.com/changesets/changesets).
6+
7+
We have a quick list of common questions to get you started engaging with this project in
8+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).

.changeset/config.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
3+
"changelog": "@changesets/cli/changelog",
4+
"commit": false,
5+
"fixed": [],
6+
"linked": [],
7+
"access": "public",
8+
"baseBranch": "main",
9+
"updateInternalDependencies": "patch",
10+
"ignore": []
11+
}

.github/dependabot.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Keeps SHA-pinned GitHub Actions current. Dependabot reads the `# vX.Y.Z` comment next
2+
# to each pinned commit SHA and bumps both the SHA and the comment together, so pinning
3+
# does not freeze us on stale (potentially vulnerable) action versions.
4+
version: 2
5+
updates:
6+
- package-ecosystem: github-actions
7+
directory: /
8+
schedule:
9+
interval: weekly
10+
groups:
11+
github-actions:
12+
patterns: ["*"]

.github/workflows/ci.yml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,32 @@ jobs:
2020
runs-on: ubuntu-latest
2121
steps:
2222
- name: Checkout
23-
uses: actions/checkout@v4
23+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
24+
with:
25+
persist-credentials: false
2426

2527
# bun-version is read from package.json "packageManager" (bun@1.3.13) automatically.
2628
- name: Setup Bun
27-
uses: oven-sh/setup-bun@v2
29+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
2830

31+
# --ignore-scripts blocks dependency lifecycle scripts (the preinstall/postinstall
32+
# vector used by Shai-Hulud-class worms). Safe here: the native deps ship prebuilt
33+
# binaries via os/cpu-constrained optionalDependencies with no lifecycle scripts.
2934
- name: Install dependencies
30-
run: bun install --frozen-lockfile
35+
run: bun install --frozen-lockfile --ignore-scripts
36+
37+
# Fail if install mutated the lockfile (frozen install already forbids it; this
38+
# makes a tampered/desynced bun.lock an explicit, visible failure).
39+
- name: Verify lockfile is unchanged
40+
run: git diff --exit-code bun.lock
3141

3242
- name: Lint (biome)
3343
run: bun run lint
3444

3545
# Type lane: type-checks all sources, the *.test.ts files, and the *.test-d.ts
36-
# type-level assertions (expect-type / @ts-expect-error) via tsc.
46+
# type-level assertions (expect-type / @ts-expect-error) via tsgo
47+
# (@typescript/native-preview). Resolves on linux-x64 under --ignore-scripts
48+
# because the platform binary ships as an os/cpu-constrained optionalDependency.
3749
- name: Typecheck
3850
run: bun run test:type
3951

@@ -47,7 +59,7 @@ jobs:
4759

4860
- name: Upload coverage (lcov)
4961
if: ${{ always() }}
50-
uses: actions/upload-artifact@v4
62+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
5163
with:
5264
name: coverage-lcov
5365
path: coverage/lcov.info

.github/workflows/preview.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Continuous preview releases via pkg.pr.new. These are NOT published to npm — they are
2+
# ephemeral, install-from-URL tarballs served by pkg.pr.new (auth = the pkg-pr-new GitHub
3+
# App + GitHub Actions run metadata, no npm token, no secrets). Safe on untrusted fork
4+
# PRs because the job holds no secrets and only a read-only token.
5+
#
6+
# Prerequisite: install the pkg.pr.new GitHub App on this repo (https://github.com/apps/pkg-pr-new).
7+
name: Preview release
8+
9+
# branches: ["**"] matches branch pushes only, so tag pushes do not trigger this.
10+
on:
11+
push:
12+
branches: ["**"]
13+
pull_request:
14+
15+
# No secrets and no elevated token are needed; comments are posted by the GitHub App.
16+
permissions: {}
17+
18+
concurrency:
19+
group: preview-${{ github.workflow }}-${{ github.ref }}
20+
cancel-in-progress: true
21+
22+
jobs:
23+
preview:
24+
name: Publish preview (pkg.pr.new)
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
29+
with:
30+
persist-credentials: false
31+
32+
- name: Setup Bun
33+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
34+
35+
- name: Install dependencies
36+
run: bun install --frozen-lockfile --ignore-scripts
37+
38+
- name: Build
39+
run: bun run build
40+
41+
# Single invocation over ./packages/* so pkg.pr.new rewrites the @tskm/vite →
42+
# @tskm/compiler workspace dependency to the matching preview URL. Runs the
43+
# lockfile-installed binary (not bunx). examples/* are private and excluded.
44+
- name: Publish preview packages
45+
run: bun run preview

.github/workflows/release.yml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Release pipeline. Two jobs so the OIDC credential exists only where it is used:
2+
# - version: opens/updates the Changesets "Version Packages" PR and decides whether a
3+
# release is due. No id-token here.
4+
# - publish: runs only when an unpublished version is on main, behind a manual
5+
# `release` Environment gate, and is the ONLY job that can mint an npm OIDC token.
6+
#
7+
# Security boundary for a real publish = Version PR merge + main branch protection +
8+
# the `release` Environment approval. This is the workflow file registered as the npm
9+
# Trusted Publisher (settings on npmjs.com must reference `release.yml`).
10+
name: Release
11+
12+
on:
13+
push:
14+
branches: [main]
15+
workflow_dispatch:
16+
17+
# Never cancel a release mid-publish.
18+
concurrency:
19+
group: release-${{ github.ref }}
20+
cancel-in-progress: false
21+
22+
permissions:
23+
contents: read
24+
25+
jobs:
26+
version:
27+
name: Version PR & publish gate
28+
runs-on: ubuntu-latest
29+
permissions:
30+
contents: write # changesets/action commits + pushes the version branch
31+
pull-requests: write # opens/updates the Version PR
32+
outputs:
33+
should_publish: ${{ steps.gate.outputs.should_publish }}
34+
steps:
35+
# persist-credentials stays default (true): changesets/action pushes the version
36+
# branch using the checkout-provided git credentials.
37+
- name: Checkout
38+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
39+
40+
- name: Setup Bun
41+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
42+
43+
- name: Install dependencies
44+
run: bun install --frozen-lockfile --ignore-scripts
45+
46+
# Runs BEFORE changesets/action mutates the working tree, so it reads the COMMITTED
47+
# versions and compares them to the registry. should_publish=true only on a
48+
# Version-PR-merge commit (versions bumped, changesets consumed). Fail-closed.
49+
- name: Publish gate (should_publish)
50+
id: gate
51+
run: bun run scripts/publish.ts --check
52+
53+
- name: Create or update the Version PR
54+
uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0
55+
with:
56+
version: bun run version
57+
# No `publish:` input — publishing is the separate, OIDC-gated job below.
58+
env:
59+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60+
61+
publish:
62+
name: Publish to npm (OIDC + provenance)
63+
needs: version
64+
if: needs.version.outputs.should_publish == 'true'
65+
runs-on: ubuntu-latest
66+
environment: release # manual approval gate (configure required reviewers)
67+
permissions:
68+
contents: read
69+
id-token: write # OIDC trusted publishing — the credential is scoped to this job only
70+
steps:
71+
- name: Checkout
72+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
73+
with:
74+
persist-credentials: false # publish auth is OIDC, not git
75+
76+
- name: Setup Bun
77+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
78+
79+
- name: Setup Node
80+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
81+
with:
82+
node-version: "22" # latest 22.x (OIDC trusted publishing requires >= 22.14)
83+
registry-url: "https://registry.npmjs.org"
84+
85+
# OIDC trusted publishing requires npm >= 11.5.1; pin it explicitly.
86+
- name: Pin npm
87+
run: npm install -g npm@11.5.1
88+
89+
- name: Assert toolchain
90+
run: |
91+
node --version
92+
npm --version
93+
node -e "const v=process.versions.node.split('.').map(Number); if (v[0]<22 || (v[0]===22 && v[1]<14)) throw new Error('Node >= 22.14 required for OIDC, got '+process.versions.node)"
94+
95+
- name: Install dependencies
96+
run: bun install --frozen-lockfile --ignore-scripts
97+
98+
- name: Build
99+
run: bun run build
100+
101+
- name: Pre-publish gates (publint + are-the-types-wrong)
102+
run: |
103+
bun run publint
104+
bun run attw
105+
106+
- name: Clean-room install smoke test
107+
run: bun run scripts/smoke.ts
108+
109+
# Ordered npm publish (compiler → tskm → vite) with workspace:* rewrite, fail-closed
110+
# idempotency, and a workspace:-leak guard. OIDC + provenance are automatic here.
111+
- name: Publish
112+
run: bun run release

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 ushironoko
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

0 commit comments

Comments
 (0)