diff --git a/gitnexus/src/cli/ci-setup.ts b/gitnexus/src/cli/ci-setup.ts index fb87e9689..e7d739e85 100644 --- a/gitnexus/src/cli/ci-setup.ts +++ b/gitnexus/src/cli/ci-setup.ts @@ -12,12 +12,21 @@ import fs from 'fs/promises'; import path from 'path'; +import { createRequire } from 'module'; import { detectEnvironment, checkPortAvailable } from './ci-setup/detect.js'; import { resolveOptions } from './ci-setup/prompts.js'; import { generateFiles } from './ci-setup/templates.js'; import type { CiSetupOptions, CiSetupResult, GeneratedFile } from './ci-setup/types.js'; import type { CiSystem, DeployTarget, AuthMode, BranchStrategy } from './ci-setup/types.js'; +// Pin generated automation/config to the wizard's own version (mirrors setup.ts). +// Read here (src/cli/, where ../../package.json resolves to gitnexus/package.json); +// do NOT read it from templates.ts, which is one level deeper. +const moduleRequire = createRequire(import.meta.url); +const pkgJson = moduleRequire('../../package.json') as { version?: unknown }; +const GITNEXUS_VERSION = + typeof pkgJson.version === 'string' && pkgJson.version ? pkgJson.version : 'latest'; + const CI_SYSTEMS: readonly CiSystem[] = ['github-actions', 'azure-devops', 'both']; const DEPLOY_TARGETS: readonly DeployTarget[] = ['docker', 'azure-container-app', 'both']; const AUTH_MODES: readonly AuthMode[] = ['token', 'none']; @@ -95,6 +104,7 @@ export const ciSetupCommand = async (options?: { if (options?.apply !== undefined) partial.apply = options.apply; if (options?.yes !== undefined) partial.yes = options.yes; if (options?.outputDir) partial.outputDir = options.outputDir; + partial.version = GITNEXUS_VERSION; // Default: dry-run when neither --dry-run nor --apply is given if (!partial.dryRun && !partial.apply) { diff --git a/gitnexus/src/cli/ci-setup/prompts.ts b/gitnexus/src/cli/ci-setup/prompts.ts index 6793f0990..b9f911259 100644 --- a/gitnexus/src/cli/ci-setup/prompts.ts +++ b/gitnexus/src/cli/ci-setup/prompts.ts @@ -32,6 +32,7 @@ export async function resolveOptions( apply: partial.apply ?? false, yes: partial.yes ?? false, outputDir: partial.outputDir ?? detect.gitRoot ?? process.cwd(), + version: partial.version ?? 'latest', }; } diff --git a/gitnexus/src/cli/ci-setup/templates.ts b/gitnexus/src/cli/ci-setup/templates.ts index 382f88368..c6a32c1dc 100644 --- a/gitnexus/src/cli/ci-setup/templates.ts +++ b/gitnexus/src/cli/ci-setup/templates.ts @@ -58,7 +58,7 @@ jobs: node-version: '22' - name: Analyze repository - run: npx gitnexus@latest analyze --skills + run: npx gitnexus@${opts.version} analyze --skills - name: Upload index artifact uses: actions/upload-artifact@v4 @@ -113,7 +113,7 @@ steps: versionSpec: '22.x' displayName: 'Set up Node.js 22' - - script: npx gitnexus@latest analyze --skills + - script: npx gitnexus@${opts.version} analyze --skills displayName: 'Analyze repository' - task: PublishPipelineArtifact@1 @@ -389,7 +389,7 @@ Add to \`~/.cursor/mcp.json\` under \`mcpServers\`: \`\`\`json "gitnexus": { "command": "npx", - "args": ["-y", "gitnexus@latest", "mcp"], + "args": ["-y", "gitnexus@${opts.version}", "mcp"], "env": {} } \`\`\` diff --git a/gitnexus/src/cli/ci-setup/types.ts b/gitnexus/src/cli/ci-setup/types.ts index 0b767dd71..b27cd7043 100644 --- a/gitnexus/src/cli/ci-setup/types.ts +++ b/gitnexus/src/cli/ci-setup/types.ts @@ -13,6 +13,8 @@ export interface CiSetupOptions { apply: boolean; yes: boolean; outputDir: string; + /** gitnexus version the generated automation/config should pin to. */ + version: string; } export interface DetectResult { diff --git a/gitnexus/test/unit/ci-setup-templates.test.ts b/gitnexus/test/unit/ci-setup-templates.test.ts index c95ea84de..d4febfe02 100644 --- a/gitnexus/test/unit/ci-setup-templates.test.ts +++ b/gitnexus/test/unit/ci-setup-templates.test.ts @@ -21,6 +21,7 @@ function makeOpts(overrides?: Partial): CiSetupOptions { apply: false, yes: false, outputDir: '/repo', + version: '9.9.9', ...overrides, }; } @@ -85,6 +86,28 @@ describe('GITNEXUS.md / workflow accuracy (U8)', () => { }); }); +describe('version pinning (U9)', () => { + it('pins the GitHub Actions workflow analyze step to the wizard version', () => { + const files = generateFiles(makeOpts({ version: '9.9.9' }), DEFAULT_DETECT); + const wf = files.find((f) => f.relativePath === '.github/workflows/gitnexus-ci.yml'); + expect(wf?.content).toContain('gitnexus@9.9.9 analyze'); + expect(wf?.content).not.toContain('gitnexus@latest'); + }); + + it('pins the Azure pipeline analyze step', () => { + const files = generateFiles(makeOpts({ version: '9.9.9', ci: 'azure-devops' }), DEFAULT_DETECT); + const az = files.find((f) => f.relativePath === 'azure-pipelines-gitnexus.yml'); + expect(az?.content).toContain('gitnexus@9.9.9 analyze'); + expect(az?.content).not.toContain('gitnexus@latest'); + }); + + it('pins the Cursor MCP launch but leaves manual re-index commands at @latest', () => { + const md = gitnexusMd({ version: '9.9.9' }); + expect(md).toContain('"gitnexus@9.9.9"'); // persisted Cursor stdio config — pinned + expect(md).toContain('npx gitnexus@latest analyze'); // human-run re-index — intentionally unpinned + }); +}); + describe('generateFiles', () => { describe('GitHub Actions workflow', () => { it('generates with correct port in healthcheck', () => {