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
30 changes: 29 additions & 1 deletion bin/verify-workspace-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,24 @@ function localManifest(root) {
}
: undefined;

// package.json (parsed) so the biome-linter rule can read the @biomejs/biome
// dep. null = not a node repo (Python/Rust/etc) → the rule is N/A there.
let packageJson = null;
try {
const pkgPath = join(root, 'package.json');
if (tracked ? tracked.has('package.json') : existsSync(pkgPath)) {
packageJson = JSON.parse(readFileSync(pkgPath, 'utf8'));
}
} catch {
packageJson = null;
}

return {
hasFile: (p) => (tracked ? tracked.has(p) : existsSync(join(root, p))),
gitignore,
...(isIgnored ? { isIgnored } : {}),
workflowRunners,
packageJson,
};
}

Expand Down Expand Up @@ -220,6 +233,8 @@ async function remoteManifest(repo, ref) {
'.beads/issues.jsonl',
'.beads/beads.db',
'.automaker/settings.json',
'biome.json',
'biome.jsonc',
];
const present = new Set();
await Promise.all(
Expand Down Expand Up @@ -269,7 +284,20 @@ async function remoteManifest(repo, ref) {
/* no .github/workflows dir */
}

return { hasFile: (p) => present.has(p), gitignore, workflowRunners };
// Fetch + parse package.json (for the biome-linter rule). null = not a node repo.
let packageJson = null;
try {
const { stdout } = await execFileAsync(
'gh',
['api', `repos/${owner}/${name}/contents/package.json?ref=${resolvedRef}`, '--jq', '.content'],
{ encoding: 'utf8' }
);
packageJson = JSON.parse(Buffer.from(stdout.trim(), 'base64').toString('utf8'));
} catch {
packageJson = null;
}

return { hasFile: (p) => present.has(p), gitignore, workflowRunners, packageJson };
}

async function defaultBranch(owner, name) {
Expand Down
24 changes: 24 additions & 0 deletions docs/reference/workspace-config-standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Enforced by `verify-workspace-config` (this package).
| `automaker-transient-gitignored` | warn | `.automaker/features/`, `checkpoints/`, `trajectory/` gitignored; `settings.json` + `context/` stay committed |
| `workflows-use-owned-runners` | error | every `runs-on:` is an org-owned runner (`namespace-profile-protolabs-linux`), never GitHub-hosted (`ubuntu-*`, `windows-*`, `macos-*`) |
| `workflow-security-lint` | warn | `.github/workflows/workflow-security-lint.yml` exists — zizmor + actionlint run in CI (CWE-94 script-injection, unpinned actions, token over-privilege) |
| `biome-linter` | warn | JS/TS repos (those with a `package.json`) lint with **biome** at the fleet-pinned version (`FLEET_BIOME_VERSION`); non-node repos are N/A |

**Errors gate CI. Warnings are advisory.**

Expand Down Expand Up @@ -81,6 +82,29 @@ jobs:
`warn` for now so the fleet can roll out without breaking CI on day one;
tighten to `error` once every managed repo carries it.

### Why JS/TS repos track one biome version

protoLabs standardizes JS/TS linting on [biome](https://biomejs.dev) — one fast
single-binary linter, no ESLint plugin sprawl. The fleet tracks **one** biome
version together so a lint rule means the same thing in every repo and upgrades
land in lockstep instead of scattering into per-repo drift.

The pinned version lives in **one place** — `FLEET_BIOME_VERSION` in
`release-tools` (`lib/workspace-config.mjs`). Bump it there and
`verify-workspace-config` flags every node repo that hasn't followed, with the
gap spelled out in the detail line (no `@biomejs/biome` dep → still on ESLint;
biome declared but no `biome.json`; or a version different from the fleet pin).
Adopt or catch up with:

```bash
npm i -D @biomejs/biome@<FLEET_BIOME_VERSION> # the version from the table above
npm pkg set scripts.lint="biome lint ." # add a biome.json, drop ESLint
```

The rule is **N/A for non-node repos** (Python, Rust — no `package.json`), so it
never fires there. `warn` for now so adoption rolls out without breaking CI;
tighten to `error` once every node repo is on biome.

### Why `.automaker/settings.json` is committed per-repo

The per-repo settings file pins the agent baseline — gateway model tiers
Expand Down
50 changes: 50 additions & 0 deletions lib/workspace-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,29 @@ jobs:
/**
* The standard. Ordered; errors gate CI, warns are advisory.
*/
/**
* The biome version the fleet tracks together. Single source of truth: bump it
* here and `verify-workspace-config` flags every JS/TS repo that hasn't followed,
* so the fleet upgrades biome in lockstep instead of drifting per-repo.
*/
export const FLEET_BIOME_VERSION = '2.4.16';

/** Strip a semver range prefix (^ ~ >= <= = and whitespace) → the bare version. */
export function cleanVersion(spec) {
return String(spec ?? '')
.trim()
.replace(/^[\^~>=<\s]+/, '');
}

/**
* The declared @biomejs/biome version spec from a parsed package.json, or null
* if the repo doesn't depend on biome. Checks devDependencies then dependencies.
*/
export function biomeVersion(pkg) {
if (!pkg || typeof pkg !== 'object') return null;
return pkg.devDependencies?.['@biomejs/biome'] ?? pkg.dependencies?.['@biomejs/biome'] ?? null;
}

export const WORKSPACE_STANDARD = /** @type {Rule[]} */ ([
{
id: 'beads-issues-jsonl',
Expand Down Expand Up @@ -196,6 +219,33 @@ export const WORKSPACE_STANDARD = /** @type {Rule[]} */ ([
ok: (m) => m.hasFile(WORKFLOW_SECURITY_LINT_PATH),
fix: `Add ${WORKFLOW_SECURITY_LINT_PATH} (run \`init-workspace-config\`, or call the reusable workflow: \`uses: protoLabsAI/release-tools/.github/workflows/workflow-security-lint.yml@main\`)`,
},
{
id: 'biome-linter',
description: `JS/TS repos lint with biome ${FLEET_BIOME_VERSION} — the fleet tracks one biome version together (no ESLint, no per-repo version drift)`,
// warn (not error) so adoption rolls out across the fleet without breaking CI
// on day one — same posture as workflow-security-lint. Tighten to error once
// every node repo is on biome. N/A for non-node repos (Python/Rust have no
// package.json) → ok=true so they don't show as violations.
severity: 'warn',
ok: (m) => {
if (!m.packageJson) return true;
const v = biomeVersion(m.packageJson);
if (!v) return false;
if (!m.hasFile('biome.json') && !m.hasFile('biome.jsonc')) return false;
return cleanVersion(v) === cleanVersion(FLEET_BIOME_VERSION);
},
fix: `Track the fleet biome: \`npm i -D @biomejs/biome@${FLEET_BIOME_VERSION}\`, add a biome.json, set \`"lint": "biome lint ."\` (drop ESLint). Fleet version is pinned in release-tools FLEET_BIOME_VERSION.`,
detail: (m) => {
if (!m.packageJson) return [];
const v = biomeVersion(m.packageJson);
if (!v) return ['no @biomejs/biome dependency (still on ESLint, or unlinted?)'];
if (!m.hasFile('biome.json') && !m.hasFile('biome.jsonc'))
return [`@biomejs/biome ${v} declared but no biome.json / biome.jsonc`];
if (cleanVersion(v) !== cleanVersion(FLEET_BIOME_VERSION))
return [`biome ${v} — fleet tracks ${FLEET_BIOME_VERSION}`];
return [];
},
},
]);

/**
Expand Down
82 changes: 82 additions & 0 deletions test/workspace-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
SCAFFOLD_FILES,
REQUIRED_GITIGNORE,
WORKFLOW_SECURITY_LINT_PATH,
FLEET_BIOME_VERSION,
cleanVersion,
biomeVersion,
} from '../lib/workspace-config.mjs';

/** Build a manifest from a list of present files + gitignore text. */
Expand Down Expand Up @@ -364,3 +367,82 @@ test('planScaffold leaves selective .automaker/features/ alone (no false removal
assert.deepEqual(plan.gitignoreRemovals, []);
assert.deepEqual(plan.gitignoreAdditions, []);
});

// ── biome-linter (fleet biome version tracking) ──────────────────────────────

/** Build a manifest for a node repo with a given biome dep + biome.json presence. */
function nodeManifest({ biomeDep, hasBiomeJson = true, dev = true } = {}) {
const files = new Set(hasBiomeJson ? ['biome.json'] : []);
const deps = biomeDep ? { '@biomejs/biome': biomeDep } : {};
return {
hasFile: (p) => files.has(p),
gitignore: '',
packageJson: { [dev ? 'devDependencies' : 'dependencies']: deps },
};
}

const biomeViolation = (m) =>
evaluateStandard(m).violations.find((v) => v.id === 'biome-linter');

test('cleanVersion strips semver range prefixes', () => {
assert.equal(cleanVersion('2.4.16'), '2.4.16');
assert.equal(cleanVersion('^2.4.16'), '2.4.16');
assert.equal(cleanVersion('~2.4.16'), '2.4.16');
assert.equal(cleanVersion('>=2.4.16'), '2.4.16');
assert.equal(cleanVersion(' 2.4.16 '), '2.4.16');
assert.equal(cleanVersion(undefined), '');
});

test('biomeVersion reads devDependencies then dependencies, null when absent', () => {
assert.equal(biomeVersion({ devDependencies: { '@biomejs/biome': '2.4.16' } }), '2.4.16');
assert.equal(biomeVersion({ dependencies: { '@biomejs/biome': '^2.0.0' } }), '^2.0.0');
assert.equal(biomeVersion({ devDependencies: { eslint: '^9' } }), null);
assert.equal(biomeVersion(null), null);
});

test('biome-linter is N/A for non-node repos (no packageJson)', () => {
// Python/Rust repos have no package.json → rule passes, no violation.
const m = { hasFile: () => false, gitignore: '' };
assert.equal(biomeViolation(m), undefined);
assert.ok(evaluateStandard(m).passed.includes('biome-linter'));
});

test('biome-linter passes when a node repo is on the fleet biome version', () => {
const m = nodeManifest({ biomeDep: FLEET_BIOME_VERSION });
assert.equal(biomeViolation(m), undefined);
// a caret range on the same version still matches (cleanVersion strips ^)
assert.equal(biomeViolation(nodeManifest({ biomeDep: `^${FLEET_BIOME_VERSION}` })), undefined);
});

test('biome-linter warns (not errors) when a node repo has no biome dep', () => {
// Otherwise-conformant repo on ESLint → only the biome-linter warn fires, and
// a warn must not fail overall conformance.
const m = { ...goodManifest(), packageJson: { devDependencies: { eslint: '^9' } } };
const r = evaluateStandard(m);
const v = r.violations.find((x) => x.id === 'biome-linter');
assert.ok(v, 'expected a biome-linter violation');
assert.equal(v.severity, 'warn');
assert.match(v.detail.join(' '), /no @biomejs\/biome/);
assert.equal(r.ok, true, 'warn-severity biome gap must not fail conformance');
});

test('biome-linter warns when biome is declared but no biome.json', () => {
const v = biomeViolation(nodeManifest({ biomeDep: FLEET_BIOME_VERSION, hasBiomeJson: false }));
assert.ok(v);
assert.match(v.detail.join(' '), /no biome\.json/);
});

test('biome-linter warns on version drift from the fleet pin', () => {
const v = biomeViolation(nodeManifest({ biomeDep: '2.3.0' }));
assert.ok(v);
assert.match(v.detail.join(' '), new RegExp(`fleet tracks ${FLEET_BIOME_VERSION.replace(/\./g, '\\.')}`));
});

test('biome.jsonc satisfies the config requirement too', () => {
const m = {
hasFile: (p) => p === 'biome.jsonc',
gitignore: '',
packageJson: { devDependencies: { '@biomejs/biome': FLEET_BIOME_VERSION } },
};
assert.equal(biomeViolation(m), undefined);
});
Loading