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
name: npm
on:
release:
types:
workflow_dispatch:
permissions:
contents: write
id-token: write
defaults:
run:
shell: bash
env:
PRODUCT_BASENAME: mdka
TAG: ${{ github.ref_name }} # tag or branch name
JOB_WORKDIR: tmp-${{ github.run_id }} # unique number
jobs:
verify-ci:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
actions: read
contents: read
steps:
# Releases are cut from commits pushed straight to main, where CI is
# advisory. This is the enforcement point: nothing publishes unless
# ci.yaml concluded success on this exact commit. Coupled by name to
# ci.yaml — if that file is renamed, this fails closed (blocks), which
# is the safe direction.
- name: Require green CI on the released commit
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
run_id=$(gh run list --commit "${{ github.sha }}" \
--workflow ci.yaml --limit 1 --json databaseId \
--jq '.[0].databaseId // empty')
if [ -z "$run_id" ]; then
echo "::error::No CI run found for ${{ github.sha }}. Refusing to publish."
exit 1
fi
gh run watch "$run_id" --exit-status
build:
needs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- name: Linux-x64-gnu
target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive_ext: .tar.gz
napiplatform: linux-x64-gnu
- name: macOS-aarch64
target: aarch64-apple-darwin
os: macos-latest
archive_ext: .zip
napiplatform: darwin-arm64
- name: Windows-x64
target: x86_64-pc-windows-msvc
os: windows-latest
archive_ext: .zip
napiplatform: win32-x64-msvc
steps:
- name: Checkout repository
uses: actions/checkout@v6
# [ Node.js dependencies ]
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
cache-dependency-path: node/package-lock.json
- name: Install npm packages
working-directory: node
run: npm install
# [ build ]
- name: Install Rust
run: bash ".github/workflows/scripts/install-rust.sh" stable ${{ matrix.target }}
- name: Cache cargo dependencies and build
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Build Rust executable
working-directory: node
run: |
npm run build
# [ release asset ]
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: bindings-${{ matrix.target }}
path: node/${{ env.PRODUCT_BASENAME }}.*.node
if-no-files-found: error
publish:
name: Publish on release tags
needs:
- build
- verify-ci
runs-on: ubuntu-latest
steps:
# This is the SECOND of two independent tag-pattern checks. The first is
# create-release.yaml's `on.push.tags` filter, which matches X.Y.Z only.
# Both must accept a tag for the automated path to publish it end to end.
#
# Deliberate asymmetry: this check also accepts X.Y.Z-rc.N, but
# create-release.yaml's filter does not. Release candidates are therefore
# cut by creating the GitHub release by hand, which still fans out via
# `release: created`. Tag-push automation is for final releases only.
- name: Check if release tag
run: |
if [[ "${TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || \
[[ "${TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
echo "${TAG}: Semantic versioning tagged - OK"
else
echo "::warning::${TAG} is not a release tag (expected X.Y.Z or X.Y.Z-rc.N)."
echo "SKIPPED DELIBERATELY - this is not an npm publishing failure."
echo "The red status below is a known limitation: this step exits non-zero"
echo "to stop the job, so an intentional skip and a real failure look alike."
exit 1
fi
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'
cache: npm
cache-dependency-path: node/package-lock.json
- name: Install dependencies
working-directory: node
run: |
npm install
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: node/artifacts
- name: Move artifacts
working-directory: node
run: |
npx napi create-npm-dirs
npx napi artifacts
# RFC 020: this step was missing entirely, which is why `npm install
# mdka` produced an unusable package for the whole 2.x line -- the
# per-platform packages were never published past 1.6.9, and the main
# package's manifest never carried optionalDependencies at all.
#
# --no-gh-release is required, not optional: `napi pre-publish`
# defaults --gh-release to true and would otherwise attempt to create
# a second GitHub release for a tag create-release.yaml already
# released, which fails (non-fatally -- it's caught and logged -- but
# there is no reason to invite the noise).
- name: napi pre-publish (publish per-platform packages, inject optionalDependencies)
working-directory: node
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx napi pre-publish --no-gh-release
# No `npm ci` here. The `napi pre-publish` step above rewrites
# package.json to add optionalDependencies for the per-platform
# packages, which puts it out of sync with package-lock.json --
# `npm ci` refuses to run in that state and fails the publish. It was
# also redundant: the "Install dependencies" step above already ran
# `npm install`, and `npm publish` does not need node_modules.
# Removed 2026-09-01 after it blocked the 2.2.1 npm publish, which is
# the first release in which pre-publish ever ran.
- name: Publish
working-directory: node
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
cp -f ../README.md .
npm publish