feat(rpc): expose node version + git commit on getNetworkInfo#864
Conversation
…) via getNetworkInfo
Motivation: when chasing a "fix-merged-to-stabilisation-but-the-symptom-
still-reproduces" bug, the first question is always "did this node get
rebuilt after the fix landed?". Without a deterministic answer the
investigation forks into "is the fix actually wrong?" vs. "is the
operator running stale bytes?". Surfacing the running binary's
provenance on the existing fork-status RPC closes that branch in one
round-trip.
`src/utilities/nodeVersion.ts` is a single source of truth that
resolves at module evaluation (frozen for the binary's lifetime):
- `name` + `version` from top-level `package.json`
- `commit` + `commitShort` from `.git/HEAD` walked up from cwd
(handles ref-pointer HEADs, detached
HEADs, and `packed-refs` fallback)
- `branch` from the HEAD ref name when applicable
- `dirty` via `git diff-index --quiet HEAD`,
tolerant of missing git binary
- `builtAt` from BUILT_AT env if a Docker image
baked one in; otherwise null
Env-var overrides (`GIT_COMMIT`, `GIT_BRANCH`, `GIT_DIRTY`) take
precedence when present so a stripped image (`git clone --depth 0`,
multistage builds that don't ship `.git/`) can still surface
meaningful values from build args. Every lookup is defensive — every
individual failure path lands on `null`, never throws — so a
corrupted `.git/HEAD` or a runtime missing `child_process` cannot
panic the node. `getNetworkInfo` MUST keep answering even when
provenance is unknown.
`NetworkInfo.nodeVersion` is additive — older SDKs that only
destructure `forks` keep working. Operators get the info via the
same call they already poll for fork status:
$ curl -sS -X POST -H 'Content-Type: application/json' \
-d '{"method":"nodeCall","params":[{"type":"nodeCall",
"message":"getNetworkInfo","data":{}}]}' http://node.../
"nodeVersion": {
"name": "demos-node-software",
"version": "0.9.8",
"commit": "4fff076da34456a99a9eac2a5caadf0b300476c8",
"commitShort": "4fff076",
"branch": "stabilisation",
"dirty": false,
"builtAt": null
}
Test plan: smoke-tested locally via `bun --eval` import of the module
on the dev branch (returned the expected current SHA + branch). No
unit test added — module is pure-IO with environment dependencies that
are tedious to mock and the failure modes are all "return null", not
"throw". A future PR can add a fixture-driven test if regressions
appear.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
Warning Review limit reached
More reviews will be available in 40 minutes and 22 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR exposes build provenance (
Confidence Score: 3/5Safe to merge after adding a timeout to the The src/utilities/nodeVersion.ts — specifically the Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant forkHandlers
participant NODE_VERSION
participant readGitInfo
participant fs as .git/HEAD
participant git as git binary
Note over NODE_VERSION,git: Module evaluation (once at startup)
NODE_VERSION->>readGitInfo: readGitInfo()
readGitInfo->>fs: readFileSync(.git/HEAD)
fs-->>readGitInfo: ref or SHA
readGitInfo->>git: execFileSync(diff-index --quiet HEAD)
git-->>readGitInfo: exit 0 (clean) / exit 1 (dirty)
readGitInfo-->>NODE_VERSION: "{commit, branch, dirty}"
Note over NODE_VERSION: frozen snapshot
Client->>forkHandlers: getNetworkInfo RPC
forkHandlers->>NODE_VERSION: NODE_VERSION (read frozen value)
NODE_VERSION-->>forkHandlers: NodeVersionInfo
forkHandlers-->>Client: "{forks: {...}, nodeVersion: {...}}"
Reviews (1): Last reviewed commit: "feat(rpc): surface node identity (name +..." | Re-trigger Greptile |
| try { | ||
| execFileSync("git", ["diff-index", "--quiet", "HEAD"], { | ||
| cwd: repoRoot, | ||
| stdio: "ignore", | ||
| }) | ||
| dirty = false | ||
| } catch (e) { | ||
| const code = (e as { status?: number }).status | ||
| // Exit 1 = dirty. Anything else (binary missing, unreadable | ||
| // refs) = "we don't know"; report clean rather than panic. | ||
| dirty = code === 1 | ||
| } |
There was a problem hiding this comment.
execFileSync without a timeout blocks node startup indefinitely
execFileSync is called synchronously during module evaluation with no timeout option. If git is installed but hangs — stale lock file at .git/index.lock, a credential helper prompting for input, or an NFS-backed .git/ with a dropped connection — the entire node process hangs at startup with no way to recover. The comment describes tolerating a missing binary, but a hung binary is not covered. Adding timeout: 5000 (or similar) ensures the call fails fast when git is unresponsive.
| try { | |
| execFileSync("git", ["diff-index", "--quiet", "HEAD"], { | |
| cwd: repoRoot, | |
| stdio: "ignore", | |
| }) | |
| dirty = false | |
| } catch (e) { | |
| const code = (e as { status?: number }).status | |
| // Exit 1 = dirty. Anything else (binary missing, unreadable | |
| // refs) = "we don't know"; report clean rather than panic. | |
| dirty = code === 1 | |
| } | |
| execFileSync("git", ["diff-index", "--quiet", "HEAD"], { | |
| cwd: repoRoot, | |
| stdio: "ignore", | |
| timeout: 5000, | |
| }) |
| /** Full 40-char git SHA, or `null` if not resolvable. */ | ||
| commit: string | null |
There was a problem hiding this comment.
The
GIT_COMMIT env var accepts 7–40 character strings (the regex lower bound is 7), but NodeVersionInfo.commit is documented as "Full 40-char git SHA." When GIT_COMMIT carries a short SHA, commitShort becomes identical to commit, and any downstream consumer relying on commit being the full SHA will get a truncated value without warning. The JSDoc should reflect the actual range.
| /** Full 40-char git SHA, or `null` if not resolvable. */ | |
| commit: string | null | |
| /** Full git SHA, or `null` if not resolvable. When sourced from the `GIT_COMMIT` env var, may be a short SHA (7–40 chars). */ | |
| commit: string | null |
… require() (#865) The require("../../package.json") path used in PR #864 silently failed in production (the rebuilt dev.node2 returned name: "demos-node" + version: "0.0.0" — the catch-clause defaults). bun + tsconfig-paths under ESM has unreliable behaviour around `require()`-resolving JSON, and the failure mode was a silent catch falling through to the wrong answer rather than a loud throw. Walk from `import.meta.url` up to the first directory containing a readable package.json — same pattern the .git/HEAD walker below already uses, so both halves of the module now behave identically. Skips nested workspace manifests by requiring a non-empty version field. Hard cap at 16 levels so a corrupted fs cannot loop. Defensive fileURLToPath fallback to cwd preserves the "every failure path returns null" invariant. After this fix, dev.node2's getNetworkInfo.nodeVersion surfaces the real package name + semver + git provenance instead of sentinel defaults, restoring the diagnostic value the original PR intended. Co-authored-by: tcsenpai <tcsenpai@discus.sh>
…from docker-run (#866) Closes the gap left by PR #864/#865: src/utilities/nodeVersion.ts already reads process.env.GIT_COMMIT/GIT_BRANCH/GIT_DIRTY when present, but nothing on the docker path was supplying them. The result was that getNetworkInfo.nodeVersion came back with commit: null on every Docker- booted node, defeating the original "is this host running the binary I think it is?" diagnostic value. Three pieces wire it end-to-end: 1) Dockerfile (runtime stage): ARG GIT_COMMIT/GIT_BRANCH/GIT_DIRTY/ BUILT_AT, then re-export each via ENV so the running node sees them in process.env. Defaults are empty strings — a build without the args (manual `docker build .`) still produces a runnable image, nodeVersion just surfaces null for the missing fields. 2) docker-compose.yml (node service build block): args passes the four values straight through with `:-` defaults, so any caller that exported them (the wrapper below, CI, an operator's shell) gets them baked in; anyone who didn't gets an empty-string ARG and the same null-field fallback. 3) scripts/docker-run: detects the working tree's git status with defensive guards (no-repo, no-git-binary, dirty-vs-clean) and exports the four variables before any `docker compose build`/`up`. `git diff-index --quiet HEAD` exit code → "true"/"false" string the nodeVersion module already parses. BUILT_AT is the UTC ISO-8601 timestamp at invocation. Operator-visible effect: after `./scripts/docker-run --rebuild -d`, curl getNetworkInfo and the response carries the real commit SHA, branch name, dirty flag, and build timestamp the image was made from. That answers "did this host actually pick up the fix I merged?" with a one-liner. Manual repro paths still work: docker compose build --build-arg GIT_COMMIT=$(git rev-parse HEAD) GIT_COMMIT=abc123 docker compose build Co-authored-by: tcsenpai <tcsenpai@discus.sh>
Summary
Adds
nodeVersionto thegetNetworkInforesponse so operators (and the SDK) can tell at a glance which binary a node is actually running.The trigger was: PR #861 (GCREdit hash normalisation) landed on
stabilisation, butdev.node2.demos.sh:53552kept returningGCREdit mismatchon every post-fork transfer. The investigation forked into "is the fix wrong?" vs. "did the host get rebuilt?" — the latter would have been a one-second answer if the RPC had told us the running commit. Same diagnostic value for any future fix that gets merged and rolled out across multiple validator hosts.Shape
NetworkInfonow carries anodeVersionblock:{ "forks": { "osDenomination": { … } }, "nodeVersion": { "name": "demos-node-software", "version": "0.9.8", "commit": "4fff076da34456a99a9eac2a5caadf0b300476c8", "commitShort": "4fff076", "branch": "stabilisation", "dirty": false, "builtAt": null } }Additive — older SDKs that only destructure
forkskeep working.Sources
src/utilities/nodeVersion.tsresolves at module evaluation (frozen for the binary's lifetime):name+version— top-levelpackage.jsoncommit+commitShort—.git/HEADwalked up fromcwd(handles ref-pointer HEADs, detached HEADs,packed-refsfallback)branch— HEAD ref name when applicabledirty—git diff-index --quiet HEAD, tolerant of missing git binarybuiltAt—BUILT_ATenv when a Docker image baked one in, elsenullEnv overrides (
GIT_COMMIT,GIT_BRANCH,GIT_DIRTY) take precedence when present so stripped images (git clone --depth 0, multistage builds that don't ship.git/) can still surface meaningful values from build args.Defensive failure modes
Every individual failure path lands on
null. A corrupted.git/HEAD, missinggitbinary, or unreadable refs cannot panic the node —getNetworkInfoMUST keep answering even when provenance is unknown.Test plan
bun run type-check-ts— no new errors (pre-existingchainBlocks.tsduplicate-import +worker-threads-testerrors unchanged)bun --evalimport returned the expected current SHA, short SHA, branch, and dirty flag from the working tree.nullinstead of throwing. A future PR can add a fixture-driven test if regressions appear.Operator usage