v0.12.0-rc.2 — queries open read-only (concurrent use no longer corrupts the graph) #26
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: Publish to npm | |
| # Triggered by publishing a GitHub Release. Builds, verifies, then publishes | |
| # engramgraph to npm via OIDC Trusted Publishing — NO token needed. | |
| # | |
| # Setup (one-time, on npmjs.com → engramgraph → Settings → Trusted | |
| # Publisher): add a GitHub Actions trusted publisher with | |
| # organization/user: AsiaOstrich repository: EngramGraph workflow: publish.yml | |
| # npm then mints short-lived credentials from this job's OIDC id-token; the repo | |
| # must be public. Provenance is generated automatically. | |
| on: | |
| release: | |
| types: [published] | |
| jobs: | |
| # DOES THE SUITE STILL PASS AGAINST WHAT USERS WILL ACTUALLY INSTALL? | |
| # // implements XSPEC-366 R3 | |
| # | |
| # A published npm package does not carry its lockfile. The `publish` job | |
| # below installs with `package-lock.json` in place, so it tests the versions | |
| # this repo pins. A consumer runs `npm install engramgraph` and gets whatever | |
| # the declared ranges resolve to at that moment. When the two differ, the | |
| # entire suite is green about a combination nobody installs. | |
| # | |
| # That is not hypothetical here. `"tree-sitter-c-sharp": "^0.23.1"` spanned | |
| # three published versions with incompatible APIs; npm took the newest, and | |
| # **no .cs file ever parsed for anyone who installed from npm** while every | |
| # test passed, for the whole life of C# support. XSPEC-365 has the full | |
| # account; XSPEC-366 R2 pinned the grammars afterwards. | |
| # | |
| # Pinning fixed the dependencies that were known to be dangerous. This job | |
| # exists for the ones that are not known yet: it deletes the lockfile, | |
| # resolves everything the way a consumer would, and runs the real suite | |
| # against the result. It does not need to be told which dependency will | |
| # break next. | |
| # | |
| # `publish` needs it, so a failure here stops the release rather than | |
| # producing a version that is green in CI and broken on installation. | |
| consumer-resolution: | |
| name: Test against consumer resolution (no lockfile) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - run: npm install -g npm@latest | |
| - name: Record what the lockfile pins, then remove it | |
| run: | | |
| set -e | |
| node -e " | |
| const lock = require('./package-lock.json'); | |
| const pkg = require('./package.json'); | |
| const declared = { ...pkg.dependencies, ...pkg.optionalDependencies }; | |
| const rows = Object.keys(declared).map((name) => ({ | |
| name, | |
| range: declared[name], | |
| locked: (lock.packages['node_modules/' + name] || {}).version || null, | |
| })); | |
| require('fs').writeFileSync('locked-versions.json', JSON.stringify(rows, null, 2)); | |
| console.log('Recorded ' + rows.length + ' runtime dependencies from the lockfile.'); | |
| " | |
| rm package-lock.json | |
| # Resolves every range fresh, exactly as a consumer's install does. | |
| - run: npm install --legacy-peer-deps | |
| - name: Report which dependencies resolved differently from what is pinned | |
| run: | | |
| set -e | |
| # Printed whether or not anything differs. A silent job leaves "the | |
| # suite passed against identical versions" and "the suite passed | |
| # against sixteen untested ones" looking the same in the log, and | |
| # only one of those is worth knowing about later. | |
| # Versions are read straight off disk, NOT through require(). A | |
| # package may block '/package.json' in its `exports` map — hono, | |
| # ryugraph and @modelcontextprotocol/sdk all do — and require() then | |
| # throws for a package that is installed and fine. The first version | |
| # of this step did exactly that and reported three drifts where there | |
| # were two, one of them fabricated. A drift report that invents | |
| # drifts is worse than none: it trains the reader to ignore it. | |
| node -e " | |
| const { readFileSync, existsSync } = require('node:fs'); | |
| const rows = require('./locked-versions.json'); | |
| let drifted = 0; | |
| let unreadable = 0; | |
| for (const row of rows) { | |
| const manifest = 'node_modules/' + row.name + '/package.json'; | |
| if (!existsSync(manifest)) { | |
| unreadable++; | |
| console.log(' ' + row.name + ' ' + row.range + ' pinned=' + row.locked + ' NOT INSTALLED'); | |
| continue; | |
| } | |
| const actual = JSON.parse(readFileSync(manifest, 'utf8')).version; | |
| if (actual !== row.locked) { | |
| drifted++; | |
| console.log(' ' + row.name + ' ' + row.range + ' pinned=' + row.locked + ' resolved=' + actual); | |
| } | |
| } | |
| if (unreadable > 0) { | |
| console.log(''); | |
| console.error(unreadable + ' declared dependencies are not installed — this check cannot speak for them.'); | |
| process.exitCode = 1; | |
| } | |
| console.log(''); | |
| console.log(drifted + ' of ' + rows.length + ' runtime dependencies resolved to a version this repo does not pin.'); | |
| console.log(drifted === 0 | |
| ? 'Consumers install exactly what the lockfile tests.' | |
| : 'The suite below runs against THOSE versions — the ones users actually get.'); | |
| " | |
| # ryugraph's ALGO extension is fetched from an unreliable host; the same | |
| # from-source build the publish job uses, so this job fails on the code | |
| # rather than on that download. | |
| - name: Build ryugraph ALGO extension from source | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y -qq cmake | |
| cd node_modules/ryugraph/ryu-source | |
| make extension-release EXTENSION_LIST=algo NUM_THREADS="$(nproc)" | |
| mkdir -p "$HOME/.ryu/extension/25.9.0/linux_amd64/algo" | |
| cp extension/algo/build/libalgo.ryu_extension "$HOME/.ryu/extension/25.9.0/linux_amd64/algo/" | |
| - run: npm run build | |
| - run: npm run test:coverage | |
| publish: | |
| name: Publish to npm | |
| needs: consumer-resolution | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write # OIDC trusted publishing + provenance | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # NOTE: no `registry-url` — it makes setup-node write an empty | |
| # //registry.npmjs.org/:_authToken to .npmrc, which suppresses OIDC | |
| # trusted publishing (npm takes the empty-token path → anonymous → E404). | |
| # Without it, npm uses the default registry and engages OIDC. | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| # OIDC trusted publishing requires npm >= 11.5.1 (newer than the bundled npm). | |
| - run: npm install -g npm@latest | |
| - run: npm --version | |
| # npm >= 11 gates native install scripts behind an approval list by | |
| # default (this broke this exact workflow on 2026-07-10 once `npm | |
| # install -g npm@latest` above started pulling npm 11.x: DTS build | |
| # failed with "Cannot find module 'ryugraph'", because ryugraph's own | |
| # install step — which copies its native binary into place — never | |
| # ran). The fix lives in package.json's `allowScripts` field | |
| # (declarative, version-pinned pre-approval for this repo's known | |
| # native deps: ryugraph/tree-sitter*/esbuild) rather than an | |
| # imperative `--all` here, so a plain install picks it up with no | |
| # extra step and no silent trust extended to future dependencies. | |
| - run: npm install --legacy-peer-deps | |
| # ryugraph's `god-nodes`/`communities`/`related` commands need its ALGO | |
| # extension (PageRank/Louvain), which isn't bundled — ryugraph normally | |
| # downloads it on first use from extension.ryugraph.io. That host is | |
| # unreliable (confirmed 2026-07-11: unreachable from this exact runner, | |
| # TCP connect just hangs — not a DNS issue). Without a fallback, tests | |
| # that trigger the download hang for 10+ minutes before finally timing | |
| # out (see structural-memory.test.ts's L3 tests on 2026-07-10 run | |
| # 29105565151). Building the extension from the source ryugraph already | |
| # ships (node_modules/ryugraph/ryu-source) and dropping it straight into | |
| # the cache path ryugraph checks first means `INSTALL ALGO` never has to | |
| # touch the network at all — verified locally with the download host | |
| # blackholed via /etc/hosts (full 81/81 tests, ~7s, vs. 617s+ hanging | |
| # before this step existed). | |
| # | |
| # `EXTENSION_LIST=algo` + the Makefile's `extension-release` target | |
| # (-DBUILD_RYU=FALSE) builds only this extension, not the whole engine. | |
| # Cache path/version/platform-string format taken from ryujs.node's own | |
| # embedded path template and cross-checked against predictable-labs/ | |
| # ryugraph#48's real-world report (25.9.0 is the extension catalog | |
| # version — distinct from, and older than, the ryugraph@25.9.1 npm | |
| # package version). | |
| - name: Build ryugraph ALGO extension from source (bypass unreliable download host) | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y -qq cmake | |
| cd node_modules/ryugraph/ryu-source | |
| make extension-release EXTENSION_LIST=algo NUM_THREADS="$(nproc)" | |
| mkdir -p "$HOME/.ryu/extension/25.9.0/linux_amd64/algo" | |
| cp extension/algo/build/libalgo.ryu_extension "$HOME/.ryu/extension/25.9.0/linux_amd64/algo/" | |
| # Build + verify before publishing (native: kuzu + tree-sitter compile here). | |
| - run: npm run build | |
| - run: npm run typecheck | |
| # XSPEC-073 G2 — test:coverage rather than test. | |
| # The thresholds added to vitest.config.ts only apply when coverage is | |
| # collected, so plain `vitest run` would leave them as decoration — the | |
| # state UDS cli's thresholds sat in for months. This is also the only | |
| # workflow in the repo that runs tests at all (both workflows here trigger | |
| # on `release` only, so there is no push/PR CI to attach a gate to). | |
| - run: npm run test:coverage | |
| # WHAT DOES A CONSUMER'S TREE LOOK LIKE, NOT WHAT DOES THIS REPO'S? | |
| # | |
| # The script packs the tarball, installs it into an empty directory and | |
| # audits that. Auditing in place would be cheaper and would answer the | |
| # wrong question: package.json overrides cmake-js to ^8.0.0 (fddd07e, | |
| # "clearing remaining tar CVEs"), and npm overrides govern the project | |
| # that declares them rather than travelling with a published package. So | |
| # `npm audit --omit=dev` here reports nothing while `npm install | |
| # engramgraph@0.9.1` into an empty directory pulls cmake-js@7.4.0 and | |
| # tar@6.2.1 — twelve advisories, one critical. | |
| # | |
| # That gap is not news: README's "Dependency vulnerability warnings" | |
| # section explains it, links the upstream issue (ryugraph#49) and gives | |
| # consumers the override to apply themselves. What was missing is anything | |
| # that checks. Prose does not notice when the count grows — the README | |
| # says "4 high severity", and a consumer install today reports one | |
| # critical and three high — and prose says nothing at all when some | |
| # *other* dependency starts doing the same thing. | |
| # | |
| # Findings are cleared through security/accepted-advisories.json, where | |
| # every entry carries an expiry date. | |
| - name: Audit what consumers install (not what this repo pins) | |
| run: npm run audit:ship | |
| - name: Determine npm dist-tag | |
| id: tag | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| if echo "$VERSION" | grep -qE '\-(beta|alpha|rc)\.'; then | |
| echo "tag=next" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "tag=latest" >> "$GITHUB_OUTPUT" | |
| fi | |
| # No NODE_AUTH_TOKEN: npm authenticates via the OIDC id-token against the | |
| # trusted publisher configured on npmjs. Provenance is automatic. | |
| - name: Publish engramgraph (OIDC trusted publishing) | |
| run: npm publish --access public --tag ${{ steps.tag.outputs.tag }} |