GitNexus/CONTRIBUTING.md
Gergő Magyar b5627f27d8
ci: add fork-safe PR autofix pipeline (#1446)
* ci: add fork-safe PR autofix pipeline

Two-workflow split posts prettier + eslint --fix output as inline
review-comment suggestions on PRs (including fork PRs) without running
fork-controlled ESLint plugins under a privileged token.

- pr-autofix.yml: untrusted, runs lint:fix/format with permissions: {},
  uploads diff artifact. paths-ignore on lockfiles/snapshots/dist to
  avoid reviewdog 406 on >3k-line diffs.
- pr-autofix-publish.yml: trusted workflow_run consumer. Validates every
  metadata.json field with regex allowlists before exporting to
  GITHUB_OUTPUT (closes head_ref newline-injection vector). Concurrency
  keyed on PR number with fork fallback to head-repo+branch. Reviewdog
  pinned to v0.21.0. Sticky comment posts only when patch is non-empty
  (no noise on clean PRs); body carries a fenced gitnexus-autofix JSON
  block under a stable HTML marker for agent parsing. gh API calls go
  through a small retry helper for transient 5xx.

Branch protection should enable merge queue + 'require branches up to
date' to handle PR freshness; chinthakagodawita/autoupdate is dropped
(unmaintained since 2023).

* ci(autofix): close zizmor template-injection findings

Move fork-controlled values (head.ref, head.repo.full_name, head.sha,
pr.number, github.repository) into the step's env: block instead of
interpolating them with `${{ }}` directly into the bash run body. The
job has permissions:{} today so this is defence-in-depth, but a future
scope grant on the untrusted half would otherwise turn a malicious
branch name into shell injection.

Add pr-autofix-publish.yml to the documented dangerous-triggers ignore
list — workflow_run is required to post sticky comments on fork PRs
and the file's structural defences (no fork checkout, allowlist on
metadata.json, base_repo equality check) match the existing
ci-report.yml exemption.

* ci(autofix): close remaining review findings

- Add an actionlint job to workflow-lint.yml. Catches YAML syntax,
  expression typing, shellcheck-inside-run, and deprecated runner
  labels on every .github/** PR — closes the gap that let pr-autofix's
  YAML literal-block bug reach review on this branch.
- pr-autofix-publish.yml emits a `gitnexus/autofix` Check Run on the
  PR head SHA: conclusion `success` for clean, `neutral` (with
  distinct output titles) for suggestions-posted vs.
  skipped-too-large. Stable name lets agents read the outcome via
  `gh pr checks` without parsing the sticky comment.
- Document the autofix signal contract in CONTRIBUTING.md — sticky
  marker, fenced gitnexus-autofix JSON schema, Check Run name. One
  source of truth so the marker / schema fields don't drift across
  the workflow files and consumers.

* ci: fix actionlint/shellcheck findings on PR #1446

Closes the actionlint warnings the new lint job (workflow-lint.yml's
actionlint runner) surfaced once it was wired into CI. Mostly
shellcheck-style cleanups across three workflows.

pr-autofix-publish.yml
  - SC2170: `[ "${{ steps.meta.outputs.changed_lines }}" -gt 3000 ]`
    interpolates a literal string into bash, breaking shellcheck's
    arithmetic-comparison parse. Move `changed_lines` through env: as
    `CHANGED_LINES` and reference as `$CHANGED_LINES` inside bash.

ci-report.yml (Read PR metadata step)
  - SC2002 ×2: `cat file | tr` -> `tr < file`.
  - SC2129: three consecutive `>> "$GITHUB_OUTPUT"` redirects collapsed
    into one `{ ...; } >> "$GITHUB_OUTPUT"` group.

ci-report.yml (Build report step)
  - SC2162 ×2: `read VAR1 VAR2` -> `read -r VAR1 VAR2` so backslashes
    in test-results.json output aren't mangled.
  - SC2034: drop unused `SUITES` aggregate. The per-framework suite
    counts (CLI_SU, WEB_SU) are now read into `_` placeholders since
    the report doesn't surface them anywhere.

release-candidate.yml
  - SC2129 ×2: collapse consecutive `>> "$GITHUB_OUTPUT"` redirects in
    the rc-version computation step and the tag-push step into one
    grouped block each.
2026-05-09 13:35:04 +01:00

11 KiB
Raw Blame History

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

  1. Clone the repository.
  2. CLI / MCP package: cd gitnexus && npm install && npm run build
  3. Web UI (if needed): cd gitnexus-web && npm install
  4. Run tests as described in TESTING.md.

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.yml enforces 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 (gitnexus and/or gitnexus-web).
  • Typecheck passes: npx tsc --noEmit in gitnexus/ and npx tsc -b --noEmit in gitnexus-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_request meta events): ${{ github.workflow }}-${{ github.event.pull_request.number || github.event.issue.number }}
    • workflow_run scope (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 (never workflow_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 a github.event_name-aware expression that falls through to github.run_id for reusable invocations (see ci.yml for the canonical form). Approved literal prefixes: CI- (ci.yml) and docker-build-push- (docker.yml). The check-workflow-concurrency.py validation 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 }} with cancel-in-progress: false (every queue entry is a distinct ref; never cancel).
  • cancel-in-progress policy:

    Event cancel-in-progress Why
    pull_request CI run true New push supersedes old run
    push to main false Every main commit gets validated
    Tag push (v* publish) false Never cancel mid-publish
    push to main for release-candidate false Never cancel mid-RC publish
    workflow_dispatch (release/publish) false Manual runs are intentional
    workflow_run (sticky-comment reports) false Serialize, don't race
    Per-PR bot workflows (@claude, review) false Serialize comments per PR
    PR-meta re-checks (pr-description-check) true Cheap, latest wins
    Single-slot utilities (triage sweep) true Latest dispatch supersedes
  • For workflows that serve multiple events at once (e.g. ci.yml handles pull_request, push, and workflow_call), make cancel-in-progress event-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 the diff as inline review-comment suggestions. 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/v1 with fields state (suggestions-posted | skipped-too-large), pr_number, head_sha, changed_lines, run_id. Parseable signal — preferred over regexing prose.
Check Run Stable name gitnexus/autofix on the PR head SHA. Conclusion: success (clean) or neutral (suggestions-posted / skipped-too-large). The output title disambiguates the two neutral cases. 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.

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

Two publish workflows ship gitnexus to npm:

  • Stable (.github/workflows/publish.yml) — triggered by pushing any v* tag. Publishes to the latest dist-tag with a changelog-backed GitHub release. Maintainers are expected to tag from main as a convention; the workflow itself does not enforce branch reachability.

  • Release Candidate (.github/workflows/release-candidate.yml) — runs on every push to main (typically a merged PR) plus manual dispatch. Docs-only changes are skipped via paths-ignore. Publishes to the rc dist-tag with version X.Y.Z-rc.N and a GitHub prerelease, where:

    • X.Y.Z is selected automatically. On push (and on dispatch with bump: auto, the default) the workflow continues the active rc cycle: if the registry already has X.Y.Z-rc.* versions with X.Y.Z > current latest, it reuses the highest such base; otherwise it patch-bumps from latest. Dispatching with bump: patch|minor|major resets the cycle from latest.
    • N is auto-incremented against existing X.Y.Z-rc.* entries on the registry. First rc for a given base is rc.1.
    • After the npm publish succeeds, the workflow calls docker.yml as a reusable workflow to build and push the corresponding RC Docker images (e.g. ghcr.io/abhigyanpatwari/gitnexus:1.7.0-rc.1, mirrored to docker.io/akonlabs/gitnexus:1.7.0-rc.1). The images are signed with Cosign; the OIDC identity is docker.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 a v<RC> release tag atomically, before calling npm publish. The guard refuses to re-run once the marker exists, so a post-publish failure will not mint a duplicate rc for the same commit. The v<RC> tag points at a detached release commit whose package.json matches the npm tarball exactly (traceable releases). Recovery after a partial failure:

    git push --delete origin rc/<HEAD_SHA> v<RC>
    # then redispatch the workflow with force: true
    

    Docker-only partial failure: if publish succeeds (npm tarball + tags are live) but the docker job subsequently fails (e.g. GHCR flakiness), the npm RC is already published and the rc/<HEAD_SHA> marker is in place. Re-running release-candidate.yml with force: true will abort at the "Version already exists on npm" guard. To recover without cutting a new RC:

    # 1. Manually trigger only the docker workflow, passing the existing RC tag:
    gh workflow run docker.yml --ref main -f tag=v<RC_VERSION>
    # (requires a workflow_dispatch trigger on docker.yml — see note below)
    

    Because docker.yml intentionally has no workflow_dispatch (images are tag-driven by design), the practical recovery options are:

    • Wait for the next commit on main, which will cut a new RC that includes the Docker build.
    • Manually run docker build + docker push locally and sign with Cosign against the same digest.
    • Delete rc/<HEAD_SHA> and v<RC> tags, then redispatch with force: true to re-run the full RC pipeline (cuts a new RC number).

The rc workflow never moves latest. To verify after a change, inspect dist-tags:

npm view gitnexus dist-tags