v0.12.0-rc.2 — queries open read-only (concurrent use no longer corrupts the graph) #27
Workflow file for this run
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: 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' |