Skip to content
Open
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
47 changes: 2 additions & 45 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,53 +91,10 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Build packages (restored from Turbo cache)
run: pnpm build
- name: Lint dependencies
run: pnpm lint:deps
- name: Lint code shape
run: pnpm lint:code
- name: Lint packages
run: pnpm lint:packages
- name: Lint examples
run: pnpm lint:examples
- name: Validate rules
run: pnpm lint:rules
- name: Validate rule symlinks
run: pnpm lint:rules:symlinks
- name: Validate skills
run: pnpm lint:skills
- name: Check rules footprint
run: pnpm lint:rules:footprint
- name: Validate package READMEs
run: pnpm lint:docs
- name: Validate package manifests (license declarations)
run: pnpm lint:manifests
- name: Lint workflow triggers (forbid Pwn Request pattern)
run: pnpm lint:workflows
- name: Test scripts/
run: pnpm test:scripts
- name: Lint casts
run: pnpm lint:casts
- name: Lint throws
run: pnpm lint:throws
- name: Lint framework vocabulary
run: pnpm lint:framework-vocabulary
- name: Lint consumer internal imports
run: pnpm lint:consumer-internal-imports
- name: Lint the legacy product name
run: pnpm lint:legacy-name

- name: Lint publishability matches the directory layout
run: pnpm lint:publishability
- name: Check upgrade-instruction coverage
env:
BASE: ${{ github.base_ref || 'main' }}
run: pnpm check:upgrade-coverage --mode pr --prev "origin/$BASE"
- name: Check error-reference completeness
run: pnpm check:error-reference
- name: Check release notes
- name: Run lint gates
env:
BASE: ${{ github.base_ref || 'main' }}
run: pnpm check:release-notes --mode pr --prev "origin/$BASE"
run: pnpm lint:ci

fixtures:
name: Fixtures
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,19 @@
"coverage:packages:merge": "vitest --merge-reports=.vitest/blob --coverage",
"lint": "turbo run lint",
"lint:agent": "node scripts/run-logged.mjs lint pnpm lint",
"lint:ci": "node scripts/lint-ci.mjs",
"lint:fix": "pnpm biome check --write .",
"lint:fix:unsafe": "pnpm biome check --write --unsafe .",
"lint:packages": "turbo run lint --filter='!./examples/**'",
"lint:packages:ci": "node scripts/lint-workspaces.mjs packages",
"lint:examples": "turbo run lint --filter='./examples/**'",
"lint:examples:ci": "node scripts/lint-workspaces.mjs examples",
"lint:deps": "depcruise --config dependency-cruiser.config.mjs packages && node scripts/lint-framework-target-imports.mjs && node scripts/lint-app-space-id.mjs && node scripts/lint-single-import-root.mjs",
"lint:deps:agent": "node scripts/run-logged.mjs lint-deps pnpm lint:deps",
"lint:casts": "node scripts/lint-casts.mjs",
"lint:throws": "node scripts/lint-throws.mjs",
"lint:framework-vocabulary": "node scripts/lint-framework-vocabulary.mjs",
"lint:ratchets": "node scripts/lint-ratchets.mjs",
"lint:code": "node scripts/lint-no-contract-cast.mjs",
"lint:no-contract-cast": "node scripts/lint-no-contract-cast.mjs",
"lint:rules": "node scripts/validate-rules.mjs",
Expand All @@ -47,7 +51,7 @@
"lint:docs": "node scripts/validate-package-readmes.mjs",
"lint:manifests": "node scripts/validate-package-manifests.mjs && node scripts/validate-typescript-peer.mjs",
"lint:workflows": "node scripts/lint-workflow-triggers.mjs",
"test:scripts": "node --test scripts/coverage-config.test.mjs scripts/coverage-report.test.mjs scripts/lint-workflow-triggers.test.mjs scripts/validate-skills.test.mjs scripts/determine-version-utils.test.ts scripts/check-upgrade-coverage.test.mjs scripts/check-release-notes.test.mjs scripts/set-version-utils.test.ts scripts/check-publish-deps.test.mjs scripts/check-conformance.test.mjs scripts/check-publish-deps-pn-pins.test.mjs scripts/check-publish-deps-declarations.test.mjs scripts/validate-package-manifests.test.mjs scripts/validate-package-readmes.test.mjs scripts/publish-packages-utils.test.mjs scripts/check-clean-tree.test.mjs scripts/lint-casts.test.mjs scripts/lint-throws.test.mjs scripts/list-error-codes.test.mjs scripts/lint-framework-vocabulary.test.mjs scripts/lint-single-import-root.test.mjs scripts/lint-legacy-name.test.mjs scripts/lint-consumer-internal-imports.test.mjs scripts/sync-agent-rules.test.mjs scripts/validate-typescript-peer.test.mjs scripts/run-logged.test.mjs scripts/migrate-migrations-layout.test.mjs skills-contrib/review-fetch-phase/scripts/render-review-state.test.mjs skills-contrib/review-triage-phase/scripts/render-review-actions.test.mjs",
"test:scripts": "node --test scripts/coverage-config.test.mjs scripts/coverage-report.test.mjs scripts/lint-workflow-triggers.test.mjs scripts/validate-skills.test.mjs scripts/determine-version-utils.test.ts scripts/check-upgrade-coverage.test.mjs scripts/check-release-notes.test.mjs scripts/set-version-utils.test.ts scripts/check-publish-deps.test.mjs scripts/check-conformance.test.mjs scripts/check-publish-deps-pn-pins.test.mjs scripts/check-publish-deps-declarations.test.mjs scripts/validate-package-manifests.test.mjs scripts/validate-package-readmes.test.mjs scripts/publish-packages-utils.test.mjs scripts/check-clean-tree.test.mjs scripts/lint-casts.test.mjs scripts/lint-throws.test.mjs scripts/lint-ratchets.test.mjs scripts/lint-workspaces.test.mjs scripts/lint-ci.test.mjs scripts/list-error-codes.test.mjs scripts/lint-framework-vocabulary.test.mjs scripts/lint-single-import-root.test.mjs scripts/lint-legacy-name.test.mjs scripts/lint-consumer-internal-imports.test.mjs scripts/sync-agent-rules.test.mjs scripts/validate-typescript-peer.test.mjs scripts/run-logged.test.mjs scripts/migrate-migrations-layout.test.mjs skills-contrib/review-fetch-phase/scripts/render-review-state.test.mjs skills-contrib/review-triage-phase/scripts/render-review-actions.test.mjs",
"bump-version": "node scripts/bump-version.ts",
"check:publish-deps": "node scripts/check-publish-deps.mjs",
"check:conformance": "node scripts/check-conformance.mjs",
Expand Down
86 changes: 86 additions & 0 deletions scripts/lint-ci.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env node

import { spawn } from 'node:child_process';

const DEFAULT_CONCURRENCY = 4;

export function createLintTasks(base) {
return [
{ name: 'packages', args: ['lint:packages:ci'] },
{ name: 'deps', args: ['lint:deps'] },
{ name: 'script_tests', args: ['test:scripts'] },
{ name: 'ratchets', args: ['lint:ratchets'] },
{ name: 'legacy_name', args: ['lint:legacy-name'] },
{ name: 'examples', args: ['lint:examples:ci'] },
{ name: 'code', args: ['lint:code'] },
{ name: 'rules', args: ['lint:rules'] },
{ name: 'rule_symlinks', args: ['lint:rules:symlinks'] },
{ name: 'skills', args: ['lint:skills'] },
{ name: 'rule_footprint', args: ['lint:rules:footprint'] },
{ name: 'docs', args: ['lint:docs'] },
{ name: 'manifests', args: ['lint:manifests'] },
{ name: 'workflows', args: ['lint:workflows'] },
{ name: 'consumer_imports', args: ['lint:consumer-internal-imports'] },
{ name: 'publishability', args: ['lint:publishability'] },
{
name: 'upgrade_coverage',
args: ['check:upgrade-coverage', '--mode', 'pr', '--prev', `origin/${base}`],
},
{ name: 'error_reference', args: ['check:error-reference'] },
{
name: 'release_notes',
args: ['check:release-notes', '--mode', 'pr', '--prev', `origin/${base}`],
},
];
}

export async function runTasks(tasks, concurrency, execute) {
let next = 0;
const failures = [];

async function worker() {
while (next < tasks.length) {
const task = tasks[next];
next += 1;
if ((await execute(task)) !== 0) failures.push(task.name);
}
}

const workerCount = Math.min(concurrency, tasks.length);
await Promise.all(Array.from({ length: workerCount }, () => worker()));
return failures;
}

function executeTask(task) {
const start = Date.now();
console.log(`[lint:ci] start ${task.name}: pnpm ${task.args.join(' ')}`);
return new Promise((resolve) => {
const child = spawn('pnpm', task.args, { stdio: 'inherit' });
child.once('error', (error) => {
console.error(`[lint:ci] ${task.name} failed to start: ${error.message}`);
resolve(1);
});
child.once('close', (status) => {
const elapsed = ((Date.now() - start) / 1000).toFixed(3);
console.log(`[lint:ci] finish ${task.name}: status=${status ?? 1}`);
console.log(`LINT_METRIC ${task.name}_s=${elapsed}`);
resolve(status ?? 1);
});
});
}

function concurrencyFromEnvironment() {
const parsed = Number.parseInt(process.env.LINT_CONCURRENCY ?? '', 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_CONCURRENCY;
}

async function main() {
const base = process.env.BASE || 'main';
const failures = await runTasks(createLintTasks(base), concurrencyFromEnvironment(), executeTask);
if (failures.length > 0) {
console.error(`[lint:ci] failed: ${failures.join(', ')}`);
process.exit(1);
}
}

if (process.argv[1] === import.meta.filename) await main();
57 changes: 57 additions & 0 deletions scripts/lint-ci.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { createLintTasks, runTasks } from './lint-ci.mjs';

describe('createLintTasks', () => {
test('includes every post-build CI lint gate', () => {
assert.deepEqual(
createLintTasks('develop').map(({ name, args }) => [name, args]),
[
['packages', ['lint:packages:ci']],
['deps', ['lint:deps']],
['script_tests', ['test:scripts']],
['ratchets', ['lint:ratchets']],
['legacy_name', ['lint:legacy-name']],
['examples', ['lint:examples:ci']],
['code', ['lint:code']],
['rules', ['lint:rules']],
['rule_symlinks', ['lint:rules:symlinks']],
['skills', ['lint:skills']],
['rule_footprint', ['lint:rules:footprint']],
['docs', ['lint:docs']],
['manifests', ['lint:manifests']],
['workflows', ['lint:workflows']],
['consumer_imports', ['lint:consumer-internal-imports']],
['publishability', ['lint:publishability']],
[
'upgrade_coverage',
['check:upgrade-coverage', '--mode', 'pr', '--prev', 'origin/develop'],
],
['error_reference', ['check:error-reference']],
['release_notes', ['check:release-notes', '--mode', 'pr', '--prev', 'origin/develop']],
],
);
});
});

describe('runTasks', () => {
test('bounds concurrency and reports every failure', async () => {
let active = 0;
let peak = 0;
const completed = [];
const tasks = ['a', 'b', 'c', 'd'].map((name) => ({ name, args: [] }));

const failures = await runTasks(tasks, 2, async ({ name }) => {
active += 1;
peak = Math.max(peak, active);
await new Promise((resolve) => setTimeout(resolve, 5));
active -= 1;
completed.push(name);
return name === 'b' || name === 'd' ? 1 : 0;
});

assert.equal(peak, 2);
assert.deepEqual(completed.sort(), ['a', 'b', 'c', 'd']);
assert.deepEqual(failures.sort(), ['b', 'd']);
});
});
Loading
Loading