chore(contract): check generated bindings before push

This commit is contained in:
Tunglies
2026-10-08 02:56:30 +08:00
parent 8a7fb6db49
commit b92d56c98e
7 changed files with 158 additions and 41 deletions
+4 -18
View File
@@ -6,6 +6,7 @@ on:
permissions:
contents: read
pull-requests: read
env:
HUSKY: 0
@@ -99,20 +100,7 @@ jobs:
id: check_inputs
uses: dorny/paths-filter@v4
with:
filters: |
contract:
- 'src/services/contract.ts'
- 'src/services/bindings.ts'
- 'Cargo.lock'
- 'Cargo.toml'
- 'src-tauri/Cargo.toml'
- 'crates/**'
- 'src-tauri/src/**'
- 'scripts/gen-contract.mjs'
- 'src-tauri/src/core/notify/**'
- 'src-tauri/src/bin/**'
- 'src-tauri/src/lib.rs'
- '.github/workflows/frontend-check.yml'
filters: scripts/contract-inputs.json
- name: Skip if contract inputs unchanged
if: steps.check_inputs.outputs.contract != 'true'
@@ -165,8 +153,6 @@ jobs:
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
done
- name: Regenerate contract and require empty diff
- name: Check generated contract without changing the checkout
if: steps.check_inputs.outputs.contract == 'true'
run: |
pnpm gen:contract
git diff --exit-code -- src/services/bindings.ts
run: pnpm gen:contract --check
+1
View File
@@ -5,4 +5,5 @@ if ! command -v "cargo-make" >/dev/null 2>&1; then
cargo install --force cargo-make
fi
node scripts/check-contract-changes.mjs --push "$1" || exit $?
cargo make pre-push
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"entry": ["src/polyfills/*.js!"],
"entry": ["src/polyfills/*.js!", "scripts/check-contract-changes.mjs!"],
"project": ["src/**/*.{js,jsx,ts,tsx,sass,scss}!", "scripts/**/*.{js,mjs}!"],
"include": [
"files",
+69
View File
@@ -0,0 +1,69 @@
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
const ROOT = path.resolve(import.meta.dirname, '..')
const git = (...args) =>
execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }).trim()
const { contract } = JSON.parse(
fs.readFileSync(path.join(ROOT, 'scripts/contract-inputs.json'), 'utf8'),
)
const inputs = contract.map((pattern) => `:(glob)${pattern}`)
const args = process.argv.slice(2)
if (args.length !== 2 || args[0] !== '--push') {
console.error('usage: check-contract-changes.mjs --push <remote>')
process.exit(2)
}
const hasChanges = (...range) =>
git('diff', '--name-only', ...range, '--', ...inputs) !== ''
const assertCleanInputs = (...range) => {
if (
hasChanges(...range) ||
git('ls-files', '--others', '--exclude-standard', '--', ...inputs)
) {
throw new Error(
'Contract inputs differ from the pushed commit. Commit those changes before pushing.',
)
}
}
let relevant = false
const head = git('rev-parse', 'HEAD')
for (const line of fs
.readFileSync(0, 'utf8')
.trim()
.split('\n')
.filter(Boolean)) {
const fields = line.trim().split(/\s+/)
if (
fields.length !== 4 ||
![fields[1], fields[3]].every((sha) => /^[a-f0-9]{40,64}$/.test(sha))
) {
throw new Error('Invalid pre-push ref update')
}
const [, local, , remote] = fields
if (/^0+$/.test(local) || local === remote) continue
// A new remote branch has no upstream; use its fork point with the remote default branch.
const base = /^0+$/.test(remote)
? git('merge-base', local, `refs/remotes/${args[1]}/HEAD`)
: remote
if (!hasChanges(base, local)) continue
const commit = git('rev-parse', `${local}^{commit}`)
if (commit !== head)
throw new Error(
'Check out the pushed commit before validating its changed IPC contract.',
)
assertCleanInputs('HEAD')
relevant = true
}
if (relevant) {
execFileSync(
process.execPath,
[path.join(ROOT, 'scripts/gen-contract.mjs'), '--check'],
{
cwd: ROOT,
stdio: 'inherit',
},
)
} else {
console.log('[contract] inputs unchanged; skipping Rust generation')
}
+23
View File
@@ -0,0 +1,23 @@
{
"contract": [
"src-tauri/**/*.rs",
"crates/**/*.rs",
"Cargo.toml",
"Cargo.lock",
"src-tauri/Cargo.toml",
"crates/**/Cargo.toml",
"rust-toolchain.toml",
"src-tauri/tauri.conf*.json",
"src/services/bindings.ts",
"scripts/gen-contract.mjs",
"scripts/check-contract-changes.mjs",
"biome.json",
"biome.jsonc",
"package.json",
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
"scripts/contract-inputs.json",
".github/workflows/frontend-check.yml",
".husky/pre-push"
]
}
+51 -18
View File
@@ -1,22 +1,55 @@
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
const ROOT = path.resolve(import.meta.dirname, '..')
execFileSync(
'cargo',
['run', '-p', 'clash-verge', '--bin', 'frontend-contract', '--locked', '-q'],
{
cwd: ROOT,
stdio: 'inherit',
},
)
execFileSync(
process.execPath,
[
path.join(ROOT, 'node_modules/@biomejs/biome/bin/biome'),
'format',
'--write',
'src/services/bindings.ts',
],
{ cwd: ROOT, stdio: 'inherit' },
)
const BINDINGS = path.join(ROOT, 'src/services/bindings.ts')
const args = process.argv.slice(2)
if (args.length > 1 || (args.length === 1 && args[0] !== '--check')) {
console.error('usage: gen-contract.mjs [--check]')
process.exit(2)
}
const check = args[0] === '--check'
const temporary = check
? fs.mkdtempSync(path.join(os.tmpdir(), 'verge-contract-'))
: null
const output = temporary ? path.join(temporary, 'bindings.ts') : BINDINGS
try {
execFileSync(
'cargo',
[
'run',
'-p',
'clash-verge',
'--bin',
'frontend-contract',
'--locked',
'-q',
...(check ? ['--', output] : []),
],
{ cwd: ROOT, stdio: 'inherit' },
)
const formatted = execFileSync(
process.execPath,
[
path.join(ROOT, 'node_modules/@biomejs/biome/bin/biome'),
'format',
'--stdin-file-path',
BINDINGS,
],
{ cwd: ROOT, input: fs.readFileSync(output) },
)
if (check) {
if (!formatted.equals(fs.readFileSync(BINDINGS))) {
console.error(
'IPC bindings are out of date. Run pnpm gen:contract and commit the generated file.',
)
process.exitCode = 1
}
} else {
fs.writeFileSync(BINDINGS, formatted)
}
} finally {
if (temporary) fs.rmSync(temporary, { recursive: true, force: true })
}
+9 -4
View File
@@ -1,7 +1,12 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
app_lib::ipc::builder().export(
specta_typescript::Typescript::default(),
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"),
)?;
let mut args = std::env::args_os().skip(1);
let output = args
.next()
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"));
if args.next().is_some() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "expected at most one output path").into());
}
app_lib::ipc::builder().export(specta_typescript::Typescript::default(), output)?;
Ok(())
}