diff --git a/CHANGELOG.md b/CHANGELOG.md index e0a8534..505a092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on Keep a Changelog and follows Semantic Versioning. ### Fixed +- **The GitHub workflow that `charter setup --ci github` and `charter bootstrap --ci github` generate no longer runs a bare `npx charter`** (`#380`). When `@stackbilt/cli` was not installed, `npx charter` fetched the unrelated `charter` registry package (no executable, so the step failed with `could not determine executable to run`). Each step now runs the CLI the install step put in `node_modules`: `pnpm exec charter …` for pnpm, and `npm exec --no --package=@stackbilt/cli -- charter …` for npm. Neither form installs anything; without a local `@stackbilt/cli` the step fails. `setup` and `bootstrap` add it to `devDependencies`, except under `--no-dependency-sync` or in a repo with no root `package.json`. The npm form still contains `charter validate --ci`, so doctor's gate-enforcement check recognizes it. The template has no Yarn form (it supports npm and pnpm only), so a Yarn repo gets the npm form. The previous template (003ec8d, with `npx charter`) has the same permissions and triggers, so `setup`/`bootstrap` refresh it without a warning, as before. Its `KNOWN_WORKFLOWS` entry stays `safe` and records what the refresh changes. - **`charter adf compile --target claude --write --mode compiled` no longer carries the template's empty `## Environment` placeholder into the retained block** (`#364`). A `CLAUDE.md` written by any bootstrap since 0.3.3 (the 1.9.3 upgrade path included) holds `## Environment` with only the `` comment. When you had added a second `## Environment` of your own, converting the pointer to compiled output carried both, comment included, and reported `kept ## Environment, ## Environment`. Each retained section is now compared with the pointer templates on its own (ignoring CRLF, whitespace at line ends and blank lines around the body), so the untouched placeholder is dropped as template text and only your section is carried. A placeholder you wrote under is yours and is carried as before. A retained block that an earlier compile already wrote is copied verbatim as always: if yours holds the placeholder, delete it from the block by hand. - **`charter adf migrate` and `charter bootstrap` wrote the generic "This project uses [ADF]…" pointer instead of the named template when run from the CLI** (`#355`). This hit `GEMINI.md`, `AGENTS.md`, `.cursorrules` and `copilot-instructions.md` in the default mode, and `CLAUDE.md` too under `--replace`. `adf-migrate` built its template table when the module loaded, and the CLI loads it while `adf.ts` is still initialising (`index` → `doctor` → `adf` → `adf-migrate`), so every entry was `undefined`. The template is now looked up when the pointer is written. Unit tests that imported `adf-migrate` directly never saw the cycle. - **`charter adf populate`, and so every `charter bootstrap`, re-added stack constraints to a `CONSTRAINTS` section you had edited** (`#371`; 2.1.0 listed this as still open under `#354`). Deleting a bullet from `core.adf` and re-running bootstrap brought a stack rule back (for example the Cloudflare `No Node.js-specific APIs in Worker handlers` rule), because populate appended any detected stack constraint it did not find. Populate now remembers what it has added or offered in a new file, `.ai/.populate.json` (`{ "version": 1, "constraints": { "core.adf": { "added": [...], "offered": [...] } } }`, sorted, written only when it changes). Commit it with the rest of `.ai/`; bootstrap lists it under what it created, and its `git add .ai` next step already includes it. The rules: diff --git a/docs/ci-integration.md b/docs/ci-integration.md index 3ac83dd..461f83b 100644 --- a/docs/ci-integration.md +++ b/docs/ci-integration.md @@ -16,7 +16,7 @@ Re-running `charter setup --ci github` or `charter bootstrap --ci github` never - **Missing:** the workflow is created. - **Identical to the current template:** left alone. -- **The current npm or pnpm template:** refreshed. A switch between them is named in the output (`package manager npm → pnpm; adds pnpm/action-setup@v4`), since the pnpm form runs the third-party `pnpm/action-setup` action. Copies that differ only in a BOM, CRLF line endings, trailing whitespace or trailing blank lines count as the template. +- **The current npm or pnpm template, or the one before it:** refreshed. The previous template (score-badge steps, run with `npx charter`) has the same permissions and triggers, so it is refreshed without a warning ([#380](https://github.com/Stackbilt-dev/charter/issues/380)). A switch between the npm and pnpm forms is named in the output (`package manager npm → pnpm; adds pnpm/action-setup@v4`), since the pnpm form runs the third-party `pnpm/action-setup` action. Copies that differ only in a BOM, CRLF line endings, trailing whitespace or trailing blank lines count as the template. - **An older Charter template:** kept, with a warning. Every template before the score-badge steps ran on `pull_request` only with `permissions: contents: read`, and refreshing one changes `contents: read → write`, adds the `push` and `workflow_dispatch` triggers, and adds a step that commits `.charter/badge.json` and pushes to the repo. That gives the workflow's `GITHUB_TOKEN` write access, and on a protected default branch the push step fails unless the token may push. Review it, then run the command again with `--force` to upgrade. The one template that pinned `@stackbilt/cli@x.y.z` is one of these, so a pinned workflow is kept whatever version it pins. - **Anything else (edited by hand):** kept byte for byte, with a warning stating what the current template grants (`contents: write` and `pull-requests: write`; runs on `push`, `pull_request` and `workflow_dispatch`; commits and pushes to the repo). Charter does not try to describe what your edit changed. @@ -34,6 +34,8 @@ On every push and pull request, the CI workflow runs: If any step exits `1`, the check fails and the merge is blocked. +Each step runs the `@stackbilt/cli` that the install step put in `node_modules`, through the repo's package manager: `pnpm exec charter …` for pnpm, and `npm exec --no --package=@stackbilt/cli -- charter …` for npm. Neither installs anything. A bare `npx charter` would fetch the unrelated `charter` package from the registry whenever the CLI is not installed. `charter setup` and `charter bootstrap` add `@stackbilt/cli` to `devDependencies` (not under `--no-dependency-sync`, nor in a repo with no root `package.json`); without it the steps fail. The template has no Yarn form; a Yarn repo gets the npm one. + ## Score Badge Refresh The generated GitHub workflow refreshes `.charter/badge.json` on pushes to `main` or `master`, then commits the file only when the Shields payload changed. Shields renders from the committed JSON at the raw GitHub URL; the workflow is what keeps that JSON current after Charter, ADF, or governance changes land. diff --git a/packages/cli/src/__tests__/fixtures/charter-governance-history.json b/packages/cli/src/__tests__/fixtures/charter-governance-history.json index 34297e7..6c67b97 100644 --- a/packages/cli/src/__tests__/fixtures/charter-governance-history.json +++ b/packages/cli/src/__tests__/fixtures/charter-governance-history.json @@ -33,5 +33,17 @@ "commit": "0fdea60", "packageManager": "pnpm", "text": "name: Governance Check\n\non:\n pull_request:\n branches: [main, master]\n\npermissions:\n contents: read\n pull-requests: write\n\njobs:\n governance:\n name: Charter\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - uses: pnpm/action-setup@v4\n with:\n version: 9\n\n - uses: actions/setup-node@v4\n with:\n node-version: '20'\n cache: 'pnpm'\n\n - name: Install dependencies\n run: pnpm install --frozen-lockfile\n\n - name: Validate Commits\n run: npx charter validate --ci --format text\n\n - name: Drift Scan\n run: npx charter drift --ci --format text\n\n - name: ADF Wiring & Pointer Integrity\n run: npx charter doctor --adf-only --ci --format text\n if: hashFiles('.ai/manifest.adf') != ''\n\n - name: ADF Evidence\n run: npx charter adf evidence --auto-measure --ci --format text\n if: hashFiles('.ai/manifest.adf') != ''\n\n - name: Audit Report\n run: npx charter audit --format json > /tmp/audit.json\n if: always()\n" + }, + { + "commit": "003ec8d", + "packageManager": "npm", + "safe": true, + "text": "name: Governance Check\n\non:\n push:\n branches: [main, master]\n pull_request:\n branches: [main, master]\n workflow_dispatch:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n governance:\n name: Charter\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - uses: actions/setup-node@v4\n with:\n node-version: '20'\n\n - name: Install dependencies\n run: |\n if [ -f package-lock.json ]; then\n npm ci\n else\n npm install\n fi\n\n - name: Validate Commits\n run: npx charter validate --ci --format text\n\n - name: Drift Scan\n run: npx charter drift --ci --format text\n\n - name: ADF Wiring & Pointer Integrity\n run: npx charter doctor --adf-only --ci --format text\n if: hashFiles('.ai/manifest.adf') != ''\n\n - name: ADF Evidence\n run: npx charter adf evidence --auto-measure --ci --format text\n if: hashFiles('.ai/manifest.adf') != ''\n\n - name: Audit Report\n run: npx charter audit --format json > /tmp/audit.json\n if: always()\n\n - name: Refresh Score Badge\n run: npx charter score --badge --write\n if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'\n\n - name: Commit Score Badge\n if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'\n run: |\n if [ -n \"$(git status --porcelain -- .charter/badge.json)\" ]; then\n git config user.name \"github-actions[bot]\"\n git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n git add .charter/badge.json\n git commit -m \"chore(charter): refresh score badge [skip ci]\" -m \"Governed-By: charter-score-badge\"\n git push\n fi\n" + }, + { + "commit": "003ec8d", + "packageManager": "pnpm", + "safe": true, + "text": "name: Governance Check\n\non:\n push:\n branches: [main, master]\n pull_request:\n branches: [main, master]\n workflow_dispatch:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n governance:\n name: Charter\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - uses: pnpm/action-setup@v4\n with:\n version: 9\n\n - uses: actions/setup-node@v4\n with:\n node-version: '20'\n cache: 'pnpm'\n\n - name: Install dependencies\n run: pnpm install --frozen-lockfile\n\n - name: Validate Commits\n run: npx charter validate --ci --format text\n\n - name: Drift Scan\n run: npx charter drift --ci --format text\n\n - name: ADF Wiring & Pointer Integrity\n run: npx charter doctor --adf-only --ci --format text\n if: hashFiles('.ai/manifest.adf') != ''\n\n - name: ADF Evidence\n run: npx charter adf evidence --auto-measure --ci --format text\n if: hashFiles('.ai/manifest.adf') != ''\n\n - name: Audit Report\n run: npx charter audit --format json > /tmp/audit.json\n if: always()\n\n - name: Refresh Score Badge\n run: npx charter score --badge --write\n if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'\n\n - name: Commit Score Badge\n if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'\n run: |\n if [ -n \"$(git status --porcelain -- .charter/badge.json)\" ]; then\n git config user.name \"github-actions[bot]\"\n git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"\n git add .charter/badge.json\n git commit -m \"chore(charter): refresh score badge [skip ci]\" -m \"Governed-By: charter-score-badge\"\n git push\n fi\n" } ] diff --git a/packages/cli/src/__tests__/setup.test.ts b/packages/cli/src/__tests__/setup.test.ts index b0a6e7b..9e27771 100644 --- a/packages/cli/src/__tests__/setup.test.ts +++ b/packages/cli/src/__tests__/setup.test.ts @@ -2,7 +2,8 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; -import { detectStack, getGithubWorkflow, loadPackageContexts, syncPackageManifest } from '../commands/setup'; +import { detectStack, getGithubWorkflow, loadPackageContexts, syncPackageManifest, workflowCharterCommand } from '../commands/setup'; +import { checkGateEnforcement } from '../commands/doctor-gate-enforcement'; import { StackPresetSchema, VALID_PRESETS } from '../commands/setup.contract'; const originalCwd = process.cwd(); @@ -117,9 +118,33 @@ describe('syncPackageManifest', () => { it('github workflow template enforces ADF routing checks', () => { const workflow = getGithubWorkflow('pnpm'); expect(workflow).toContain('ADF Wiring & Pointer Integrity'); - expect(workflow).toContain('npx charter doctor --adf-only --ci --format text'); + expect(workflow).toContain('pnpm exec charter doctor --adf-only --ci --format text'); expect(workflow).toContain('ADF Evidence'); - expect(workflow).toContain('npx charter adf evidence --auto-measure --ci --format text'); + expect(workflow).toContain('pnpm exec charter adf evidence --auto-measure --ci --format text'); + }); + + it('github workflow runs Charter through the package manager, never a bare npx charter (#380)', () => { + expect(workflowCharterCommand('pnpm')).toBe('pnpm exec charter'); + expect(workflowCharterCommand('npm')).toBe('npm exec --no --package=@stackbilt/cli -- charter'); + for (const pm of ['npm', 'pnpm'] as const) { + const workflow = getGithubWorkflow(pm); + expect(workflow).not.toContain('npx'); + const runs = workflow.split('\n').filter(l => /^\s+run: /.test(l) && /\bcharter\b/.test(l)).map(l => l.trim()); + expect(runs).toHaveLength(6); + for (const run of runs) expect(run.startsWith(`run: ${workflowCharterCommand(pm)} `)).toBe(true); + } + }); + + it('doctor recognizes the generated workflow as the CI validate gate for npm and pnpm (#380)', () => { + for (const pm of ['npm', 'pnpm'] as const) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'charter-setup-gate-')); + tempDirs.push(dir); + fs.mkdirSync(path.join(dir, '.github', 'workflows'), { recursive: true }); + fs.writeFileSync(path.join(dir, '.github', 'workflows', 'charter-governance.yml'), getGithubWorkflow(pm)); + process.chdir(dir); + expect(checkGateEnforcement()).toMatchObject({ status: 'PASS' }); + process.chdir(originalCwd); + } }); it('github workflow template refreshes the score badge on push', () => { @@ -127,7 +152,7 @@ describe('syncPackageManifest', () => { expect(workflow).toContain('push:'); expect(workflow).toContain('contents: write'); expect(workflow).toContain('Refresh Score Badge'); - expect(workflow).toContain('npx charter score --badge --write'); + expect(workflow).toContain('pnpm exec charter score --badge --write'); expect(workflow).toContain('Commit Score Badge'); expect(workflow).toContain('git status --porcelain -- .charter/badge.json'); expect(workflow).toContain('Governed-By: charter-score-badge'); diff --git a/packages/cli/src/__tests__/workflow-mcp-backup.test.ts b/packages/cli/src/__tests__/workflow-mcp-backup.test.ts index 2eb5eb6..3e88a55 100644 --- a/packages/cli/src/__tests__/workflow-mcp-backup.test.ts +++ b/packages/cli/src/__tests__/workflow-mcp-backup.test.ts @@ -16,7 +16,7 @@ vi.mock('../git-helpers', async (importOriginal) => { }); import { bootstrapCommand } from '../commands/bootstrap'; -import { getGithubWorkflow, setupCommand } from '../commands/setup'; +import { getGithubWorkflow, setupCommand, workflowCharterCommand } from '../commands/setup'; import { applyGovernanceWorkflow, KNOWN_WORKFLOWS, normalizeWorkflow, TEMPLATE_PERMISSIONS, workflowHash } from '../commands/github-workflow'; const TEXT: CLIOptions = { configPath: '.charter', format: 'text', ciMode: false, yes: false }; @@ -161,10 +161,12 @@ describe('#374 .mcp.json: CRLF', () => { }); // Every historical template (not the current ones), as setup.ts generated it. -const HISTORY: Array<{ commit: string; packageManager: 'npm' | 'pnpm'; text: string }> = JSON.parse( +// `safe: true` marks one with the current permissions and triggers (#380). +const HISTORY: Array<{ commit: string; packageManager: 'npm' | 'pnpm'; safe?: boolean; text: string }> = JSON.parse( fs.readFileSync(path.join(__dirname, 'fixtures', 'charter-governance-history.json'), 'utf-8'), ); const PRE_BADGE = 'contents: read → write; adds push and workflow_dispatch triggers; adds a step that commits and pushes to the repo'; +const PM_CLI = 'runs Charter as pnpm exec charter (pnpm) or npm exec --no --package=@stackbilt/cli -- charter (npm) instead of npx charter'; const TEMPLATE_LINE = 'the current Charter template grants contents: write and pull-requests: write, runs on push, pull_request and workflow_dispatch, and has a step that commits and pushes to the repo'; // Hand edits that a YAML diff could misread; none may be described as a change. @@ -191,16 +193,29 @@ describe('#374 known workflow versions', () => { for (const k of KNOWN_WORKFLOWS) { expect(k.hash).toMatch(/^[0-9a-f]{64}$/); expect(typeof k.safe).toBe('boolean'); - expect(k.origin).toMatch(/^[0-9a-f]{7}[ ,]/); + expect(k.origin).toMatch(/^(?:[0-9a-f]{7}|#\d+)[ ,]/); if (!k.safe) expect(k.refreshChanges.trim().length).toBeGreaterThan(0); } }); - it('the history fixture and the known list match one to one, and every historical template is unsafe', () => { + it('the history fixture and the known list match one to one, and every pre-badge template is unsafe', () => { const fixtureHashes = HISTORY.map(h => workflowHash(h.text)); const current = [workflowHash(getGithubWorkflow('npm')), workflowHash(getGithubWorkflow('pnpm'))]; expect(new Set([...fixtureHashes, ...current])).toEqual(new Set(KNOWN_WORKFLOWS.map(k => k.hash))); - for (const h of HISTORY) { + expect(fixtureHashes.length + current.length).toBe(KNOWN_WORKFLOWS.length); + for (const h of HISTORY.filter(e => e.safe)) { + // A safe historical template grants exactly what the current one does (#380). + const k = KNOWN_WORKFLOWS.find(e => e.hash === workflowHash(h.text)); + expect(k).toMatchObject({ safe: true, packageManager: h.packageManager, refreshChanges: PM_CLI }); + expect(k?.origin.startsWith(h.commit)).toBe(true); + const head = (t: string): string => t.slice(0, t.indexOf('\njobs:')); + expect(head(h.text)).toBe(head(getGithubWorkflow(h.packageManager))); + expect(h.text).toContain('git push'); + expect(h.text).toContain('npx charter validate'); + // ...and the refresh changes only how Charter is run. + expect(h.text.replace(/npx charter /g, '{{CLI}} ')).toBe(getGithubWorkflow(h.packageManager).split(`${workflowCharterCommand(h.packageManager)} `).join('{{CLI}} ')); + } + for (const h of HISTORY.filter(e => !e.safe)) { const k = KNOWN_WORKFLOWS.find(e => e.hash === workflowHash(h.text)); expect(k).toMatchObject({ safe: false, packageManager: h.packageManager, refreshChanges: PRE_BADGE }); expect(k?.origin.startsWith(h.commit)).toBe(true); @@ -302,7 +317,19 @@ describe('#374 bootstrap --ci github: the workflow', () => { expect(fs.existsSync('.charter/.backup')).toBe(false); }); - for (const h of HISTORY) { + for (const h of HISTORY.filter(e => e.safe)) { + it(`refreshes the ${h.commit} ${h.packageManager} template under --yes, silently (same permissions, #380)`, async () => { + write(WF, h.text); + if (h.packageManager === 'pnpm') write('pnpm-lock.yaml', ''); + expect(await bootstrapCommand(YES, ['--yes', '--ci', 'github', ...BOOT])).toBe(0); + expect(read(WF)).toBe(getGithubWorkflow(h.packageManager)); + expect(out).toContain(` Updated ${P(WF)}`); + expect(out.some(l => l.includes('Warning: Kept'))).toBe(false); + expect(fs.existsSync('.charter/.backup')).toBe(false); + }); + } + + for (const h of HISTORY.filter(e => !e.safe)) { it(`keeps the ${h.commit} ${h.packageManager} template under --yes with the static message`, async () => { write(WF, h.text); if (h.packageManager === 'pnpm') write('pnpm-lock.yaml', ''); @@ -470,7 +497,18 @@ describe('#374 setup --ci github: the workflow', () => { expect(warning).toContain('To replace only the workflow, delete it and re-run'); }); - for (const h of HISTORY) { + for (const h of HISTORY.filter(e => e.safe)) { + it(`setup refreshes the ${h.commit} ${h.packageManager} template without --force (#380)`, async () => { + write(WF, h.text); + if (h.packageManager === 'pnpm') write('pnpm-lock.yaml', ''); + expect(await setupCommand(YES, SETUP)).toBe(0); + expect(read(WF)).toBe(getGithubWorkflow(h.packageManager)); + expect(out.some(l => l.includes('Warning: Kept'))).toBe(false); + expect(fs.existsSync('.charter/.backup')).toBe(false); + }); + } + + for (const h of HISTORY.filter(e => !e.safe)) { it(`keeps the ${h.commit} ${h.packageManager} template with the static message`, async () => { write(WF, h.text); if (h.packageManager === 'pnpm') write('pnpm-lock.yaml', ''); diff --git a/packages/cli/src/commands/github-workflow.ts b/packages/cli/src/commands/github-workflow.ts index bbdaba6..a3656ac 100644 --- a/packages/cli/src/commands/github-workflow.ts +++ b/packages/cli/src/commands/github-workflow.ts @@ -4,7 +4,8 @@ * * `bootstrap --ci github` and `setup --ci github` overwrite the workflow * without `--force` only when it is a known Charter template marked `safe` - * (the current npm or pnpm form). An older Charter template is kept, with a + * (the current npm or pnpm form, or an earlier one with the same permissions + * and triggers). An older Charter template is kept, with a * warning that states what refreshing it changes; a workflow that matches no * Charter template is kept, with a warning that states what the current * template grants. No claim is made about what a hand edit changed: the YAML @@ -22,19 +23,26 @@ export const TEMPLATE_PERMISSIONS = 'the current Charter template grants contents: write and pull-requests: write, runs on push, pull_request and workflow_dispatch, ' + 'and has a step that commits and pushes to the repo'; +/** What refreshing a 003ec8d template to the current one changes. Permissions and triggers are the same, so it is safe. */ +const PM_CLI_CHANGES = 'runs Charter as pnpm exec charter (pnpm) or npm exec --no --package=@stackbilt/cli -- charter (npm) instead of npx charter'; + /** What refreshing any pre-badge template (contents: read, pull_request only) to the current one changes. */ const PRE_BADGE_CHANGES = 'contents: read → write; adds push and workflow_dispatch triggers; adds a step that commits and pushes to the repo'; export interface KnownWorkflow { /** sha256 of the template after `normalizeWorkflow`. */ hash: string; - /** The first commit in setup.ts's history that generated it, and what set it apart. */ + /** The first commit in setup.ts's history that generated it (or, before it merges, its issue), and what set it apart. */ origin: string; /** The package manager its install steps are for. */ packageManager: 'npm' | 'pnpm'; /** Whether refreshing it to the current template changes nothing CI may do. */ safe: boolean; - /** For an entry that is not safe: what refreshing it changes, shown as is in the warning. */ + /** + * What refreshing it to the current template changes. For an entry that is + * not safe it is shown as is in the warning; for a safe one it only documents + * the change (a safe refresh is silent). + */ refreshChanges: string; } @@ -43,11 +51,15 @@ export interface KnownWorkflow { * history of `getGithubWorkflow` / `GITHUB_WORKFLOW` in setup.ts (each * template's npm and pnpm form). When the template changes, add the new * hashes (a test fails until the current ones are listed), mark the old ones - * unsafe if the new template grants more, and describe the change by hand. + * unsafe if the new template grants more, describe the change by hand, and add + * the old templates to the history fixture. */ export const KNOWN_WORKFLOWS: readonly KnownWorkflow[] = [ - { hash: '59b8380e5926f47656fb4870effe234090528bd9b1f0705d5e283e009e0ff225', origin: '003ec8d, current (score-badge steps)', packageManager: 'npm', safe: true, refreshChanges: '' }, - { hash: '1ff96c6454952dbc630b6cc079cd1fd7840f6b18673098bec030595968a302cd', origin: '003ec8d, current (score-badge steps)', packageManager: 'pnpm', safe: true, refreshChanges: '' }, + { hash: '66c71b0e133ffc5d555b941302346211d5b62ea43c8f1b0dc95e16d05f7cdf67', origin: '#380, current (Charter run through the package manager)', packageManager: 'npm', safe: true, refreshChanges: '' }, + { hash: 'afd0be1c5f55dc3628a0012f65e8cf51a3f672030c9af017470ceeb654374fcb', origin: '#380, current (Charter run through the package manager)', packageManager: 'pnpm', safe: true, refreshChanges: '' }, + // Same permissions and triggers as the current template: refreshed silently. + { hash: '59b8380e5926f47656fb4870effe234090528bd9b1f0705d5e283e009e0ff225', origin: '003ec8d (score-badge steps, npx charter)', packageManager: 'npm', safe: true, refreshChanges: PM_CLI_CHANGES }, + { hash: '1ff96c6454952dbc630b6cc079cd1fd7840f6b18673098bec030595968a302cd', origin: '003ec8d (score-badge steps, npx charter)', packageManager: 'pnpm', safe: true, refreshChanges: PM_CLI_CHANGES }, // Everything below ran on pull_request only, with contents: read. { hash: '38f58106b0dd4c9a0363d971bfd10c23f1a5067c1998224abd5ed9a70337f98f', origin: '0fdea60 (ADF wiring and evidence steps)', packageManager: 'npm', safe: false, refreshChanges: PRE_BADGE_CHANGES }, { hash: '87c27e54791175c4a3937490b9f7f03f841269b6d01b1fe303352ab0ebb5748f', origin: '0fdea60 (ADF wiring and evidence steps)', packageManager: 'pnpm', safe: false, refreshChanges: PRE_BADGE_CHANGES }, diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 3195126..a591c23 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -18,7 +18,20 @@ import packageJson from '../../package.json'; const CLI_VERSION = packageJson.version; +/** + * How the generated workflow runs Charter (#380): through the repo's package + * manager, from the @stackbilt/cli the install step put in node_modules. A bare + * `npx charter` names the unrelated `charter` registry package, which npx + * fetches when the CLI is not installed. Neither form installs anything: with no + * local @stackbilt/cli the step fails. The npm form keeps `charter validate` in + * the step, which doctor's CI-gate check looks for. + */ +export function workflowCharterCommand(packageManager: 'npm' | 'pnpm'): string { + return packageManager === 'pnpm' ? 'pnpm exec charter' : 'npm exec --no --package=@stackbilt/cli -- charter'; +} + export function getGithubWorkflow(packageManager: 'npm' | 'pnpm'): string { + const cli = workflowCharterCommand(packageManager); const installStep = packageManager === 'pnpm' ? ` - uses: pnpm/action-setup@v4 with: @@ -69,25 +82,25 @@ jobs: ${installStep} - name: Validate Commits - run: npx charter validate --ci --format text + run: ${cli} validate --ci --format text - name: Drift Scan - run: npx charter drift --ci --format text + run: ${cli} drift --ci --format text - name: ADF Wiring & Pointer Integrity - run: npx charter doctor --adf-only --ci --format text + run: ${cli} doctor --adf-only --ci --format text if: hashFiles('.ai/manifest.adf') != '' - name: ADF Evidence - run: npx charter adf evidence --auto-measure --ci --format text + run: ${cli} adf evidence --auto-measure --ci --format text if: hashFiles('.ai/manifest.adf') != '' - name: Audit Report - run: npx charter audit --format json > /tmp/audit.json + run: ${cli} audit --format json > /tmp/audit.json if: always() - name: Refresh Score Badge - run: npx charter score --badge --write + run: ${cli} score --badge --write if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - name: Commit Score Badge