Repository navigation
docs: README accuracy pass and a step-by-step guide to the cheat factory #19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |