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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ jobs:
npm run check:tui-copy
node --test scripts/check-tui-copy.test.mjs

- name: Test staged Biome hook
if: steps.plan.outputs.code == 'true'
run: node --test scripts/biome-staged-check.test.mjs

# The header audit above remains install-free. The complete source gate
# also exercises generation and therefore runs after its pinned formatter
# dependency is installed, matching the source-candidate workflow.
Expand Down
21 changes: 21 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

node scripts/biome-staged-check.mjs
node scripts/asf-license-headers.mjs check-staged
node scripts/protocol-epoch-check.mjs --staged
git diff --cached --check
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"apps/desktop"
],
"scripts": {
"prepare": "npm run sync:model-metadata",
"prepare": "npm run sync:model-metadata && node scripts/install-husky.mjs",
"postinstall": "node scripts/apply-dependency-patches.mjs && node scripts/install-electron-with-retry.mjs",
"lint": "biome lint .",
"format": "biome format --write .",
Expand Down Expand Up @@ -110,6 +110,7 @@
"@biomejs/biome": "2.5.9",
"@types/node": "^26.2.0",
"esbuild": "^0.28.1",
"husky": "^9.1.7",
"knip": "^6.32.2",
"patch-package": "8.0.1",
"typescript": "^7.0.2",
Expand Down
41 changes: 39 additions & 2 deletions scripts/asf-license-headers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const coveredExtensions = new Map([
/** Covered files whose name carries no extension. */
const coveredNames = new Map([
['Dockerfile', 'hash'],
['pre-commit', 'hash'],
// A POSIX shell script that the Eval egress sidecar invokes by name.
['network-policy', 'hash'],
]);
Expand Down Expand Up @@ -617,6 +618,38 @@ export function auditTree({ root = defaultRepoRoot } = {}) {
return { ...result, mode: listing.mode, root };
}

export function auditStaged({ root = defaultRepoRoot } = {}) {
const output = execFileSync('git', ['diff', '--cached', '--name-only', '--diff-filter=A', '-z'], {
cwd: root,
encoding: 'utf8',
maxBuffer: maxCommandBuffer,
});
const files = output.split('\0').filter(Boolean);
const result = auditSourceFiles({ files, mode: 'staged' });
for (const path of files) {
const classification = classifyPath(path);
if (classification.status !== 'covered') continue;
const contents = execFileSync('git', ['show', `:${path}`], {
cwd: root,
encoding: 'utf8',
maxBuffer: maxCommandBuffer,
});
const status = classifyHeader(contents, classification.style, {
textAsData: licenseTextAsData.has(path),
});
if (status === 'absent') result.missing.push(path);
else if (status === 'duplicated') result.duplicated.push(path);
else if (status === 'unrecognized') result.unrecognized.push(path);
if (
!reviewedProvenance.has(path) &&
provenanceMarkers.some((marker) => marker.test(contents))
) {
result.unreviewedProvenance.push(path);
}
}
return { ...result, mode: 'staged', root };
}

export function writeHeaders({ root = defaultRepoRoot } = {}) {
const { files } = listSourceFiles(root);
const changed = [];
Expand Down Expand Up @@ -651,8 +684,8 @@ function reportExclusions(result) {
}
}

function runCheck({ report, root }) {
const result = auditTree({ root });
function runCheck({ report, root, staged = false }) {
const result = staged ? auditStaged({ root }) : auditTree({ root });
const excluded = [...result.excludedByRule.values()].reduce(
(total, paths) => total + paths.length,
0,
Expand Down Expand Up @@ -732,6 +765,10 @@ function main() {
runCheck({ report, root });
return;
}
if (command === 'check-staged') {
runCheck({ report, root, staged: true });
return;
}
if (command === 'write') {
const changed = writeHeaders({ root });
console.log(`Added the ASF header to ${changed.length} file(s)`);
Expand Down
1 change: 1 addition & 0 deletions scripts/asf-license-headers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ describe('ASF header classification', () => {
'experiments/windows-sandbox/launcher/src/main.rs',
'packages/eval/harbor/egress-proxy/Dockerfile',
'packages/eval/harbor/egress-proxy/network-policy',
'.husky/pre-commit',
'.github/workflows/ci.yml',
'README.md',
]) {
Expand Down
75 changes: 75 additions & 0 deletions scripts/biome-staged-check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env node
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { execFileSync, spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';

const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const defaultRepoRoot = resolve(scriptDirectory, '..');
const defaultBiomePath = join(
defaultRepoRoot,
'node_modules',
'.bin',
process.platform === 'win32' ? 'biome.cmd' : 'biome',
);

export function checkStagedWithBiome({
root = defaultRepoRoot,
biomePath = defaultBiomePath,
} = {}) {
const output = execFileSync(
'git',
['diff', '--cached', '--name-only', '--diff-filter=ACMR', '-z'],
{ cwd: root },
);
const paths = output.toString('utf8').split('\0').filter(Boolean);

for (const path of paths) {
const contents = execFileSync('git', ['show', `:${path}`], { cwd: root });
const result = spawnSync(
biomePath,
[
'check',
'--write',
`--stdin-file-path=${path}`,
'--files-ignore-unknown=true',
'--no-errors-on-unmatched',
],
{ cwd: root, input: contents },
);
if (result.error) throw result.error;
if (result.status !== 0) {
if (result.stdout.length > 0) process.stdout.write(result.stdout);
if (result.stderr.length > 0) process.stderr.write(result.stderr);
return false;
}
if (!result.stdout.equals(contents)) {
process.stderr.write(`${path}: staged content is not formatted by Biome\n`);
return false;
}
}

return true;
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (!checkStagedWithBiome()) process.exitCode = 1;
}
73 changes: 73 additions & 0 deletions scripts/biome-staged-check.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { checkStagedWithBiome } from './biome-staged-check.mjs';

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const biomePath = join(
repoRoot,
'node_modules',
'.bin',
process.platform === 'win32' ? 'biome.cmd' : 'biome',
);

function fixture() {
const root = mkdtempSync(join(tmpdir(), 'maka-biome-staged-'));
execFileSync('git', ['init', '-q'], { cwd: root });
writeFileSync(
join(root, 'biome.json'),
JSON.stringify({ formatter: { enabled: true }, linter: { enabled: false } }),
);
return root;
}

test('checks staged bytes when the working tree was formatted afterward', () => {
const root = fixture();
try {
const path = join(root, 'example.js');
writeFileSync(path, 'const value={answer:42};\n');
execFileSync('git', ['add', 'example.js'], { cwd: root });
writeFileSync(path, 'const value = { answer: 42 };\n');

assert.equal(checkStagedWithBiome({ root, biomePath }), false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

test('ignores unstaged formatting drift when staged bytes are formatted', () => {
const root = fixture();
try {
const path = join(root, 'example.js');
writeFileSync(path, 'const value = { answer: 42 };\n');
execFileSync('git', ['add', 'example.js'], { cwd: root });
writeFileSync(path, 'const value={answer:42};\n');

assert.equal(checkStagedWithBiome({ root, biomePath }), true);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
35 changes: 35 additions & 0 deletions scripts/install-husky.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env node
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

try {
const install = (await import('husky')).default;
process.stdout.write(install());
} catch (error) {
if (
error instanceof Error &&
'code' in error &&
error.code === 'ERR_MODULE_NOT_FOUND' &&
error.message.includes("package 'husky'")
) {
console.warn('husky is not installed; skipping Git hook setup.');
} else {
throw error;
}
}
Loading
Loading