Skip to content

Commit 8ff84bf

Browse files
committed
ci: add focused Windows source contract
Run a Windows Node 24 contract on non-documentation pull requests while keeping the full capability suite on Linux and macOS. Share the gate through the verifier and Vitest suite manifest, keep the compatibility matrix macOS/Linux, and document the Windows-only boundary.
1 parent f35ffba commit 8ff84bf

10 files changed

Lines changed: 100 additions & 22 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,16 @@ jobs:
5555
strategy:
5656
fail-fast: false
5757
matrix:
58-
# Windows validation is temporarily paused until its checks are reliable.
59-
os: [ubuntu-latest, macos-latest]
58+
# Windows runs a focused contract; full capability coverage remains on Linux/macOS.
59+
os: [ubuntu-latest, macos-latest, windows-latest]
6060
node: ["24"]
6161
include:
6262
- os: ubuntu-latest
6363
node: "24"
6464
profile: full
65+
- os: windows-latest
66+
node: "24"
67+
profile: windows
6568
runs-on: ${{ matrix.os }}
6669
steps:
6770
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

‎CONTRIBUTING.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ pnpm install --frozen-lockfile
3737
pnpm verify
3838
```
3939

40-
`pnpm verify` runs the complete gate list in the same order as GitHub CI. Normal PR and main-branch checks use Node.js 24 on Linux and macOS. Windows CI is temporarily paused while its checks are made reliable. The Linux job runs this full profile; the macOS job uses `pnpm verify --profile platform`, which omits only the duplicate TypeScript compiler check. Both jobs still export source, build, and test their own platform artifacts. Gates that depend on platform behaviour are selected by platform rather than skipped silently; run `pnpm verify --list` or `pnpm verify --profile platform --list` to inspect either plan. Individual gates remain available as their own scripts, such as `pnpm typecheck` or `pnpm test:byok`, while you iterate.
40+
`pnpm verify` runs the complete gate list in the same order as GitHub CI. Normal PR and main-branch checks use Node.js 24 on Linux and macOS. The Linux job runs the full profile; the macOS job uses `pnpm verify --profile platform`, which omits only the duplicate TypeScript compiler check. Windows runs the focused `pnpm verify --profile windows` contract on PRs; the profile is Windows-only and fails closed elsewhere. It checks source inventory, release tooling, build boundaries, artifacts, and Windows-specific tests without running the full capability suite. Gates that depend on platform behaviour are selected by platform rather than skipped silently; run `pnpm verify --list`, `pnpm verify --profile platform --list`, or `pnpm verify --profile windows --list` to inspect each plan. Individual gates remain available as their own scripts, such as `pnpm typecheck` or `pnpm test:byok`, while you iterate.
4141

4242
CI writes per-gate timing and exit metadata to the Job Summary and a seven-day `verification-<os>-node-<version>-<attempt>` artifact. For a local report, set `MCODE_VERIFY_REPORT_DIR` to a directory outside the repository. Reports distinguish `PASS`, `FAIL`, intentional `SKIP`, and `NOT_RUN` after a failure. JSON is checkpointed before and after each gate; a cancelled run may leave `RUNNING`, which is not a pass. If installation fails before verification starts, no verification report is available. Reports do not collect command output, environment variables, or runtime data; read the corresponding gate's job log for failure details, including the existing bounded BYOK timeout diagnostics. CI jobs have a 15-minute verification limit and a 10-minute release-audit limit.
4343

4444
Existing README files, `CONTRIBUTING.md`, `.github/PULL_REQUEST_TEMPLATE.md`, Markdown under `docs/`, and media directly under `docs/assets/` use the `docs` profile when they are the only changed paths. That profile checks the source inventory and generated paths, exports the committed source, and tests release tooling. History and source-snapshot secret scans still run; platform builds and distribution scans are skipped. Mixed changes, unknown paths, missing comparisons, and any `release/` inventory change get full CI. Documentation-only changes skip the platform matrix entirely. The `verification` aggregate check always runs and rejects failed, cancelled, or unexpectedly skipped jobs. Use it together with `source-history-artifact` as required checks when configuring branch protection; this repository's automation does not change administrative settings.
4545

46-
`Node compatibility` runs weekly and on demand against macOS and Linux with Node 22.19.0, 24.2.0, 25 and 26. Windows is also temporarily excluded from this matrix. It does not run automatically on PRs. Dispatch it on the selected branch for changes to supported Node versions, native dependencies or compatibility-sensitive verification tooling, and before a source release. This covers the minimum versions of the two supported ranges and the additional supported majors. Deferring those versions from ordinary PR checks can delay regression discovery; a known failure in a supported version still needs resolution before release. Dependabot proposes weekly Actions and npm updates, grouping Actions and development-tool minor/patch updates. External Actions use reviewed full commit SHAs, while local actions and reusable workflows come from the same checked-out revision.
46+
`Node compatibility` runs weekly and on demand against macOS and Linux with Node 22.19.0, 24.2.0, 25 and 26. Windows remains excluded from this matrix; the PR Windows contract is a focused Node 24 check, not a substitute for the full compatibility matrix. It does not run automatically on PRs. Dispatch it on the selected branch for changes to supported Node versions, native dependencies or compatibility-sensitive verification tooling, and before a source release. This covers the minimum versions of the two supported ranges and the additional supported majors. Deferring those versions from ordinary PR checks can delay regression discovery; a known failure in a supported version still needs resolution before release. Dependabot proposes weekly Actions and npm updates, grouping Actions and development-tool minor/patch updates. External Actions use reviewed full commit SHAs, while local actions and reusable workflows come from the same checked-out revision.
4747

4848
Source candidates are requested independently through the `Source candidate` workflow; ordinary PRs and main pushes do not produce them. Its Linux/macOS archive validation is described in [Releasing](docs/releasing.md). `CLI release` builds npm-installable tar.gz packages from version tags and validates the same archive before attaching it to a GitHub Release. Its `package` verification profile tests installation of an existing archive; it does not replace full source verification. npm registry and official installer publication remain separate release processes.
4949

‎docs/releasing.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Before npm or installer distribution, separately validate the published package
8787

8888
The `Source candidate` workflow exports the selected commit without Git history, verifies its receipt, and scans both repository history and the extracted source. Linux and macOS runners authenticate the same archive, install from public npm into fresh stores, and run the archive verification profile.
8989

90-
Windows validation is temporarily paused across source verification, Node compatibility, and source candidates. Candidate reports cover only Linux and macOS; a successful candidate does not establish Windows acceptance. Restore the Windows workflow matrices and the required report set in `scripts/source-candidate.mjs` together when Windows checks are reliable again.
90+
Windows full validation is temporarily paused for Node compatibility and source candidates. Ordinary pull requests run a focused Windows source-verification contract, but candidate reports still cover only Linux and macOS; a successful candidate does not establish Windows acceptance. Restore the Windows compatibility/source-candidate matrices and the required report set in `scripts/source-candidate.mjs` together when those checks are reliable again.
9191

9292
After both platform jobs pass, the workflow creates a `source-candidate-<full-SHA>` artifact containing:
9393

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"test:status-contract": "node scripts/run-vitest-suite.mjs status-contract",
2626
"check:source": "node scripts/source-inventory.mjs",
2727
"test:capabilities": "node scripts/run-vitest-suite.mjs capability",
28+
"test:windows": "node scripts/run-vitest-suite.mjs windows",
2829
"test:artifact": "node --test test/public-artifact.test.mjs",
2930
"test:release-tools": "node --test test/source-sync.test.mjs"
3031
},

‎release/public-source.json‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎scripts/check-windows-source-location.mjs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,18 @@ export function checkWindowsSourceLocation({
2727
if (!/^[a-z]:\\$/iu.test(root) || root.startsWith("\\\\")) {
2828
return fail("The checkout root is not a local drive-letter path.");
2929
}
30+
// `fsutil` accepts a drive letter more reliably than a root path with a
31+
// trailing backslash across Windows runner images.
32+
const volume = root.slice(0, 2);
3033

3134
let driveType;
3235
let volumeInfo;
3336
try {
34-
driveType = execFile("fsutil", ["fsinfo", "drivetype", root], {
37+
driveType = execFile("fsutil", ["fsinfo", "drivetype", volume], {
3538
encoding: "utf8",
3639
stdio: ["ignore", "pipe", "pipe"],
3740
});
38-
volumeInfo = execFile("fsutil", ["fsinfo", "volumeinfo", root], {
41+
volumeInfo = execFile("fsutil", ["fsinfo", "volumeinfo", volume], {
3942
encoding: "utf8",
4043
stdio: ["ignore", "pipe", "pipe"],
4144
});

‎scripts/verify.mjs‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ const { values } = parseArgs({
2525
},
2626
});
2727
const profile = values.profile;
28-
if (!["full", "platform", "docs", "archive", "package"].includes(profile))
28+
if (!["full", "platform", "windows", "docs", "archive", "package"].includes(profile))
2929
throw new Error(`Unknown verification profile: ${profile}`);
30+
if (profile === "windows" && process.platform !== "win32")
31+
throw new Error("Windows verification profile requires a Windows host");
3032
if (profile === 'package' && !['darwin', 'linux'].includes(process.platform))
3133
throw new Error('Package verification currently supports Linux and macOS only.');
3234
// Listing must not leave a temporary export directory behind.
@@ -36,22 +38,24 @@ const temporary = values.list
3638
const preview = path.join(temporary ?? tmpdir(), "minimax-code-source.tar.gz");
3739

3840
const steps = [
39-
{ name: "check:source", script: "check:source", docs: true },
40-
{ name: "check:tsconfig", script: "check:tsconfig", docs: true },
41+
{ name: "check:source", script: "check:source", docs: true, windows: true },
42+
{ name: "check:tsconfig", script: "check:tsconfig", docs: true, windows: true },
4143
{
4244
name: "export source preview",
4345
docs: true,
46+
windows: true,
4447
requiresGit: true,
4548
command: ["scripts/export-source-preview.mjs", "--out", preview],
4649
},
47-
{ name: "test:release-tools", script: "test:release-tools", docs: true },
50+
{ name: "test:release-tools", script: "test:release-tools", docs: true, windows: true },
4851
// Compiler inputs are identical across the matrix. One Linux job runs this;
4952
// all platforms still build and validate native artifacts on their own platform.
5053
{ name: "typecheck", script: "typecheck", fullOnly: true },
51-
{ name: "build", script: "build" },
52-
{ name: "check:standalone", script: "check:standalone" },
53-
{ name: "test:artifact", script: "test:artifact" },
54+
{ name: "build", script: "build", windows: true },
55+
{ name: "check:standalone", script: "check:standalone", windows: true },
56+
{ name: "test:artifact", script: "test:artifact", windows: true },
5457
{ name: "test:capabilities", script: "test:capabilities" },
58+
{ name: "test:windows", script: "test:windows", platforms: ["win32"], windows: true },
5559
{ name: "test:status-contract", script: "test:status-contract" },
5660
{ name: "test:smoke", script: "test:smoke" },
5761
{ name: "test:byok", script: "test:byok" },
@@ -74,6 +78,7 @@ const steps = [
7478
function skipReason(step) {
7579
if (profile === 'package' && !step.packageOnly) return 'validating an npm release archive';
7680
if (profile !== 'package' && step.packageOnly) return 'requires an npm release archive';
81+
if (profile === "windows" && !step.windows) return "not part of Windows contract";
7782
if (profile === "docs" && !step.docs) return "documentation-only change";
7883
if (profile === "archive" && step.requiresGit)
7984
return "validating an already exported archive";

‎test/source-sync.test.mjs‎

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ test('Windows source preflight requires a local NTFS checkout', () => {
4444
checkWindowsSourceLocation({ platform: 'win32', cwd: 'C:\\repo', execFile }),
4545
{ ok: true, skipped: false },
4646
);
47-
assert.deepEqual(calls.map(([command, args]) => [command, args[1]]), [
48-
['fsutil', 'drivetype'],
49-
['fsutil', 'volumeinfo'],
47+
assert.deepEqual(calls.map(([command, args]) => [command, args[1], args[2]]), [
48+
['fsutil', 'drivetype', 'C:'],
49+
['fsutil', 'volumeinfo', 'C:'],
5050
]);
5151
});
5252

@@ -740,15 +740,18 @@ test('CI aggregate rejects failed, cancelled, missing and unexpectedly skipped c
740740
assert.notEqual(run({ ...full, DOCS_ONLY: scope }), 0);
741741
});
742742

743-
test('ordinary CI pauses Windows without invoking release-only matrices', () => {
743+
test('ordinary CI runs a focused Windows contract while compatibility remains macOS/Linux', () => {
744744
const readWorkflow = name => parseYaml(readFileSync(new URL(`../.github/workflows/${name}.yml`, import.meta.url), 'utf8'));
745745
const ci = readWorkflow('ci');
746746
assert.ok(Object.hasOwn(ci.on, 'pull_request'));
747747
assert.deepEqual(ci.on.push.branches, ['main']);
748748
assert.deepEqual(Object.keys(ci.jobs).sort(), ['changes', 'docs', 'verification', 'verify']);
749-
assert.deepEqual(ci.jobs.verify.strategy.matrix.os, ['ubuntu-latest', 'macos-latest']);
749+
assert.deepEqual(ci.jobs.verify.strategy.matrix.os, ['ubuntu-latest', 'macos-latest', 'windows-latest']);
750750
assert.deepEqual(ci.jobs.verify.strategy.matrix.node, ['24']);
751-
assert.deepEqual(ci.jobs.verify.strategy.matrix.include, [{ os: 'ubuntu-latest', node: '24', profile: 'full' }]);
751+
assert.deepEqual(ci.jobs.verify.strategy.matrix.include, [
752+
{ os: 'ubuntu-latest', node: '24', profile: 'full' },
753+
{ os: 'windows-latest', node: '24', profile: 'windows' },
754+
]);
752755
assert.equal(ci.jobs.verify.needs, 'changes');
753756
assert.equal(ci.jobs.verify.if, "needs.changes.outputs.docs_only == 'false'");
754757
assert.equal(ci.jobs.docs.if, "needs.changes.outputs.docs_only == 'true'");
@@ -758,12 +761,11 @@ test('ordinary CI pauses Windows without invoking release-only matrices', () =>
758761
const compatibility = readWorkflow('compatibility');
759762
assert.deepEqual(Object.keys(compatibility.on).sort(), ['schedule', 'workflow_dispatch']);
760763
assert.deepEqual(compatibility.jobs.compatibility.strategy.matrix.node, ['22.19.0', '24.2.0', '25', '26']);
761-
assert.deepEqual(compatibility.jobs.compatibility.strategy.matrix.os, ci.jobs.verify.strategy.matrix.os);
764+
assert.deepEqual(compatibility.jobs.compatibility.strategy.matrix.os, ['ubuntu-latest', 'macos-latest']);
762765
const audit = readWorkflow('security');
763766
assert.ok(Object.hasOwn(audit.on, 'pull_request'));
764767
assert.ok(audit.jobs['source-history-artifact'].steps.some(step => step.run?.includes('gitleaks dir dist')));
765768
});
766-
767769
test('manual source candidates pin every checkout and receipt to the selected revision', () => {
768770
const workflow = parseYaml(readFileSync(new URL('../.github/workflows/source-candidate.yml', import.meta.url), 'utf8'));
769771
assert.deepEqual(Object.keys(workflow.on).sort(), ['workflow_call', 'workflow_dispatch']);
@@ -891,6 +893,47 @@ test('candidate rejects mismatched receipts and requires successful same-revisio
891893
});
892894

893895

896+
test('Windows contract profile fails closed off Windows', () => {
897+
const result = spawnSync(process.execPath, ['scripts/verify.mjs', '--profile', 'windows', '--list'], {
898+
cwd: path.resolve('.'),
899+
encoding: 'utf8',
900+
});
901+
if (process.platform === 'win32') {
902+
assert.equal(result.status, 0, result.stderr);
903+
} else {
904+
assert.notEqual(result.status, 0);
905+
assert.match(result.stderr, /requires a Windows host/);
906+
}
907+
});
908+
909+
test('Windows contract profile selects focused gates', () => {
910+
const root = mkdtempSync(path.join(tmpdir(), 'windows-profile-'));
911+
try {
912+
const fixture = path.join(root, 'verify.mjs');
913+
copyFileSync(new URL('../scripts/verify.mjs', import.meta.url), fixture);
914+
const preload = path.join(root, 'platform.cjs');
915+
writeFileSync(preload, "Object.defineProperty(process, 'platform', { value: 'win32' });\n");
916+
const result = spawnSync(process.execPath, ['--require', preload, fixture, '--profile', 'windows', '--list'], {
917+
cwd: root,
918+
encoding: 'utf8',
919+
env: { ...process.env },
920+
});
921+
assert.equal(result.status, 0, result.stderr);
922+
assert.deepEqual(result.stdout.trim().split('\n'), [
923+
'check:source',
924+
'check:tsconfig',
925+
'export source preview',
926+
'test:release-tools',
927+
'build',
928+
'check:standalone',
929+
'test:artifact',
930+
'test:windows',
931+
]);
932+
} finally {
933+
rmSync(root, { recursive: true, force: true });
934+
}
935+
});
936+
894937
test('source inventory rejects unregistered, missing and duplicate first-party test gates', () => {
895938
const existing = 'packages/example/src/existing.test.ts';
896939
const omitted = 'packages/example/test/omitted.spec.tsx';

‎test/vitest-suites.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"comment": "Vitest suites for the standalone distribution, grouped by the gate that runs them. `vitest.oss.config.mjs` includes every group; `scripts/run-vitest-suite.mjs <group>` runs one. Add a test file here rather than in package.json or the Vitest config.",
33
"suites": {
4+
"windows": [
5+
"test/windows-contract.test.mjs"
6+
],
47
"capability": [
58
"packages/agent-core/test/unit/bash-subprocess-env.test.ts",
69
"packages/agent-modules/permission/test/unit/permission/bash-policy-regressions.test.ts",

‎test/windows-contract.test.mjs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import assert from "node:assert/strict";
2+
import { describe, it } from "vitest";
3+
import { checkWindowsSourceLocation } from "../scripts/check-windows-source-location.mjs";
4+
import { resolveWslPath } from "../packages/tui/src/host/wsl-path.js";
5+
6+
const windowsPath = String.raw`D:\Users\demo\Documents\Screen shots\截图.png`;
7+
8+
describe.skipIf(process.platform !== "win32")("Windows source contract", () => {
9+
it("accepts the Windows checkout on a local NTFS volume", () => {
10+
assert.deepEqual(checkWindowsSourceLocation(), {
11+
ok: true,
12+
skipped: false,
13+
});
14+
});
15+
16+
it("preserves Windows path syntax on the native host", async () => {
17+
assert.equal(await resolveWslPath(windowsPath), windowsPath);
18+
});
19+
});

0 commit comments

Comments
 (0)