Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<!-- Add runtime/OS/shell-specific notes here (not stack rules) -->` 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:
Expand Down
4 changes: 3 additions & 1 deletion docs/ci-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
33 changes: 29 additions & 4 deletions packages/cli/src/__tests__/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -117,17 +118,41 @@ 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', () => {
const workflow = getGithubWorkflow('pnpm');
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');
Expand Down
Loading
Loading