Skip to content

docs: README accuracy pass and a step-by-step guide to the cheat factory #19

docs: README accuracy pass and a step-by-step guide to the cheat factory

docs: README accuracy pass and a step-by-step guide to the cheat factory #19

Workflow file for this run

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