code-foundry 1.43.1 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -0
- package/CHANGELOG.md +14 -0
- package/README.md +3 -2
- package/docs/CONFIGURATION.md +55 -23
- package/package.json +1 -1
- package/src/commands/sync.mjs +38 -1
- package/src/lib/pre-commit.mjs +114 -0
- package/src/runtime-core.mjs +74 -14
package/AGENTS.md
CHANGED
|
@@ -211,6 +211,14 @@ At minimum:
|
|
|
211
211
|
|
|
212
212
|
If a check cannot run, state the exact reason. A skipped check is not a passing check.
|
|
213
213
|
|
|
214
|
+
The generated `.githooks/pre-commit` gate is change-aware so parallel work on a
|
|
215
|
+
shared checkout is not serialized behind repository-wide checks. It blocks
|
|
216
|
+
whitespace errors, formats and lints only staged files, runs the project's
|
|
217
|
+
type-check command only when typed sources or compiler configuration are
|
|
218
|
+
staged, and does not build unless `pre_commit_build: true` is set. Passing the
|
|
219
|
+
hook is not full validation: run the broader checks above before opening or
|
|
220
|
+
readying a pull request.
|
|
221
|
+
|
|
214
222
|
<!-- /code-foundry-managed: validation -->
|
|
215
223
|
|
|
216
224
|
<!-- code-foundry-managed: tests-and-coverage -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.44.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.2...v1.44.0) (2026-10-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **pre-commit:** make the commit gate change-aware ([#702](https://github.com/0xPlayerOne/code-foundry/issues/702)) ([f5bca91](https://github.com/0xPlayerOne/code-foundry/commit/f5bca91c177b30b99f5d62287255d191617b4788))
|
|
9
|
+
|
|
10
|
+
## [1.43.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.1...v1.43.2) (2026-10-03)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* require candidate asset qualification before stable promotion ([#705](https://github.com/0xPlayerOne/code-foundry/issues/705)) ([f42e4f5](https://github.com/0xPlayerOne/code-foundry/commit/f42e4f5fb063ea22fa25791a13c75c90d41bf17a))
|
|
16
|
+
|
|
3
17
|
## [1.43.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.43.0...v1.43.1) (2026-10-03)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -44,8 +44,9 @@ contract. For normal updates, edit that file and run `npx code-foundry sync`.
|
|
|
44
44
|
outcomes comparable across revisions and executors.
|
|
45
45
|
- An opt-in product-quality runner for static sites, web apps, Workers, and
|
|
46
46
|
published packages.
|
|
47
|
-
- A small `.githooks/pre-commit` launcher with
|
|
48
|
-
|
|
47
|
+
- A small `.githooks/pre-commit` launcher with a change-aware gate: it formats
|
|
48
|
+
and lints only staged files, type-checks only when typed sources are staged,
|
|
49
|
+
and leaves the build to CI.
|
|
49
50
|
- An optional `.mise.toml`/`mise.lock`, repository profile configuration, and
|
|
50
51
|
standard GitHub forms and policy files.
|
|
51
52
|
- AGENTS instructions, CODEOWNERS, license/notice files, and reusable release
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -115,29 +115,31 @@ See [Merge queue validation](merge-queues.md) before enabling it.
|
|
|
115
115
|
|
|
116
116
|
## Validation and quality
|
|
117
117
|
|
|
118
|
-
| Key
|
|
119
|
-
|
|
|
120
|
-
| `performance`
|
|
121
|
-
| `performance_command`
|
|
122
|
-
| `performance_profile`
|
|
123
|
-
| `performance_budget_file`
|
|
124
|
-
| `eval`
|
|
125
|
-
| `eval_command`
|
|
126
|
-
| `eval_report_file`
|
|
127
|
-
| `eval_budget_file`
|
|
128
|
-
| `eval_runner`
|
|
129
|
-
| `required_capabilities`
|
|
130
|
-
| `coverage_enforcement`
|
|
131
|
-
| `coverage_minimum`
|
|
132
|
-
| `coverage_metrics`
|
|
133
|
-
| `coverage_report`
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
118
|
+
| Key | Values | Purpose |
|
|
119
|
+
| ----------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `performance` | `auto`, `true`, `false` | Discover, require, or disable performance checks. |
|
|
121
|
+
| `performance_command` | JSON argv array or array of argv arrays | Ordered commands for a non-package performance harness. |
|
|
122
|
+
| `performance_profile` | empty or `node-package` | Shared package import, memory, archive, and dependency audit. |
|
|
123
|
+
| `performance_budget_file` | repository-relative path | Budget file for `node-package`; defaults to `performance-package-budgets.json`. |
|
|
124
|
+
| `eval` | `auto`, `true`, `false` | Discover, require, or disable the deterministic eval tier. |
|
|
125
|
+
| `eval_command` | JSON argv array | Explicit harness command when there is no `eval` package script. |
|
|
126
|
+
| `eval_report_file` | repository-relative path | Report validated against the eval contract; defaults to `eval-results/result.json`. |
|
|
127
|
+
| `eval_budget_file` | repository-relative path | Optional budget file; defaults to `eval-budgets.json`. |
|
|
128
|
+
| `eval_runner` | runner label | Runner for the `Validation / Eval` lane; defaults to the repository runner. Browser evals need a Chrome-capable runner such as `ubuntu-latest`. |
|
|
129
|
+
| `required_capabilities` | comma-separated task names | Fail closed when a required task or coverage evidence is unavailable. |
|
|
130
|
+
| `coverage_enforcement` | `auto`, `required`, `off` | Shared coverage-report policy. |
|
|
131
|
+
| `coverage_minimum` | `0`–`100` | Minimum percentage; defaults to `80`. |
|
|
132
|
+
| `coverage_metrics` | `lines`, `functions`, `branches`, `statements` | Metrics checked by the coverage gate. |
|
|
133
|
+
| `coverage_report` | comma-separated repository paths | Istanbul JSON summary or LCOV evidence files. |
|
|
134
|
+
| `pre_commit_build` | `true`, `false` | Run the project's build in the local pre-commit gate. Defaults to `false`; see [Pre-commit gate](#pre-commit-gate). |
|
|
135
|
+
| `filter_<task>` | comma-separated repository path globs | Skip a lane when no changed path matches; see [Lane path filters](#lane-path-filters). |
|
|
136
|
+
| `rust_sccache` | `true`, `false` | Install sccache and wrap rustc so compile artifacts survive manifest/lockfile churn. |
|
|
137
|
+
| `rust_nextest` | `true`, `false` | Install cargo-nextest and prefer `cargo nextest run` over `cargo test` in Rust test lanes. |
|
|
138
|
+
| `e2e_shards` | `1`–`8` | Fan the `Validation / Test / E2E` job into one runner per shard. The repository's `test:e2e`/`e2e` script receives `E2E_SHARD_INDEX` and `E2E_TOTAL_SHARDS` and partitions itself (Playwright takes `--shard=$E2E_SHARD_INDEX/$E2E_TOTAL_SHARDS`); each shard owns a runner-local test database, so shards stay isolated. |
|
|
139
|
+
| `release_batching_schedule` | five-field numeric cron | Batch Release Please into scheduled windows. Ordinary pushes skip the producer; release-PR squashes and manual dispatches still run. Batched releases are created privately as drafts and published with prerelease visibility atomically. Manual dispatches publish stable hotfixes. Absent, releases publish per merge. Requires a single root Release Please package. |
|
|
140
|
+
| `release_batching_prerelease` | `true`, `false` | Defaults to `true` with a schedule. Sync enables `draft` and `force-tag-creation` in Release Please; the producer publishes the draft with the selected visibility. `false` keeps scheduled releases stable-visible. |
|
|
141
|
+
| `release_batching_soak_hours` | `1`–`336` | Generate a daily promoter that promotes the newest prerelease when the oldest candidate since the last stable has soaked this many hours, stitching batch notes. Requires a batching schedule and prerelease publication. Set `STABLE_PROMOTION_HELD=true` in repository Actions variables to hold scheduled and manual promotion; `force` overrides the hold or soak, and `version` selects a plain semantic version. |
|
|
142
|
+
| `release_batching_qualification_workflow` | workflow filename (for example `release-assets.yml`) | Require the latest release-event run of this workflow for the candidate tag’s exact source commit to succeed before stable promotion. Requires a batching soak. Running, missing, failed, or cancelled runs hold promotion, including named and forced promotions; `force` only bypasses hold and soak. The promoter receives read-only Actions access. |
|
|
141
143
|
|
|
142
144
|
Supported task capabilities are `format`, `lint`, `type_check`, `build`, `unit`,
|
|
143
145
|
`integration`, `e2e`, `smoke`, `eval`, and `performance`. `coverage` is a policy
|
|
@@ -168,6 +170,36 @@ to keep discovery optional. Product-quality profiles are repository-owned
|
|
|
168
170
|
manifests invoked by existing build or E2E commands; they are not activated by a
|
|
169
171
|
configuration key. See [Product quality profiles](product-quality.md).
|
|
170
172
|
|
|
173
|
+
### Pre-commit gate
|
|
174
|
+
|
|
175
|
+
`code-foundry sync` installs `.githooks/pre-commit`, which runs
|
|
176
|
+
`code-foundry pre-commit` when the repository depends on `code-foundry`. The
|
|
177
|
+
gate is change-aware and never runs work for code the commit cannot affect:
|
|
178
|
+
|
|
179
|
+
- With nothing staged it exits immediately. `git diff --cached --check` still
|
|
180
|
+
blocks whitespace errors in every staged change.
|
|
181
|
+
- Oxfmt and Oxlint check only the staged files they support, honoring their
|
|
182
|
+
own ignore patterns. Staging `.oxfmtrc.json`, `.oxlintrc.json`, or their
|
|
183
|
+
`*.config.*` equivalents widens that tool to the whole repository through the
|
|
184
|
+
repository's `format:check`/`lint` script. A repository without Oxfmt or
|
|
185
|
+
Oxlint keeps its own script, which runs only when a matching file is staged.
|
|
186
|
+
- The project's `type-check`/`typecheck`/`type:check` script (or `tsc --noEmit`)
|
|
187
|
+
runs only when `.ts`, `.tsx`, `.mts`, `.cts`, or `tsconfig*.json` files are
|
|
188
|
+
staged, and for JavaScript sources when the repository type-checks them with
|
|
189
|
+
a root `jsconfig.json` or `checkJs`.
|
|
190
|
+
- Ruff checks only staged Python files (`--force-exclude`), or the whole
|
|
191
|
+
repository when `pyproject.toml`, `ruff.toml`, or a requirements file is
|
|
192
|
+
staged. Staged Rust sources or `Cargo.toml` run `cargo fmt --check` and
|
|
193
|
+
Clippy.
|
|
194
|
+
- The build does not run at commit time; CI and explicit commands own it. Set
|
|
195
|
+
`pre_commit_build: true` to run the `build` script (or `cargo build`) when
|
|
196
|
+
code is staged.
|
|
197
|
+
|
|
198
|
+
Tools read working-tree contents, so a partially staged file is checked as it
|
|
199
|
+
exists on disk. Task scripts that declare dependencies of their own, such as a
|
|
200
|
+
Turborepo `typecheck` task with `dependsOn: ["^build"]`, still run them; drop
|
|
201
|
+
that dependency to keep type checks build-free.
|
|
202
|
+
|
|
171
203
|
## Release and branch policy
|
|
172
204
|
|
|
173
205
|
| Key | Values | Purpose |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.44.0",
|
|
4
4
|
"description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
|
|
5
5
|
"homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
|
|
6
6
|
"bugs": {
|
package/src/commands/sync.mjs
CHANGED
|
@@ -754,6 +754,7 @@ on:
|
|
|
754
754
|
|
|
755
755
|
permissions:
|
|
756
756
|
contents: write
|
|
757
|
+
actions: read
|
|
757
758
|
|
|
758
759
|
concurrency:
|
|
759
760
|
group: promote-stable
|
|
@@ -769,6 +770,7 @@ jobs:
|
|
|
769
770
|
env:
|
|
770
771
|
GH_TOKEN: \${{ github.token }}
|
|
771
772
|
SOAK_HOURS: '__SOAK_HOURS__'
|
|
773
|
+
QUALIFICATION_WORKFLOW: '__QUALIFICATION_WORKFLOW__'
|
|
772
774
|
VERSION: \${{ inputs.version }}
|
|
773
775
|
FORCE: \${{ inputs.force }}
|
|
774
776
|
STABLE_PROMOTION_HELD: \${{ vars.STABLE_PROMOTION_HELD }}
|
|
@@ -777,6 +779,19 @@ jobs:
|
|
|
777
779
|
repo="$GITHUB_REPOSITORY"
|
|
778
780
|
summary="$GITHUB_STEP_SUMMARY"
|
|
779
781
|
|
|
782
|
+
# Force bypasses hold and soak, but never asset qualification.
|
|
783
|
+
require_qualification() {
|
|
784
|
+
[ -z "$QUALIFICATION_WORKFLOW" ] && return 0
|
|
785
|
+
local candidate_sha conclusion
|
|
786
|
+
candidate_sha="$(gh api "repos/$repo/commits/$1" --jq '.sha')" || return 1
|
|
787
|
+
[ -n "$candidate_sha" ] && [ "$candidate_sha" != "null" ] || return 1
|
|
788
|
+
conclusion="$(gh run list --repo "$repo" --workflow "$QUALIFICATION_WORKFLOW" --event release --commit "$candidate_sha" --limit 1 --json conclusion --jq '.[0].conclusion // "missing"')" || return 1
|
|
789
|
+
if [ "$conclusion" != "success" ]; then
|
|
790
|
+
echo "Asset qualification for $1 is $conclusion; holding stable promotion." | tee -a "$summary"
|
|
791
|
+
return 1
|
|
792
|
+
fi
|
|
793
|
+
}
|
|
794
|
+
|
|
780
795
|
# The repository variable applies equally to scheduled and manual runs.
|
|
781
796
|
if [ "$STABLE_PROMOTION_HELD" = "true" ] && [ "$FORCE" != "true" ]; then
|
|
782
797
|
echo "Stable promotion is held." | tee -a "$summary"
|
|
@@ -788,6 +803,10 @@ jobs:
|
|
|
788
803
|
echo "::error::version must be a plain semantic version" >&2
|
|
789
804
|
exit 1
|
|
790
805
|
fi
|
|
806
|
+
if ! require_qualification "v$VERSION"; then
|
|
807
|
+
echo "Qualification unavailable or unsuccessful; holding v$VERSION." | tee -a "$summary"
|
|
808
|
+
exit 0
|
|
809
|
+
fi
|
|
791
810
|
gh release edit "v$VERSION" --repo "$repo" --prerelease=false --latest
|
|
792
811
|
echo "Promoted v$VERSION to stable by name." | tee -a "$summary"
|
|
793
812
|
exit 0
|
|
@@ -833,6 +852,11 @@ jobs:
|
|
|
833
852
|
exit 0
|
|
834
853
|
fi
|
|
835
854
|
|
|
855
|
+
if ! require_qualification "$tip"; then
|
|
856
|
+
echo "Qualification unavailable or unsuccessful; holding $tip." | tee -a "$summary"
|
|
857
|
+
exit 0
|
|
858
|
+
fi
|
|
859
|
+
|
|
836
860
|
# Stitch the batch's notes oldest→newest so the stable release
|
|
837
861
|
# documents everything stable users are getting, not just the tip.
|
|
838
862
|
jq -r '.[] | "## " + .tag + "\\n\\n" + (.body // "")' <<<"$batch" > batch-notes.md
|
|
@@ -878,6 +902,16 @@ function validateReleaseBatchingConfig(config) {
|
|
|
878
902
|
`Unsupported release_batching_prerelease: ${config.release_batching_prerelease}; use true or false.`
|
|
879
903
|
)
|
|
880
904
|
}
|
|
905
|
+
if (config.release_batching_qualification_workflow) {
|
|
906
|
+
if (
|
|
907
|
+
!/^[A-Za-z0-9_.-]+\.ya?ml$/.test(config.release_batching_qualification_workflow) ||
|
|
908
|
+
!config.release_batching_soak_hours
|
|
909
|
+
) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
'Unsupported release_batching_qualification_workflow: use a workflow filename with a batching soak.'
|
|
912
|
+
)
|
|
913
|
+
}
|
|
914
|
+
}
|
|
881
915
|
if (config.release_batching_soak_hours) {
|
|
882
916
|
const hours = Number(config.release_batching_soak_hours)
|
|
883
917
|
if (!Number.isInteger(hours) || hours < 1 || hours > 336) {
|
|
@@ -902,7 +936,10 @@ function validateReleaseBatchingConfig(config) {
|
|
|
902
936
|
export function renderPromoteStable(config) {
|
|
903
937
|
const soakHours = Number(config.release_batching_soak_hours ?? '')
|
|
904
938
|
if (!Number.isInteger(soakHours) || soakHours < 1 || soakHours > 336) return null
|
|
905
|
-
return PROMOTE_STABLE_TEMPLATE.replaceAll('__SOAK_HOURS__', String(soakHours))
|
|
939
|
+
return PROMOTE_STABLE_TEMPLATE.replaceAll('__SOAK_HOURS__', String(soakHours)).replaceAll(
|
|
940
|
+
'__QUALIFICATION_WORKFLOW__',
|
|
941
|
+
config.release_batching_qualification_workflow ?? ''
|
|
942
|
+
)
|
|
906
943
|
}
|
|
907
944
|
|
|
908
945
|
/**
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change-aware planning for the local commit gate.
|
|
5
|
+
*
|
|
6
|
+
* The gate runs once per commit and blocks every agent or developer sharing a
|
|
7
|
+
* checkout, so it only does work the staged change can affect: format and lint
|
|
8
|
+
* the staged files, type-check only when a typed source or compiler config is
|
|
9
|
+
* staged, and leave the build to CI unless a repository opts back in with
|
|
10
|
+
* `pre_commit_build: true`. Repository-wide runs happen only when a staged file
|
|
11
|
+
* changes the tool's own configuration, because that can affect every file.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Files Oxfmt formats; anything else is left out of the staged file list. */
|
|
15
|
+
const FORMATTABLE =
|
|
16
|
+
/\.(?:[cm]?[jt]sx?|json[c5]?|mdx?|ya?ml|toml|css|scss|less|html?|vue|svelte|astro|graphql|gql|hbs|handlebars)$/i
|
|
17
|
+
/** Files Oxlint lints. */
|
|
18
|
+
const LINTABLE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/i
|
|
19
|
+
/** Typed sources and compiler configuration: the type-check trigger. */
|
|
20
|
+
const TYPED = /\.(?:ts|tsx|mts|cts)$|(?:^|\/)tsconfig[^/]*\.json$/i
|
|
21
|
+
/** JavaScript sources, type-checked only when the repository opts in with a root jsconfig. */
|
|
22
|
+
const JAVASCRIPT = /\.(?:js|jsx|mjs|cjs)$|(?:^|\/)jsconfig[^/]*\.json$/i
|
|
23
|
+
const FORMAT_CONFIG = /^(?:\.oxfmtrc\.jsonc?|oxfmt\.config\.[^/]+|\.prettierignore)$/
|
|
24
|
+
const LINT_CONFIG = /^(?:\.oxlintrc\.json|oxlint\.config\.[^/]+)$/
|
|
25
|
+
const PYTHON = /\.pyi?$/i
|
|
26
|
+
const PYTHON_CONFIG =
|
|
27
|
+
/^(?:pyproject\.toml|ruff\.toml|\.ruff\.toml)$|(?:^|\/)requirements[^/]*\.txt$/
|
|
28
|
+
const RUST = /\.rs$|(?:^|\/)Cargo\.toml$/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} PreCommitPlan
|
|
32
|
+
* @property {boolean} formatAll A formatter configuration file is staged.
|
|
33
|
+
* @property {string[]} formatFiles Staged files the formatter should check.
|
|
34
|
+
* @property {boolean} lintAll A linter configuration file is staged.
|
|
35
|
+
* @property {string[]} lintFiles Staged files the linter should check.
|
|
36
|
+
* @property {boolean} typeCheck Run the project's type-check command.
|
|
37
|
+
* @property {boolean} pythonAll A Python tool configuration file is staged.
|
|
38
|
+
* @property {string[]} pythonFiles Staged Python files.
|
|
39
|
+
* @property {boolean} rust Rust sources or manifests are staged.
|
|
40
|
+
* @property {boolean} build Run the project's build command.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {string[]} staged Repository-relative paths of staged files that still exist.
|
|
45
|
+
* @param {{checkJs?: boolean, build?: boolean}} [options]
|
|
46
|
+
* @returns {PreCommitPlan}
|
|
47
|
+
*/
|
|
48
|
+
export function planPreCommit(staged, options = {}) {
|
|
49
|
+
const formatAll = staged.some((file) => FORMAT_CONFIG.test(file))
|
|
50
|
+
const lintAll = staged.some((file) => LINT_CONFIG.test(file))
|
|
51
|
+
const pythonAll = staged.some((file) => PYTHON_CONFIG.test(file))
|
|
52
|
+
const formatFiles = staged.filter((file) => FORMATTABLE.test(file))
|
|
53
|
+
const lintFiles = staged.filter((file) => LINTABLE.test(file))
|
|
54
|
+
const pythonFiles = staged.filter((file) => PYTHON.test(file))
|
|
55
|
+
const typeCheck = staged.some(
|
|
56
|
+
(file) => TYPED.test(file) || (options.checkJs === true && JAVASCRIPT.test(file))
|
|
57
|
+
)
|
|
58
|
+
const rust = staged.some((file) => RUST.test(file))
|
|
59
|
+
const code =
|
|
60
|
+
typeCheck ||
|
|
61
|
+
lintFiles.length > 0 ||
|
|
62
|
+
pythonFiles.length > 0 ||
|
|
63
|
+
rust ||
|
|
64
|
+
staged.some((file) => /(?:^|\/)package\.json$/.test(file))
|
|
65
|
+
return {
|
|
66
|
+
formatAll,
|
|
67
|
+
formatFiles,
|
|
68
|
+
lintAll,
|
|
69
|
+
lintFiles,
|
|
70
|
+
typeCheck,
|
|
71
|
+
pythonAll,
|
|
72
|
+
pythonFiles,
|
|
73
|
+
rust,
|
|
74
|
+
build: options.build === true && code,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse `git diff --cached --name-only -z` output.
|
|
80
|
+
* @param {string} output
|
|
81
|
+
* @returns {string[]}
|
|
82
|
+
*/
|
|
83
|
+
export function parseStagedFiles(output) {
|
|
84
|
+
return output.split('\0').filter(Boolean)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the `pre_commit_build` configuration value. Building at commit time
|
|
89
|
+
* is opt-in: it serializes parallel work on a shared checkout and CI rebuilds
|
|
90
|
+
* every change anyway.
|
|
91
|
+
* @param {string|undefined} value
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
export function preCommitBuildEnabled(value) {
|
|
95
|
+
const resolved = value === undefined || value === '' ? 'false' : value
|
|
96
|
+
if (resolved !== 'true' && resolved !== 'false')
|
|
97
|
+
throw new Error(`Unsupported pre_commit_build: ${resolved}; use true or false.`)
|
|
98
|
+
return resolved === 'true'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Split a file list so one tool invocation never exceeds the platform's
|
|
103
|
+
* argument-length limit on very large commits.
|
|
104
|
+
* @param {string[]} files
|
|
105
|
+
* @param {number} [size]
|
|
106
|
+
* @returns {string[][]}
|
|
107
|
+
*/
|
|
108
|
+
export function chunkFiles(files, size = 200) {
|
|
109
|
+
/** @type {string[][]} */
|
|
110
|
+
const chunks = []
|
|
111
|
+
for (let index = 0; index < files.length; index += size)
|
|
112
|
+
chunks.push(files.slice(index, index + size))
|
|
113
|
+
return chunks
|
|
114
|
+
}
|
package/src/runtime-core.mjs
CHANGED
|
@@ -20,6 +20,12 @@ import { docsOnlyPullRequest } from './lib/docs-only.mjs'
|
|
|
20
20
|
import { readTaskFilters, resolveChangedPaths, taskAffected } from './lib/task-filters.mjs'
|
|
21
21
|
import { classifyValidationMode, evaluateValidationGate } from './lib/validation-policy.mjs'
|
|
22
22
|
import { readReleaseConfig, validateGeneratedReleaseDiff } from './lib/release-policy.mjs'
|
|
23
|
+
import {
|
|
24
|
+
chunkFiles,
|
|
25
|
+
parseStagedFiles,
|
|
26
|
+
planPreCommit,
|
|
27
|
+
preCommitBuildEnabled,
|
|
28
|
+
} from './lib/pre-commit.mjs'
|
|
23
29
|
import { runNodePackagePerformance } from './lib/node-package-performance.mjs'
|
|
24
30
|
import {
|
|
25
31
|
EVAL_BUDGET_FILE_DEFAULT,
|
|
@@ -1167,35 +1173,89 @@ function printProfile(command) {
|
|
|
1167
1173
|
writeOutput('npm_publish', config.npm_publish ?? 'false')
|
|
1168
1174
|
}
|
|
1169
1175
|
|
|
1176
|
+
/** Whether JavaScript sources are type-checked (a root jsconfig or `checkJs`). */
|
|
1177
|
+
function checksJavascript() {
|
|
1178
|
+
if (existsSync(resolve(root, 'jsconfig.json'))) return true
|
|
1179
|
+
return /"checkJs"\s*:\s*true/.test(readFileSafe(resolve(root, 'tsconfig.json')) ?? '')
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** @param {string} tool @param {string[]} args @param {string[]} files */
|
|
1183
|
+
function runToolOnFiles(tool, args, files) {
|
|
1184
|
+
for (const chunk of chunkFiles(files)) runTool(tool, [...args, ...chunk])
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1170
1187
|
/**
|
|
1171
1188
|
* The local commit gate.
|
|
1172
1189
|
*
|
|
1173
1190
|
* Exported so the generated `.githooks/pre-commit` can reach it: the hook is
|
|
1174
|
-
* what turns this into an actual gate
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1191
|
+
* what turns this into an actual gate. The generated hook is the
|
|
1192
|
+
* consumer-visible contract, so keep the two in step.
|
|
1193
|
+
*
|
|
1194
|
+
* The gate is change-aware: it formats and lints only staged files, runs the
|
|
1195
|
+
* project's type-check command only when typed sources or compiler config are
|
|
1196
|
+
* staged, and never builds unless `pre_commit_build: true` opts back in. The
|
|
1197
|
+
* full suite still runs in CI. Tools see working-tree contents, so a partially
|
|
1198
|
+
* staged file is checked as it exists on disk.
|
|
1177
1199
|
*/
|
|
1178
1200
|
export function preCommit() {
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1201
|
+
const staged = parseStagedFiles(
|
|
1202
|
+
capture('git', ['diff', '--cached', '--name-only', '-z', '--diff-filter=ACMR'])
|
|
1203
|
+
)
|
|
1204
|
+
// Deletions still go through the whitespace guard; they need no tool run.
|
|
1205
|
+
if (!staged.length && !capture('git', ['diff', '--cached', '--name-only'])) return
|
|
1181
1206
|
const check = spawnSync('git', ['diff', '--cached', '--check'], { cwd: root, stdio: 'inherit' })
|
|
1182
1207
|
if (check.status !== 0) process.exit(check.status ?? 1)
|
|
1183
|
-
if (
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1208
|
+
if (!staged.length) return
|
|
1209
|
+
|
|
1210
|
+
const plan = planPreCommit(staged, {
|
|
1211
|
+
checkJs: checksJavascript(),
|
|
1212
|
+
build: preCommitBuildEnabled(config.pre_commit_build),
|
|
1213
|
+
})
|
|
1214
|
+
const js = (hasLanguage('typescript') || hasLanguage('javascript')) && hasRootJavascriptProject()
|
|
1215
|
+
|
|
1216
|
+
if (js && (plan.formatAll || plan.formatFiles.length)) {
|
|
1217
|
+
if (plan.formatAll) {
|
|
1218
|
+
if (!runScript(['format:check', 'format', 'fmt']) && hasOxfmtSetup())
|
|
1219
|
+
runTool('oxfmt', ['--check', '.'])
|
|
1220
|
+
} else if (hasOxfmtSetup()) {
|
|
1221
|
+
runToolOnFiles('oxfmt', ['--check', '--no-error-on-unmatched-pattern'], plan.formatFiles)
|
|
1222
|
+
} else if (plan.formatFiles.some((file) => /\.(?:[cm]?[jt]sx?|json|mdx?|ya?ml)$/.test(file))) {
|
|
1223
|
+
// A repository-owned formatter cannot be scoped generically; its script
|
|
1224
|
+
// still runs, but only when a file it plausibly owns is staged.
|
|
1225
|
+
runScript(['format:check', 'format', 'fmt'])
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (js && (plan.lintAll || plan.lintFiles.length)) {
|
|
1229
|
+
if (plan.lintAll) {
|
|
1230
|
+
if (!runScript(['lint']) && hasOxlintSetup()) runTool('oxlint', ['--deny-warnings'])
|
|
1231
|
+
} else if (hasOxlintSetup()) {
|
|
1232
|
+
runToolOnFiles(
|
|
1233
|
+
'oxlint',
|
|
1234
|
+
['--deny-warnings', '--no-error-on-unmatched-pattern'],
|
|
1235
|
+
plan.lintFiles
|
|
1236
|
+
)
|
|
1237
|
+
} else {
|
|
1238
|
+
runScript(['lint'])
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (js && plan.typeCheck) {
|
|
1242
|
+
// Use the project's own command so project references, workspaces, and
|
|
1243
|
+
// path aliases resolve exactly as they do in CI.
|
|
1244
|
+
const scripted = runScript(['type-check', 'typecheck', 'type:check'])
|
|
1245
|
+
if (!scripted && existsSync(resolve(root, 'tsconfig.json'))) runTool('tsc', ['--noEmit'])
|
|
1190
1246
|
}
|
|
1191
|
-
if (
|
|
1247
|
+
if (plan.rust) {
|
|
1192
1248
|
run('cargo', ['fmt', '--check'])
|
|
1193
1249
|
run('cargo', ['clippy', '--all-targets', '--', '-D', 'warnings'])
|
|
1194
1250
|
}
|
|
1195
|
-
if (
|
|
1251
|
+
if (plan.pythonAll) {
|
|
1196
1252
|
runTool('ruff', ['format', '--check', '.'])
|
|
1197
1253
|
runTool('ruff', ['check', '.'])
|
|
1254
|
+
} else if (plan.pythonFiles.length) {
|
|
1255
|
+
runToolOnFiles('ruff', ['format', '--check', '--force-exclude'], plan.pythonFiles)
|
|
1256
|
+
runToolOnFiles('ruff', ['check', '--force-exclude'], plan.pythonFiles)
|
|
1198
1257
|
}
|
|
1258
|
+
if (plan.build) ci('build')
|
|
1199
1259
|
}
|
|
1200
1260
|
|
|
1201
1261
|
// Only dispatch when this module is the process entry point. The CLI imports
|