-
Notifications
You must be signed in to change notification settings - Fork 0
ci: dogfood git-mind suggest action with Claude CLI #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||
| name: git-mind Suggest | ||||||||||||||
|
|
||||||||||||||
| on: | ||||||||||||||
| pull_request: | ||||||||||||||
| types: [opened, synchronize, reopened, ready_for_review] | ||||||||||||||
| branches: [main] | ||||||||||||||
|
|
||||||||||||||
| concurrency: | ||||||||||||||
| group: gitmind-suggest-${{ github.event.pull_request.number }} | ||||||||||||||
| cancel-in-progress: true | ||||||||||||||
|
|
||||||||||||||
| permissions: | ||||||||||||||
| contents: read | ||||||||||||||
| pull-requests: write | ||||||||||||||
|
|
||||||||||||||
| jobs: | ||||||||||||||
| suggest: | ||||||||||||||
| if: > | ||||||||||||||
| !github.event.pull_request.draft && | ||||||||||||||
| github.actor != 'dependabot[bot]' | ||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||
| continue-on-error: true | ||||||||||||||
|
|
||||||||||||||
| steps: | ||||||||||||||
| - name: Check for API key | ||||||||||||||
| id: check-key | ||||||||||||||
| env: | ||||||||||||||
| HAS_KEY: ${{ secrets.ANTHROPIC_API_KEY != '' }} | ||||||||||||||
| run: | | ||||||||||||||
| if [ "$HAS_KEY" != "true" ]; then | ||||||||||||||
| echo "::notice::ANTHROPIC_API_KEY not configured — skipping suggest" | ||||||||||||||
| echo "skip=true" >> "$GITHUB_OUTPUT" | ||||||||||||||
| else | ||||||||||||||
| echo "skip=false" >> "$GITHUB_OUTPUT" | ||||||||||||||
| fi | ||||||||||||||
|
|
||||||||||||||
| - name: Checkout | ||||||||||||||
| if: steps.check-key.outputs.skip != 'true' | ||||||||||||||
| uses: actions/checkout@v4 | ||||||||||||||
| with: | ||||||||||||||
| fetch-depth: 0 | ||||||||||||||
|
|
||||||||||||||
| - name: Setup Node.js | ||||||||||||||
| if: steps.check-key.outputs.skip != 'true' | ||||||||||||||
| uses: actions/setup-node@v4 | ||||||||||||||
| with: | ||||||||||||||
| node-version: '22' | ||||||||||||||
| cache: 'npm' | ||||||||||||||
|
|
||||||||||||||
| - name: Install dependencies | ||||||||||||||
| if: steps.check-key.outputs.skip != 'true' | ||||||||||||||
| run: npm ci | ||||||||||||||
|
|
||||||||||||||
| - name: Install Claude CLI | ||||||||||||||
| if: steps.check-key.outputs.skip != 'true' | ||||||||||||||
| run: npm install -g @anthropic-ai/claude-code | ||||||||||||||
|
Comment on lines
+54
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The latest published version of Source: npm package “Versions” tab shows Version 1.0.108 and “Last publish 5 hours ago.” [1] References Pin Line 56: Fix- run: npm install -g `@anthropic-ai/claude-code`
+ run: npm install -g `@anthropic-ai/claude-code`@1.0.108📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents🧩 Analysis chain🌐 Web query:
💡 Result: Anthropic’s official Claude Code (Claude CLI) docs state that “NPM installation is deprecated” and recommend using the native installation method instead; to migrate an existing npm install to native, run Recent Claude CLI release notes also mention an added deprecation notification for npm installations, pointing users to run Sources: 🌐 Web query:
💡 Result: The latest published version of the Source: npm package “Versions” tab. [1] 🌐 Web query:
💡 Result:
🌐 Web query:
💡 Result: Anthropic’s official Claude Code docs do not state that the CLI binary installs to Official install methods & where the
Citations:
"Installation via npm is deprecated. Use one of the recommended methods below." Anthropic's official documentation and release notes are explicit about this. The package itself now ships with a deprecation notification for npm installations. Beyond deprecation, the install is unpinned — every run silently picks up whatever the registry serves, with no auditability or rollback path if a breaking release lands. 🚑 Proposed fix — switch to the native installer at the current version - name: Install Claude CLI
if: steps.check-key.outputs.skip != 'true'
- run: npm install -g `@anthropic-ai/claude-code`
+ run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.108(The proposed fix in the original comment uses an incorrect version number 🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| - name: Run git-mind suggest | ||||||||||||||
| if: steps.check-key.outputs.skip != 'true' | ||||||||||||||
| uses: ./ | ||||||||||||||
| with: | ||||||||||||||
| agent: 'claude -p --output-format json' | ||||||||||||||
| env: | ||||||||||||||
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | ||||||||||||||
|
Comment on lines
+60
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This step executes the local action from the PR checkout ( Useful? React with 👍 / 👎. |
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -246,44 +246,60 @@ export async function remove(cwd, source, target, opts = {}) { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Install a post-commit Git hook that processes directives. | ||
| * @param {string} cwd | ||
| */ | ||
| export async function installHooks(cwd) { | ||
| const hookPath = join(cwd, '.git', 'hooks', 'post-commit'); | ||
| const PRE_PUSH_SCRIPT = `#!/bin/sh | ||
| # git-mind pre-push hook | ||
| # Processes commit directives and runs suggest on commits about to be pushed | ||
|
|
||
| const hookScript = `#!/bin/sh | ||
| # git-mind post-commit hook | ||
| # Parses commit directives and creates edges automatically | ||
| command -v npx >/dev/null 2>&1 || exit 0 | ||
|
|
||
| SHA=$(git rev-parse HEAD) | ||
| MSG=$(git log -1 --format=%B "$SHA") | ||
| ZERO="0000000000000000000000000000000000000000" | ||
|
|
||
| # Only run if git-mind is available | ||
| command -v npx >/dev/null 2>&1 || exit 0 | ||
| while read local_ref local_sha remote_ref remote_sha; do | ||
| [ "$local_sha" = "$ZERO" ] && continue # deleting ref | ||
| if [ "$remote_sha" = "$ZERO" ]; then | ||
| RANGE="HEAD~10..HEAD" # new branch — use last 10 | ||
| else | ||
| RANGE="\${remote_sha}..\${local_sha}" | ||
| fi | ||
|
Comment on lines
+259
to
+263
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When
🐛 Proposed fix- RANGE="HEAD~10..HEAD" # new branch — use last 10
+ RANGE="${local_sha}~10..${local_sha}" # new branch — use last 10🤖 Prompt for AI Agents |
||
|
|
||
| # Process commit directives for each commit in the range | ||
| for SHA in $(git rev-list "$RANGE" 2>/dev/null); do | ||
| npx git-mind process-commit "$SHA" 2>/dev/null || true | ||
| done | ||
|
|
||
| npx git-mind process-commit "$SHA" 2>/dev/null || true | ||
| # Run suggest if an agent is configured | ||
| if [ -n "$GITMIND_AGENT" ]; then | ||
| npx git-mind suggest --context "$RANGE" 2>/dev/null || true | ||
| fi | ||
| done | ||
|
|
||
| exit 0 | ||
| `; | ||
|
|
||
| /** | ||
| * Install pre-push Git hook (directives + suggest). | ||
| * @param {string} cwd | ||
| */ | ||
| export async function installHooks(cwd) { | ||
| const hookPath = join(cwd, '.git', 'hooks', 'pre-push'); | ||
|
|
||
| try { | ||
| // Check if a hook already exists | ||
| let exists = false; | ||
| try { | ||
| await access(hookPath, constants.F_OK); | ||
| exists = true; | ||
| } catch { /* doesn't exist */ } | ||
|
|
||
| if (exists) { | ||
| console.error(error(`Post-commit hook already exists at ${hookPath}`)); | ||
| console.error(error(`pre-push hook already exists at ${hookPath}`)); | ||
| console.error(info('Remove it manually or append git-mind to the existing hook')); | ||
| process.exitCode = 1; | ||
| return; | ||
| } | ||
|
|
||
| await writeFile(hookPath, hookScript); | ||
| await writeFile(hookPath, PRE_PUSH_SCRIPT); | ||
| await chmod(hookPath, 0o755); | ||
| console.log(success('Installed post-commit hook')); | ||
| console.log(success('Installed pre-push hook')); | ||
| } catch (err) { | ||
| console.error(error(`Failed to install hook: ${err.message}`)); | ||
| process.exitCode = 1; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,12 @@ | ||
| import { describe, it, expect, beforeEach, afterEach } from 'vitest'; | ||
| import { mkdtemp, rm } from 'node:fs/promises'; | ||
| import { mkdtemp, rm, readFile, stat, writeFile, mkdir } from 'node:fs/promises'; | ||
| import { join } from 'node:path'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { execSync } from 'node:child_process'; | ||
| import { initGraph } from '../src/graph.js'; | ||
| import { queryEdges } from '../src/edges.js'; | ||
| import { parseDirectives, processCommit } from '../src/hooks.js'; | ||
| import { installHooks } from '../src/cli/commands.js'; | ||
|
|
||
| describe('hooks', () => { | ||
| describe('parseDirectives', () => { | ||
|
|
@@ -97,4 +98,69 @@ RELATES-TO: module:session`; | |
| expect(edges.length).toBe(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe('installHooks', () => { | ||
| let tempDir; | ||
|
|
||
| beforeEach(async () => { | ||
| tempDir = await mkdtemp(join(tmpdir(), 'gitmind-test-')); | ||
| execSync('git init', { cwd: tempDir, stdio: 'ignore' }); | ||
| await mkdir(join(tempDir, '.git', 'hooks'), { recursive: true }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Redundant
🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await rm(tempDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| it('creates pre-push hook', async () => { | ||
| await installHooks(tempDir); | ||
|
|
||
| const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8'); | ||
| expect(prePush).toContain('git-mind suggest'); | ||
| expect(prePush).toContain('process-commit'); | ||
| }); | ||
|
|
||
| it('makes hook executable', async () => { | ||
| await installHooks(tempDir); | ||
|
|
||
| const prePushStat = await stat(join(tempDir, '.git', 'hooks', 'pre-push')); | ||
| expect(prePushStat.mode & 0o111).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('does not overwrite existing hook', async () => { | ||
| const existingContent = '#!/bin/sh\necho "existing hook"'; | ||
| await writeFile(join(tempDir, '.git', 'hooks', 'pre-push'), existingContent); | ||
|
|
||
| await installHooks(tempDir); | ||
|
|
||
| const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8'); | ||
| expect(prePush).toBe(existingContent); | ||
| }); | ||
|
Comment on lines
+130
to
+138
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Fix: capture and restore 🔥 Proposed fix it('does not overwrite existing hook', async () => {
const existingContent = '#!/bin/sh\necho "existing hook"';
await writeFile(join(tempDir, '.git', 'hooks', 'pre-push'), existingContent);
+ const prevExitCode = process.exitCode;
await installHooks(tempDir);
+ expect(process.exitCode).toBe(1);
+ process.exitCode = prevExitCode;
const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8');
expect(prePush).toBe(existingContent);
});🤖 Prompt for AI Agents |
||
|
|
||
| it('hook runs suggest only when GITMIND_AGENT is set', async () => { | ||
| await installHooks(tempDir); | ||
|
|
||
| const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8'); | ||
| expect(prePush).toContain('GITMIND_AGENT'); | ||
| expect(prePush).toMatch(/if \[ -n "\$GITMIND_AGENT" \]/); | ||
| }); | ||
|
|
||
| it('hook always exits 0', async () => { | ||
| await installHooks(tempDir); | ||
|
|
||
| const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8'); | ||
| expect(prePush).toContain('exit 0'); | ||
| expect(prePush).toContain('|| true'); | ||
| }); | ||
|
|
||
| it('hook processes directives unconditionally', async () => { | ||
| await installHooks(tempDir); | ||
|
|
||
| const prePush = await readFile(join(tempDir, '.git', 'hooks', 'pre-push'), 'utf-8'); | ||
| // process-commit runs outside the GITMIND_AGENT guard | ||
| const agentGuardIndex = prePush.indexOf('if [ -n "$GITMIND_AGENT" ]'); | ||
| const processCommitIndex = prePush.indexOf('process-commit'); | ||
| expect(processCommitIndex).toBeLessThan(agentGuardIndex); | ||
| }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Bot guard only excludes
dependabot[bot]— other automation actors will consume API quota.renovate[bot],github-actions[bot], and custom bots are not excluded. Unless intentional, tighten the guard.🤖 Proposed fix
if: > !github.event.pull_request.draft && - github.actor != 'dependabot[bot]' + github.actor != 'dependabot[bot]' && + !endsWith(github.actor, '[bot]')📝 Committable suggestion
🤖 Prompt for AI Agents
🧹 Nitpick | 🔵 Trivial
Bot guard only excludes
dependabot[bot]— other automation actors will consume API credits.renovate[bot],github-actions[bot], and any custom bot actors are not excluded. Unless intentional, add a broader guard.🤖 Proposed fix
if: > !github.event.pull_request.draft && - github.actor != 'dependabot[bot]' + github.actor != 'dependabot[bot]' && + github.actor != 'renovate[bot]' && + !endsWith(github.actor, '[bot]')🤖 Prompt for AI Agents