Repository navigation
227 lines (197 loc) · 8.92 KB
/
Copy pathci.yml
File metadata and controls
227 lines (197 loc) · 8.92 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
name: CI
# Every push and pull request is tested and built. A GitHub release (tag v<version>,
# installer attached, notes generated from the commits) is created automatically only
# when a push to master carries a package.json version that has no release yet, so you
# publish by bumping the version, not by tagging by hand, and ordinary pushes do not
# create releases.
#
# Dry run: the release job can also be started by hand (Actions -> CI -> Run workflow). On any branch
# other than master it builds the installer and uploads it as a workflow artifact instead of
# publishing, which is how to check that a build works before merging.
on:
push:
pull_request:
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
# Never cancel a run on master: it may be the one publishing a release.
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
permissions:
contents: read
defaults:
run:
shell: bash
# Two Node versions on purpose. The native addon is BUILT with Node 22: Node 24+ headers use the
# ClangCL toolset, which the pinned node-gyp (9.4.1) cannot drive ("llvm-lib.exe exited with code 1").
# The tests RUN on Node 26 (what development uses): they load a TypeScript worker
# (src/main/ctImportSafe.ts) and import the compiled .node file directly, and Node 22 rejects that
# ("Unknown file extension .node"). The addon is N-API, which is ABI-stable, so a build made under 22
# loads fine under 26. Revisit after upgrading node-gyp.
#
# Both jobs run on windows-2022 rather than windows-latest on purpose: the latest image now
# ships Visual Studio 2026, which the pinned node-gyp (9.4.1) cannot find a compiler in
# ("unknown version ... could not find a version of Visual Studio 2017 or newer"). The 2022
# image has VS 2022, which is what the README asks contributors to install. Move to
# windows-latest after upgrading node-gyp.
jobs:
test:
name: Test and build
runs-on: windows-2022
steps:
- uses: actions/checkout@v4
# node-gyp 9's bundled gyp still imports distutils, which Python 3.12 (the runner default)
# removed. Build with 3.11 until node-gyp is upgraded.
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
# The native addon needs the C++ build tools, which the Windows runner ships.
# node-addon-api comes from the root install; node-gyp is a root dev dependency.
- name: Build native addon
id: native
working-directory: native
run: |
set -o pipefail
{ npx node-gyp configure && npx node-gyp build; } 2>&1 | tee "$RUNNER_TEMP/native-build.log"
# Job logs need a login to download, so on failure surface the toolchain and the tail
# of the build log as an annotation anyone can read on the run page.
- name: Explain native build failure
if: failure() && steps.native.outcome == 'failure'
run: |
{
echo "node $(node -v) | npm $(npm -v) | node-gyp $(npx node-gyp -v 2>&1 | head -1)"
echo "python: $(python --version 2>&1)"
echo "vswhere: $("/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe" -all -format value -property displayName 2>&1 | tr '\n' ';')"
echo "--- error lines in the build log ---"
grep -nE "Error|error|No module|not found|ERR!" "$RUNNER_TEMP/native-build.log" | grep -v "gyp info" | head -14
echo "--- last lines of the build log ---"
tail -n 14 "$RUNNER_TEMP/native-build.log"
} | sed -e 's/%/%25/g' -e 's/\r//g' | sed ':a;N;$!ba;s/\n/%0A/g' | sed 's/^/::error title=Native addon build failed::/'
# Everything from here runs the tests, so switch to the Node they need (see the note at the top).
- uses: actions/setup-node@v4
with:
node-version: 26
# The MCP server tests spawn its built dist/ and load the native addon.
- name: Build MCP server
working-directory: mcp-server
run: |
npm ci
npm run build
- name: Type check
run: npx tsc --noEmit
- name: Production bundle compiles
run: npm run build
- name: Unit and native-harness tests
id: tests
run: |
set -o pipefail
npx vitest run 2>&1 | tee "$RUNNER_TEMP/vitest.log"
# Same reason as the native build step: the job log needs a login, annotations do not.
- name: Explain test failure
if: failure() && steps.tests.outcome == 'failure'
run: |
{
grep -nE "FAIL|Error:|AssertionError|Segmentation|exited|timed out|Test Files|Tests " "$RUNNER_TEMP/vitest.log" | head -30
echo "--- tail of the test output ---"
tail -n 12 "$RUNNER_TEMP/vitest.log"
} | awk 'BEGIN { ORS = "%0A"; printf "::error title=Tests failed::" } { gsub(/%/, "%25"); gsub(/\r/, ""); print }'
release:
name: Publish release
needs: test
if: >-
(github.event_name == 'push' && github.ref == 'refs/heads/master') ||
github.event_name == 'workflow_dispatch'
runs-on: windows-2022
permissions:
contents: write # create the tag and the release
concurrency:
group: release
cancel-in-progress: false
env:
GH_TOKEN: ${{ github.token }}
# Anything that is not master builds but never publishes.
DRY_RUN: ${{ github.ref != 'refs/heads/master' }}
steps:
- uses: actions/checkout@v4
# Decide first, so a push that does not bump the version stops here in seconds
# instead of building an installer it will not publish.
- name: Is this version already released?
id: version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [ "$DRY_RUN" = "true" ]; then
echo "Dry run (not master): building v$VERSION but will not publish."
echo "publish=true" >> "$GITHUB_OUTPUT"
elif gh release view "v$VERSION" >/dev/null 2>&1; then
echo "v$VERSION is already released: nothing to publish."
echo "publish=false" >> "$GITHUB_OUTPUT"
else
echo "v$VERSION has no release yet: publishing."
echo "publish=true" >> "$GITHUB_OUTPUT"
fi
# node-gyp 9's bundled gyp still imports distutils, which Python 3.12 (the runner default)
# removed. Build with 3.11 until node-gyp is upgraded.
- uses: actions/setup-python@v5
if: steps.version.outputs.publish == 'true'
with:
python-version: "3.11"
- uses: actions/setup-node@v4
if: steps.version.outputs.publish == 'true'
with:
node-version: 22
cache: npm
- name: Install dependencies
if: steps.version.outputs.publish == 'true'
run: npm ci
- name: Build native addon
if: steps.version.outputs.publish == 'true'
working-directory: native
run: |
npx node-gyp configure
npx node-gyp build
# `dist` is `electron-vite build && electron-builder --win`. --publish never keeps
# electron-builder from uploading anything itself: the release step below does that.
- name: Build installer
if: steps.version.outputs.publish == 'true'
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false" # the installer is not code-signed
run: npm run dist -- --publish never
- name: Check the installer was built
if: steps.version.outputs.publish == 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
INSTALLER="release/Apprentice-Setup-${VERSION}.exe"
test -f "$INSTALLER" || { echo "Expected $INSTALLER was not built"; ls release; exit 1; }
(cd release && sha256sum "Apprentice-Setup-${VERSION}.exe" > SHA256SUMS.txt)
ls -l release/*.exe release/SHA256SUMS.txt
- name: Upload the installer (dry run)
if: steps.version.outputs.publish == 'true' && env.DRY_RUN == 'true'
uses: actions/upload-artifact@v4
with:
name: Apprentice-Setup-${{ steps.version.outputs.version }}
path: |
release/Apprentice-Setup-*.exe
release/SHA256SUMS.txt
retention-days: 3
- name: Create release
if: steps.version.outputs.publish == 'true' && env.DRY_RUN != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
INSTALLER="release/Apprentice-Setup-${VERSION}.exe"
# A version with a hyphen (0.1.2-beta) is a prerelease.
FLAGS=""
case "$VERSION" in *-*) FLAGS="--prerelease" ;; esac
gh release create "v${VERSION}" \
--target "$GITHUB_SHA" \
--title "Apprentice v${VERSION}" \
--generate-notes \
$FLAGS \
"$INSTALLER" release/SHA256SUMS.txt