fix(ci-setup): pin generated workflows and Cursor MCP to the wizard version

Generated automation used `npx gitnexus@latest`, which silently pulls
whatever was last published — a non-reproducible single point of failure
for every consumer's CI. Read the wizard's own version from
gitnexus/package.json (the setup.ts MCP_PINNED_REF pattern; read in
ci-setup.ts where ../../package.json resolves correctly, threaded via
CiSetupOptions to avoid a generateFiles signature churn) and pin the
GitHub Actions + Azure pipeline analyze steps and the persisted Cursor
stdio MCP launch to `gitnexus@<version>`. The human-facing manual
re-index commands in GITNEXUS.md intentionally stay `@latest`, matching
setup.ts's "READMEs may use latest, persisted configs are pinned" rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 09:08:23 +00:00
parent 317bb3b4ea
commit b05e71c644
5 changed files with 39 additions and 3 deletions

View file

@ -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) {

View file

@ -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',
};
}

View file

@ -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": {}
}
\`\`\`

View file

@ -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 {

View file

@ -21,6 +21,7 @@ function makeOpts(overrides?: Partial<CiSetupOptions>): 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', () => {