* chore(ci): add shared vendored-grammars manifest; monitor reads it .github/vendored-grammars.json is the single source of truth for the vendored tree-sitter grammars (c/swift/kotlin/dart/proto): name, upstream coords, and policy holds. update-vendored-grammars.mjs now builds its GRAMMARS map from the manifest (behavior-preserving — same exported shape). Adds manifest-agreement tests so the loader can't silently skew from the file. * fix(ci): classify vendored grammars from manifest, drop bare "?" (#858) The readiness report decided "is this vendored?" via is_vendored_pin (a file: package.json spec) — but the 5 vendored grammars aren't in package.json, so they were misrouted through the npm path and rendered bare "?" for ABI (read from an empty node_modules), plus a spurious "? (fetch failed)" for github-only proto. Now vendored grammars are classified by membership in the shared manifest and their ABI is read from gitnexus/vendor/<name>/src/parser.c (always in a checkout). github-only vendored grammars skip the npm peer-dep fetch; the tree-sitter-c hold is surfaced from the manifest (held, not plain "Ready"); and every remaining unintrospectable value renders a labeled token, never a bare "?". --assert-current now covers the vendored grammars too instead of skipping them. Adds a stdlib unittest suite incl. a manifest⇄vendor-dir consistency guard. * docs(ci): document the shared vendored-grammars manifest Both tree-sitter workflow headers now point at .github/vendored-grammars.json as the shared source of truth; the readiness workflow gains a PR-path trigger on the manifest + test, and runs the readiness unit tests on validation events. CONTRIBUTING.md documents the manifest contract under CI automation contracts. * fix(review): apply autofix feedback - Guard manifest reads in both scripts with a clear error (was an opaque module-import traceback that crashed the script and test collection). - Never render a bare "?": relabel the npm-path ABI/version/peer sentinels and the vendored upstream-ABI miss to labeled tokens; the report is now ?-free regardless of node_modules/network, and the test is hermetic. - Add a VENDORED_NAMES ⊆ GRAMMARS guard + manifest-missing error test. - Drop now-dead is_vendored_pin/is_vendored/_(vendored)_. - Compose held + out-of-range vendored blocker reasons instead of overwriting. - Reword the shared-manifest docs to not over-claim shared upstream coords. * fix(ci): apply root prettier formatting to mjs + ts test The quality/format gate runs root `prettier --check .` (printWidth 100, the gitnexus-local config differs and falsely passed locally). * test(ci): make both tree-sitter scripts testable offline The scripts hit live npm/GitHub, which makes the report run flaky and the monitor's detect/apply logic untestable. Add hermetic seams: - readiness: --offline flag (+ GITNEXUS_TS_READINESS_OFFLINE env) no-ops the npm registry + upstream fetches; the report renders deterministically (vendored ABIs from the repo, npm columns marked 'offline', no bare '?'). 3 tests assert an offline run touches ZERO network (urlopen patched to raise). - monitor: detect() and apply() accept injected deps (vendoredVersion/ resolveUpstream/fetchSource/readAbi) so the newer/ABI/hold gating runs offline with fixtures; apply gains --dry-run (validates but writes nothing). 6 tests cover newer/same-version/held-c/ABI-15/applicable + a no-mutation dry-run. * fix(review): keep --assert-current hermetic + harden the no-bare-? invariant Tri-review findings (PR #2187): - P2 REGRESSION: --assert-current (documented 'hermetic and offline', run in CI without --offline) routed the 5 vendored grammars through vendored_drift_summary, which fetches upstream parser.c + commit sha — 10 discarded network calls per run. Fix: read the vendored ABI locally via a new vendored_abi_from_repo() helper (also used by vendored_drift_summary). Now verifiably network-free. - Unify the upstream-ABI miss sentinel: prose said 'n/a (generated at build)' while the matrix said 'n/a' — and 'generated at build' is a wrong cause (swift HAS a committed parser.c). Both now render neutral 'n/a'. - Fix the stale assert_current docstring claiming swift is prebuilt-only/no parser.c. - Guard the last latent bare-? path (vendor package.json missing 'version'). Tests: AssertCurrent (network-free guard + out-of-range via the new injection point), malformed-JSON manifest, detect() error-path, explicit npm/github undefined assertions. 17 Python + 15 vitest, all hermetic. * fix(review): use a single unittest import style (CodeQL 753) CodeQL py/import-and-import-from flagged `import unittest` + `from unittest import mock`. Collapse to `from unittest import TestCase, main, mock`. * fix(review): explicit raise in _matrix_row (CodeQL 754) CodeQL py/mixed-returns flagged the implicit fall-through after self.fail() (which it doesn't model as NoReturn). End with an explicit raise AssertionError. * test(review): replace non-null assertions with a must() guard @typescript-eslint/no-non-null-assertion flagged 4 `!` operators. Add a narrowing must<T>(value, message) helper (throws on undefined) and a named baseResolveUpstream, removing every non-null assertion. * fix(review): unguessable heredoc delimiter for the report output The report embeds the manifest `hold` field (fork-PR-editable); a fixed DRIFT_EOF delimiter in a hold value could close the $GITHUB_OUTPUT heredoc early and inject output keys. Use DRIFT_EOF_$(openssl rand -hex 16) — a value the report cannot contain. (Randomized delimiter over base64: keeps REPORT raw markdown, no consumer-side decode.) * fix(review): scope issues:write to scheduled runs (two-job split) GitHub Actions has no step-level permissions, so the only way to keep PR runs (incl. forks) from receiving `issues: write` is to split the job. A `report` job (contents:read, all events) renders the report + the PR `:⚠️:` and exposes report/exit_code as job outputs; a schedule-only `upsert-issue` job (needs: report, issues:write, no checkout) consumes them for the issue upsert + close. The 'Check upgrade readiness' check name is preserved. * fix(review): launder npm-version '?' in disposition prose The disposition bucket prose interpolated r['npm_version'] raw, so a successful 200 npm /latest response lacking a 'version' key would render a bare '?' (the matrix cell already laundered it). Add npm_version_label ('unknown' for '?') and use it in all five bucket renderers. Test a version-less npm response. * refactor(review): load_vendored_manifest returns only the consumed 'hold' The readiness script reads only the grammar names + 'hold'; the 'key' and 'upstream' fields were phantom data (upstream-drift coords live in the script's own GRAMMARS map). Narrow the return to {hold}. * fix(review): unify detect()/apply() 'newer' check for github grammars detect() compared the bare sha7 while apply() compared up.version (the full <base>-g<sha7> provenance string apply() also writes). After the bot re-vendored a github grammar once, detect() reported a perpetual false 'update available' while apply() correctly saw 'already current' — a noisy job summary + wasted --apply subprocess (the PR-exists guard absorbed it before any duplicate PR). Extract a shared isNewer(up, have) helper used by both. Tests cover equal- provenance (false), first-vendoring plain-version (true, not suppressed), and sha-advanced (true). Coupled with U12 (the detect⇄apply agreement assertion lives there once apply()'s not-newer path returns instead of process.exit). * test(review): cover main()'s out-of-range + prebuilt-only vendored ABI branches main()'s vendored-ABI classification reads through vendored_abi_from_repo (the local-read seam --assert-current uses), so patching it drives the 'Vendored (ABI out of range)' blocker branch and the prebuilt-only (vendored_abi None → 'prebuilt' cell, not '?') branch — neither reachable today since all 5 vendor dirs ship parser.c at ABI 14. * test(review): monitor-side manifest⇄vendor-dir consistency guard Mirror the Python consistency guard on the monitor side — the monitor consumes the same manifest and is the side that WRITES files from manifest `name`, so manifest/vendor-dir drift must fail CI here too. * fix(review): validate grammar names at manifest load (path-traversal guard) The manifest `name` is joined into gitnexus/vendor/<name> paths in both scripts (and apply() WRITES there), so reject any name not matching tree-sitter-[a-z0-9-]+ at the single load chokepoint — defense-in-depth even though the live trust boundary already prevents exploitation. loadManifestGrammars gains an injectable `raw` arg + export for testing; tests reject a '../etc' name in both scripts. * refactor(review): apply() throws ApplyExit; CLI maps to exit codes apply()'s 4 process.exit calls killed the vitest worker, blocking in-process tests of its error branches. Replace them with a thrown ApplyExit{code}; the not-newer (already-current) path returns `have` instead of exit(0). The isMain CLI block try/catches and maps ApplyExit.code → process.exit, so the monitor's subprocess contract (exit 0/2/3) is byte-identical (verified via subprocess smoke). Tests cover unknown-key=2, held=3, ABI-reject=3, and not-newer (returns current, no throw, no write). * refactor(review): extract vendored render helper; trim docstrings (<1000 lines) Extract the 'Vendored parsers' prose render into _render_vendored_section() so main() coordinates named phases rather than inlining a ~450-line monolith, and condense the most verbose docstrings/comments. The script drops from 1092 to 999 lines (under the 1000 bar the maintainability review flagged). Behavior-preserving: the deterministic --offline render is byte-identical before/after (verified in-place), --assert-current still passes, and the full unit suite is green. * fix(review): row-diff regex captures only the Status cell The change-detection regex captured the whole row tail as group 2, so any non-status cell drift (e.g. an upstream-ABI bump) emitted a false-positive 'change' line. Capture only the Status cell ([^|]+? before the final |$). The workflow parseRows regex and the Python _ROW_DIFF_RE stay byte-identical; the stability test now asserts group 2 is the status string (e.g. c → 'Vendored — held') and contains no pipe. * fix(ci): hoist intro string out of the list literal (CodeQL 755) The U13 extraction moved the 'Vendored parsers' intro paragraph (implicitly concatenated string literals) INTO a list literal, tripping CodeQL py/implicit-string-concatenation-in-list (reads as a possibly-missing comma between elements). Hoist it into a parenthesized `intro` variable. Render is byte-identical.
19 KiB
Contributing to GitNexus
How to propose changes, run checks locally, and open pull requests.
License
This project uses the PolyForm Noncommercial License 1.0.0. By contributing, you agree your contributions are licensed under the same terms unless stated otherwise.
Where to discuss
- Issues & feature ideas: use GitHub Issues for the upstream repo, or your fork’s tracker if you work from a fork.
- Community: see the Discord link in the root README.md.
Development setup
Prerequisites: Node.js — gitnexus/ requires >=22.0.0 and gitnexus-web/ requires ^20.19.0 || >=22.12.0 (enforced via the engines field in each package). Use nvm install to match the local version.
- Clone the repository.
- CLI / MCP package:
cd gitnexus && npm install && npm run build - Web UI (if needed):
cd gitnexus-web && npm install - Run tests as described in TESTING.md.
Containerized development (optional)
If you prefer an isolated environment with Claude Code, OpenAI Codex CLI, and Cursor CLI pre-installed, open the repo in VS Code with the Dev Containers extension and run Dev Containers: Reopen in Container. See .devcontainer/README.md for first-time auth flows and Windows WSL2 setup.
Branch and pull requests
- Use short-lived branches off the default branch of the repo you are targeting.
- PR titles MUST follow the conventional-commit format —
pr-labeler.ymlenforces this on every PR and auto-applies the matching label so release notes group the change correctly. - PR description: what changed, why, how to verify (commands), and any risk or rollback notes.
Pull request titles
Format: <type>[(scope)][!]: <subject>
Allowed types and the release-notes section each one lands in (defined in .github/release.yml):
| Type | Label applied | Release-notes section |
|---|---|---|
feat |
enhancement |
🚀 Features |
fix |
bug |
🐛 Bug Fixes |
perf |
performance |
🏎️ Performance |
refactor |
refactor |
🔄 Refactoring |
test |
test |
🧪 Tests |
ci |
ci |
👷 CI/CD |
build / deps |
dependencies |
📦 Dependencies |
docs |
documentation |
(grouped under Other Changes unless a Docs section is added) |
chore / revert |
chore |
(excluded from release notes) |
Append ! to the type (e.g. feat(api)!: drop /v1 endpoint) or include BREAKING CHANGE: in the PR body to flag a breaking change — the labeler then adds the breaking label and the 💥 Breaking Changes section is rendered first.
Examples:
feat(web): add smart chat scroll
fix(extractors): resolve silent contract mis-resolution
perf: avoid O(n²) traversal in heritage walker
chore(deps): bump vitest to 3.0.0
ci: standardize workflow concurrency
Commits within a PR may use any style — only the merged PR title shows up in release notes, so that's the one the convention applies to.
Before you open a PR
- Tests pass for the packages you touched (
gitnexusand/orgitnexus-web). - Typecheck passes:
npx tsc --noEmitingitnexus/andnpx tsc -b --noEmitingitnexus-web/. - No secrets, tokens, or machine-specific paths committed.
- Documentation updated if behavior or public CLI/MCP contract changes.
- Pre-commit hook runs clean (
.husky/pre-commit— formatting via lint-staged + typecheck for staged packages; tests run in CI only).
Code review
Maintainers may request changes for correctness, tests, performance, or consistency with existing patterns. Keeping diffs focused makes review faster.
GitHub Actions — Concurrency Convention
Every workflow under .github/workflows/ MUST declare a top-level concurrency: block using this convention:
-
Group key starts with
${{ github.workflow }}so no two workflows can collide on the same group name. The discriminator that follows is chosen per event shape:- Branch/tag scope:
${{ github.workflow }}-${{ github.ref }} - Per-PR scope (for
issue_comment,pull_request_review*,pull_requestmeta events):${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number }} workflow_runscope (e.g.ci-report.yml):${{ github.workflow }}-${{ github.event.workflow_run.pull_requests[0].number || format('{0}/{1}', github.event.workflow_run.head_repository.full_name, github.event.workflow_run.head_branch) }}— the fork fallback must be stable across reruns (neverworkflow_run.id, which is per-run-unique and defeats serialization).- Global single-slot (manual dispatch utilities):
${{ github.workflow }} - Reusable workflows invoked via
workflow_call: do NOT use${{ github.workflow }}in the group key — in called-workflow context its evaluation is ambiguous and can resolve to the caller's name, which would deadlock against the caller's own group. Use a hardcoded literal prefix and agithub.event_name-aware expression that falls through togithub.run_idfor reusable invocations (seeci.ymlfor the canonical form). Approved literal prefixes:CI-(ci.yml) anddocker-build-push-(docker.yml). Thecheck-workflow-concurrency.pyvalidation script must be updated whenever a new approved literal prefix is added. - Merge queue (
merge_group): when this event is added, use${{ github.workflow }}-${{ github.event.merge_group.head_ref }}withcancel-in-progress: false(every queue entry is a distinct ref; never cancel).
- Branch/tag scope:
-
cancel-in-progresspolicy:Event cancel-in-progressWhy pull_requestCI runtrueNew push supersedes old run pushtomainfalseEvery main commit gets validated Tag push ( v*publish)falseNever cancel mid-publish pushtomainfor release-candidatefalseNever cancel mid-RC publish workflow_dispatch(release/publish)falseManual runs are intentional workflow_run(sticky-comment reports)falseSerialize, don't race Per-PR bot workflows ( @claude, review)falseSerialize comments per PR PR-meta re-checks (pr-description-check) trueCheap, latest wins Single-slot utilities (triage sweep) trueLatest dispatch supersedes -
For workflows that serve multiple events at once (e.g.
ci.ymlhandlespull_request,push, andworkflow_call), makecancel-in-progressevent-aware:concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} -
When adding a new workflow, copy the concurrency block from an existing workflow of the same event shape.
CI automation contracts
Two workflows produce machine-readable signals on every PR. Coding agents and humans alike can rely on the names and shapes below — change them with intent.
gitnexus/autofix
pr-autofix.yml (untrusted) + pr-autofix-publish.yml (trusted) run prettier --write and eslint --fix against the PR head and surface a single ChatOps button on the PR. Three signals are emitted:
| Surface | Where | Notes |
|---|---|---|
| Sticky PR comment | Top-level comment with the HTML marker <!-- gitnexus:pr-autofix-summary --> and heading ## :sparkles: PR Autofix. Only posted when there is something to fix; clean PRs stay silent. |
Edit-in-place via marker; one comment per PR. |
| Fenced JSON block | Inside the sticky, fenced as gitnexus-autofix. Schema gitnexus.pr-autofix/v2 with fields state (fixes-available), pr_number, head_sha, changed_lines, run_id, and apply_command (literal /autofix). |
Parseable signal — preferred over regexing prose. v1 fields preserved as a superset. |
| Check Run | Stable name gitnexus/autofix on the PR head SHA. Conclusion: success (clean) or neutral (fixes-available). The neutral title is Autofix available — comment /autofix to apply. |
Surfaced under PR Checks; readable via gh pr checks <pr>. |
To detect outcome from an agent: gh pr checks <pr> --json name,conclusion,output | jq '.[] | select(.name == "gitnexus/autofix")'.
Forks are supported. The untrusted half runs fork code with permissions: {} and ships the diff as an artifact; the trusted publish job consumes only the diff (data, not code) and posts the comment + check run.
Applying autofix
Comment /autofix on the PR (whole-line, no arguments). The pr-autofix-apply.yml workflow:
- Validates the comment body matches
^/autofix\s*$exactly. Quoted or inline mentions are silently ignored. - Validates the commenter has
admin,write, ormaintainpermission on the repo, OR is the PR author. Other commenters get a 👎 reaction and a refusal reply. - Locates the most recent successful
pr-autofix.ymlrun for the PR's current head SHA, downloads itsautofixartifact, applies the patch, and pushes achore(autofix): ...commit back to the PR head branch. - Reacts ✅ on success, 👎 on stale-patch / push-failure, and posts a short reply with the apply-run URL in either case.
The apply workflow runs from the default branch's copy of the file regardless of where the comment originates — that's the trust anchor. There is no diff-size cap (the apply workflow uses git apply + push, not the GitHub review-comment API).
For fork PRs, the push succeeds only when the contributor has Allow edits by maintainers enabled on the PR (the default). When they have disabled it, the workflow fails loud with a 👎 reaction and an explanation comment.
Re-invoking /autofix after a successful apply is a safe no-op — the workflow detects the already-applied state via git apply --check --reverse and reacts ✅ without pushing.
Sensitive paths. The apply workflow refuses any patch that touches .github/ (workflow files, CODEOWNERS, dependabot config). A malicious PR could ship a custom prettier or ESLint config that reformats workflow YAML; if accepted, those edits would be pushed under contents: write without human review. Apply formatter changes to files under .github/ manually in a normal commit so they get the same review every other workflow change gets.
Vendored tree-sitter grammars
.github/vendored-grammars.json is the single source of truth for the vendored tree-sitter grammar set and each grammar's policy hold (the ones shipped from gitnexus/vendor/<name> rather than installed from npm). It lists each grammar's name, upstream coords (npm or github), and any hold. The monitor resolves upstreams from it; the readiness report keeps its own upstream-drift coords and reads vendored ABIs from gitnexus/vendor/. Two workflows read it:
grammar-update-monitor.yml(.github/scripts/update-vendored-grammars.mjs) — weekly; opens auto-PRs re-vendoring ABI-compatible upstream updates.tree-sitter-upgrade-readiness.yml(.github/scripts/check-tree-sitter-upgrade-readiness.py) — daily; renders the tree-sitter-0.25 readiness report (issue #858), reading each vendored grammar's ABI fromgitnexus/vendor/<name>/src/parser.c.
Sharing the manifest keeps the two aligned: a consistency-guard test asserts the manifest set equals the gitnexus/vendor/tree-sitter-* directories. When you vendor a new grammar (or remove one), update .github/vendored-grammars.json in the same change — otherwise that guard fails CI and the readiness report regresses to ? placeholders.
AI-assisted contributions
If you use coding agents, follow project context files (e.g. AGENTS.md, CLAUDE.md) and avoid drive-by refactors unrelated to the issue. Prefer incremental, test-backed changes.
Releases
One workflow ships gitnexus to npm — .github/workflows/publish.yml. It
routes between two modes based on the triggering event:
-
Stable mode — triggered by pushing any
v<X.Y.Z>tag (no-rc.*suffix; RC tags are excluded at trigger via a negative glob). Publishes to thelatestdist-tag with a changelog-backed GitHub release. Maintainers are expected to tag frommainas a convention; the workflow itself does not enforce branch reachability. No Docker build (RC-only). Before cutting a stable release, keepgitnexus/package.json,gitnexus-claude-plugin/.claude-plugin/plugin.json,.claude-plugin/marketplace.json, and the matchingCHANGELOG.mdentry in lockstep — the always-ongitnexusunit suite now fails if those manifest versions drift. -
Release-candidate mode — runs on every push to
main(typically a merged PR) plus manualworkflow_dispatch. Docs-only changes are skipped viapaths-ignore. Publishes to thercdist-tag with versionX.Y.Z-rc.Nand a GitHub prerelease, where:X.Y.Zis selected automatically. On push (and on dispatch withbump: auto, the default) the workflow continues the active rc cycle: if the registry already hasX.Y.Z-rc.*versions withX.Y.Z> currentlatest, it reuses the highest such base; otherwise it patch-bumps fromlatest. Dispatching withbump: patch|minor|majorresets the cycle fromlatest.Nis auto-incremented against existingX.Y.Z-rc.*entries on the registry. First rc for a given base isrc.1.- After the npm publish succeeds, the workflow calls
docker.ymlas a reusable workflow to build and push the corresponding RC Docker images (e.g.ghcr.io/abhigyanpatwari/gitnexus:1.7.0-rc.1, mirrored todocker.io/akonlabs/gitnexus:1.7.0-rc.1). The images are signed with Cosign; the OIDC identity isdocker.yml@refs/heads/main(the caller's ref — see README.md § Docker for the verify command).
Idempotency: the workflow pushes an
rc/<HEAD_SHA>marker tag and av<RC>release tag atomically, before callingnpm publish. The RC guard refuses to re-run once the marker exists, so a post-publish failure will not mint a duplicate rc for the same commit. Thev<RC>tag points at a detached release commit whosepackage.jsonmatches the npm tarball exactly (traceable releases). The RC tag is excluded from this workflow'spush: tags:filter, so it does not re-trigger publishing — preventing the double-publish failure mode tracked in #1609. Recovery after a partial failure: the workflow'sif: failure()cleanup step in thepublishjob auto-deletes the v-tag and marker on most post-publish failures, so the typical retry is just:gh workflow run publish.yml --ref main -f force=true # or push a new commit to main, which will cut a fresh RCIf auto-cleanup didn't run (e.g. the cleanup step itself failed, or the failure happened in the route/rc-guard phase before the marker was pushed), manual cleanup is:
git push --delete origin rc/<HEAD_SHA> v<RC> # then redispatch with force: trueRelease-PR-skip subject pattern. The rc-guard job recognizes a squash-merged release commit by matching the commit subject against
^chore: release vX.Y.Z(optionally followed by(#NNNN)for the squash-merge PR-number suffix). Match is case-insensitive —Chore: Release v1.2.3works too. PRs that should suppress the RC build must either use this subject shape, or carry thereleaselabel so the label-based fallback fires. Other release-style subjects (chore(release): v1.2.3,release: v1.2.3) will NOT trigger the skip — please name the release PR exactlychore: release vX.Y.Zto keep the dedup deterministic.Docker-only partial failure: if
publishsucceeds (npm tarball + tags are live) but thedockerjob subsequently fails (e.g. GHCR flakiness), the npm RC is already published and therc/<HEAD_SHA>marker is in place. Recovery without cutting a new RC:# Re-run only the failed docker job from the original workflow run: gh run rerun <run-id> --failedFind the run ID via
gh run list --workflow=publish.yml --branch main.docker.ymlintentionally has noworkflow_dispatchtrigger (images are tag-driven by design), so the gh-run-rerun path is the supported recovery.GitHub Release transient failure (npm publish succeeded, Release step failed): the npm artifact is live but no GitHub Release page exists. Recover by either re-running the failed job (
gh run rerun <run-id> --failed), or creating the Release manually:gh release create v<RC> --prerelease --generate-notes # RC gh release create v<X.Y.Z> --notes-file gitnexus/CHANGELOG.md # stable
The rc workflow never moves latest. To verify after a change, inspect dist-tags:
npm view gitnexus dist-tags