Skip to content

v0.12.0-rc.2 — queries open read-only (concurrent use no longer corrupts the graph) #27

v0.12.0-rc.2 — queries open read-only (concurrent use no longer corrupts the graph)

v0.12.0-rc.2 — queries open read-only (concurrent use no longer corrupts the graph) #27

name: Cross-Platform Compatibility Check
# Release-time gate — NOT a per-PR check (keeps day-to-day CI cost/time
# unchanged). Verifies a published npm version actually installs and loads
# across the real target platform matrix: Windows/Linux/macOS x ARM64/AMD64,
# plus a Linux glibc-version check. Implements XSPEC-328 R2/R3/R5.
#
# Background: on 2026-07-10, engramgraph@0.3.0's dependency ryugraph@25.9.1
# was found broken on Linux ARM64 (wrong binary shipped, see
# predictable-labs/ryugraph#48) and on Linux x64 with glibc < 2.38 (e.g.
# Debian 12 bookworm, Ubuntu 22.04 LTS) — neither was caught by the existing
# publish.yml CI, which only runs on GitHub's ubuntu-latest (x64, glibc
# 2.39+, i.e. squarely in the "safe zone" for both bugs).
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Package version to test (default: latest published on npm)'
required: false
default: ''
include_intel_mac:
description: 'Also run the Intel Mac job (queues ~50min — see below)'
type: boolean
required: false
default: false
jobs:
resolve-version:
name: Resolve version under test
runs-on: ubuntu-latest
outputs:
package-spec: ${{ steps.resolve.outputs.package-spec }}
steps:
- name: Determine target version and wait for npm registry propagation
id: resolve
shell: bash
run: |
set -e
INPUT_VERSION="${{ github.event.inputs.version }}"
if [ -n "$INPUT_VERSION" ]; then
SPEC="engramgraph@${INPUT_VERSION}"
elif [ "${{ github.event_name }}" = "release" ]; then
# Strip a leading "v" from the git tag (v0.4.0 -> 0.4.0)
TAG="${{ github.event.release.tag_name }}"
SPEC="engramgraph@${TAG#v}"
else
SPEC="engramgraph@latest"
fi
# HOW LONG TO WAIT, AND WHY THIS NUMBER (XSPEC-365 R6).
#
# This job used to poll 10 times at 15s = ~2.5 minutes, and it lost
# that race every single time. Both this workflow and publish.yml
# trigger on `release: published` and start in the same second, but
# the package only reaches the registry when publish.yml finishes —
# measured at 3m36s / 3m31s / 3m51s / 3m36s for v0.5.0 through
# v0.8.0. Every release was ~60-80s short. The consequence was not a
# flaky gate, it was NO gate: `resolve-version` failed, and with it
# every matrix job was skipped (0s), across four consecutive
# releases, while the README cited this workflow as proof that
# Windows and macOS were verified.
#
# 40 attempts x 15s = 10 minutes, roughly 2.5x the slowest observed
# publish. Sized against measurement rather than intuition, and
# deliberately generous: the cost of waiting too long is a few
# runner-minutes, while the cost of waiting too little is a gate that
# reports failure in a way indistinguishable from a real breakage.
ATTEMPTS=40
echo "Waiting for $SPEC to be resolvable on the npm registry..."
for i in $(seq 1 "$ATTEMPTS"); do
if npm view "$SPEC" version >/dev/null 2>&1; then
echo "Found $SPEC on npm registry after ~$(( (i - 1) * 15 ))s."
echo "package-spec=$SPEC" >> "$GITHUB_OUTPUT"
exit 0
fi
echo " attempt $i/$ATTEMPTS: not yet available, waiting 15s..."
sleep 15
done
# TELL THE TWO FAILURES APART (XSPEC-365 R6).
#
# "the package never got published" and "a platform cannot install
# it" both used to surface as this one workflow going red, which is
# how a gate that had never run at all went unnoticed for four
# releases. This branch is the former, and says so explicitly: no
# platform was tested, so nothing about platform support can be
# concluded from this run either way.
echo "::error title=Package never reached npm — NO platform was tested::" \
"$SPEC did not appear on the npm registry within $(( ATTEMPTS * 15 / 60 )) minutes." \
"This is a publish/registry problem, NOT a platform-compatibility failure:" \
"the matrix below did not run, so this release is UNVERIFIED rather than broken." \
"Check whether publish.yml succeeded for this tag."
exit 1
# NOTE: intentionally does NOT include macos-13 (Intel Mac). GitHub's
# hosted Intel Mac runners currently have severe queue-capacity issues —
# a real run on 2026-07-10 sat in "queued" for ~50 minutes with no start —
# and `timeout-minutes` cannot bound queue wait (it only starts counting
# once a job begins executing), so there is no way to cap how long this
# would block a release. Apple's own support lifecycle backs this up:
# macOS 26 Tahoe is the last release with Intel support, macOS 27 (Sept
# 2026) drops it entirely, and only security-only updates continue until
# ~2029 — Intel Mac is a sunsetting platform on both Apple's and GitHub's
# side. See the best-effort `macos-x64-intel-manual` job below instead.
# KNOWN-BROKEN PLATFORMS ARE EXPECTATIONS, NOT FAILURES (XSPEC-365 R6).
#
# `linux-arm64` cannot work: upstream ryugraph ships the x86-64 binary under
# the arm64 filename (predictable-labs/ryugraph#48). Before this change it
# failed every run, and so did the workflow — which meant the top-level
# red/green carried no information at all. "It went red again" and "something
# new just broke" were indistinguishable, which is the exact failure this
# whole spec was written to diagnose, sitting inside the gate meant to fix it.
#
# So a known-broken platform now declares `expect-fail`, and the assertion is
# inverted rather than suppressed. `continue-on-error: true` alone would have
# been worse than the disease: it hides a newly-broken platform just as
# effectively as an expected one, and it never tells you when upstream is
# fixed. Here, failing is the pass condition — and PASSING is a failure,
# because the README documents these platforms as broken, and a doc that
# overstates breakage is as wrong as one that understates it.
platform-matrix:
name: ${{ matrix.label }}
needs: resolve-version
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
label: linux-x64
- os: ubuntu-24.04-arm
label: linux-arm64
expect-fail: true
reason: 'upstream ryugraph ships the x86-64 binary under the arm64 filename (predictable-labs/ryugraph#48)'
- os: windows-latest
label: windows-x64
- os: macos-latest
label: macos-arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- id: smoke
# Tolerated here so the outcome can be compared against the expectation
# in the next step. Nothing is suppressed: every path below either
# passes deliberately or fails loudly.
continue-on-error: true
uses: ./.github/actions/fresh-install-smoke-test
with:
package-spec: ${{ needs.resolve-version.outputs.package-spec }}
smoke-command: 'npx egr --version'
- name: Compare the outcome against what this platform is expected to do
shell: bash
env:
OUTCOME: ${{ steps.smoke.outcome }}
EXPECT_FAIL: ${{ matrix.expect-fail }}
REASON: ${{ matrix.reason }}
run: |
set -e
if [ "$EXPECT_FAIL" = "true" ]; then
if [ "$OUTCOME" = "failure" ]; then
echo "${{ matrix.label }}: failed as documented — $REASON"
echo "Not a regression. The README records this platform as broken."
exit 0
fi
echo "::error title=${{ matrix.label }} works now — the docs are wrong::" \
"This platform is documented as broken ($REASON) and it just installed" \
"successfully. Upstream appears fixed. Update the README platform matrix" \
"and drop expect-fail from this matrix entry — a doc that overstates" \
"breakage sends people away from a platform that works."
exit 1
fi
if [ "$OUTCOME" = "success" ]; then
echo "${{ matrix.label }}: OK"
exit 0
fi
echo "::error title=${{ matrix.label }} is broken::This platform is expected to work and did not."
exit 1
# The job the platform matrix above cannot be (XSPEC-365 R6 / AC-9).
#
# `windows-latest` ships Visual Studio with the C++ workload preinstalled, so
# the matrix job above proves the package installs on a Windows machine that
# already has a compiler — which is not the machine this was broken for. Most
# Windows users do not have one, and for them `npm install -g engramgraph`
# failed outright until XSPEC-365, because the Dart grammar (prebuilt for
# linux-x64 only) had to be compiled and was a hard dependency.
#
# Rather than trying to uninstall Visual Studio from a hosted runner, this
# makes node-gyp refuse the one that is there:
#
# env npm_config_msvs_version=2019
# -> node-gyp.js strips `npm_config_`, maps `_` to `-` => opts['msvs-version']
# -> configure.js passes it as `configMsvsVersion`
# -> find-visualstudio.js sets configVersionYear = 2019
# -> checkConfigVersion() rejects every installation whose year differs
#
# Traced through node-gyp's source rather than assumed, because a knob that
# quietly did nothing would leave this job green over an untested surface —
# the exact failure this whole workflow exists to stop — and then confirmed
# against a real run on 2026-08-04, which logged:
#
# --msvs_version=2019 was set on the command line
# checking VS2026 (18.8.12023.21) found at "...\Microsoft Visual Studio\18\Enterprise"
# - found VC++ toolset: v145
# - msvs_version does not match this version
# could not find a version of Visual Studio 2017 or newer to use
#
# Note what that run also showed, since it contradicts a claim made earlier
# in this project's docs: the runner carries Visual Studio 2026 with a
# working v145 toolset, and node-gyp 12.x recognises it fine (it offers
# "2026" as a valid msvs_version). It is only node-gyp 11.x — the version
# npm 11 bundles — that cannot see VS 2026 at all. This job does not depend
# on either behaviour: whichever Visual Studio the runner happens to ship,
# asking for a year it does not have rejects it.
#
# Set as a raw environment variable rather than via `npm config`: npm 11
# deprecated arbitrary keys in its own config, but node-gyp reads
# `process.env` directly, so this route is unaffected.
windows-no-toolchain:
name: windows-x64-no-toolchain
needs: resolve-version
runs-on: windows-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Install with the C++ toolchain made unusable
shell: bash
env:
# See the job comment: makes node-gyp discard the runner's Visual Studio.
npm_config_msvs_version: "2019"
run: |
set -o pipefail
# No `set -e` guard on the install itself: the whole point of this job
# is that npm must SUCCEED here. If the Dart grammar's build failure
# aborts the install, this step fails, which is the regression the job
# exists to catch.
npm install -g "${{ needs.resolve-version.outputs.package-spec }}" 2>&1 | tee install.log
# npm >= 11 holds native install scripts behind an approval gate. The
# first version of this job missed that and never reached a compile at
# all — it asserted against an install where node-gyp had not run, and
# would have passed a package that could not build anything. Approve
# and rebuild so the Dart compile is genuinely attempted and genuinely
# fails on this toolchain-less runner.
npm approve-scripts --all 2>&1 | tail -5 || true
npm rebuild -g 2>&1 | tail -20 || true
- name: egr works, minus Dart
shell: bash
run: |
set -e
# R2a: the CLI exists at all, which is the thing that used to be
# impossible here — the install aborted before producing one.
egr --version
# R2 + the doctor command: the user-facing channel. `egr doctor` is
# asserted rather than the install output on purpose. The install-time
# notice this job used to check reaches nobody: npm suppresses
# lifecycle-script output by default and gates the scripts entirely,
# so it appeared zero times on a real install. A command the user
# types cannot be suppressed by a package manager.
egr doctor 2>&1 | tee doctor.log
grep -q "Dart" doctor.log \
|| { echo "::error::doctor does not mention Dart at all"; exit 1; }
# Dart must be reported unavailable WITH a reason. "unavailable" on
# its own sends the reader nowhere.
if grep -qE "^ ✗ Dart" doctor.log; then
grep -E "^ ✗ Dart" doctor.log | grep -q "vokturz" \
|| { echo "::error::Dart is unavailable but doctor names no package/reason"; exit 1; }
echo "Dart correctly reported unavailable with a reason."
else
echo "::error::Dart loaded on a runner with no usable C++ toolchain — either the toolchain block failed (check the msvs_version log above) or a prebuilt binary appeared upstream. Either way this job is no longer testing what it claims."
exit 1
fi
# Every other language must still be there. Losing one is the design;
# losing several would mean the degradation is over-broad.
grep -q "12 available, 1 unavailable" doctor.log \
|| { echo "::error::expected 12 available / 1 unavailable"; grep "languages:" doctor.log; exit 1; }
- name: Indexing still works, and says what it skipped
shell: bash
run: |
set -e
mkdir -p probe/src
printf 'export function a() { return b(); }\nexport function b() { return 1; }\n' > probe/src/a.ts
printf 'public class C { public string M() { return "x"; } }\n' > probe/src/b.cs
printf 'void main() { greet(); }\nvoid greet() {}\n' > probe/src/c.dart
cd probe
egr index ./src 2>&1 | tee index.log
# TypeScript and C# must be in the graph. C# specifically: it is the
# language that never parsed for anyone who installed from npm before
# the grammar versions were pinned.
grep -qE "code: [2-9][0-9]* files" index.log \
|| { echo "::error::fewer files indexed than expected"; exit 1; }
grep -q "0 failed" index.log \
|| { echo "::error::some files failed to parse"; exit 1; }
# And the Dart file must be reported as skipped, by name. Silence here
# is the worse bug: the files would simply be absent from the graph
# with nothing to say a whole language had been dropped.
grep -q "skipped:" index.log \
|| { echo "::error::no 'skipped:' line — degradation was not reported"; exit 1; }
grep "skipped:" index.log | grep -q "Dart" \
|| { echo "::error::skipped line does not name Dart"; exit 1; }
# Same inversion as the matrix above (XSPEC-365 R6). glibc < 2.38 cannot work:
# upstream ryugraph's binary needs a newer glibc than Ubuntu 22.04 LTS and
# Debian 12 ship. Failing is the documented state; succeeding means upstream
# moved and the README's "❌ Broken" row became a false claim.
glibc-matrix:
name: linux-x64-glibc-compat
needs: resolve-version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: glibc
continue-on-error: true
uses: ./.github/actions/glibc-compat-check
with:
package-spec: ${{ needs.resolve-version.outputs.package-spec }}
smoke-command: 'npx egr --version'
- name: Compare the outcome against the documented glibc state
shell: bash
env:
OUTCOME: ${{ steps.glibc.outcome }}
run: |
set -e
if [ "$OUTCOME" = "failure" ]; then
echo "glibc compat: failed as documented — upstream ryugraph requires glibc >= 2.38."
echo "Not a regression. The README records glibc < 2.38 as broken."
exit 0
fi
echo "::error title=glibc compat passes now — the docs are wrong::" \
"The README records glibc < 2.38 as broken and this check just passed." \
"Update the platform matrix and drop this inversion."
exit 1
# Best-effort only — never blocks a release. Only runs when explicitly
# requested via workflow_dispatch (not on `release: published`), so a
# release is never left waiting on an Intel Mac runner that may never
# become available. Run manually if you specifically want to check
# Intel Mac compatibility.
macos-x64-intel-manual:
name: macos-x64-intel (manual, best-effort)
# Opt-in, not "any manual run" (XSPEC-365 R6 follow-up). Gating this on
# `workflow_dispatch` alone made every manual run inherit the ~50-minute
# Intel Mac queue this job's own comment documents — the whole run sits
# incomplete until it lands, which makes `workflow_dispatch` useless for
# the thing it is most needed for: checking a change to this workflow
# without cutting a release. Measured on 2026-08-04: every other job
# finished within ~4 minutes while this one was still queued.
if: github.event_name == 'workflow_dispatch' && inputs.include_intel_mac
needs: resolve-version
runs-on: macos-13
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- uses: ./.github/actions/fresh-install-smoke-test
with:
package-spec: ${{ needs.resolve-version.outputs.package-spec }}
smoke-command: 'npx egr --version'