Skip to content
Merged
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
38 changes: 33 additions & 5 deletions .github/workflows/windows-baseline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,31 @@ permissions:
contents: read

jobs:
changes:
runs-on: ubuntu-latest
outputs:
windows: ${{ steps.plan.outputs.windows }}
windows_runtime: ${{ steps.plan.outputs.windows_runtime }}
windows_storage: ${{ steps.plan.outputs.windows_storage }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- id: plan
name: Select affected Windows surfaces
env:
BASE_SHA: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event_name == 'push' && github.sha || github.event.pull_request.head.sha }}
run: |
if [[ "$BASE_SHA" =~ ^0+$ ]] || ! git cat-file -e "${BASE_SHA}^{commit}"; then
node scripts/ci-test-plan.mjs --full >> "$GITHUB_OUTPUT"
else
node scripts/ci-test-plan.mjs --base "$BASE_SHA" --head "$HEAD_SHA" >> "$GITHUB_OUTPUT"
fi

windows_baseline:
needs: changes
if: needs.changes.outputs.windows == 'true'
name: windows_baseline (non-blocking)
runs-on: windows-latest
timeout-minutes: 45
Expand Down Expand Up @@ -101,18 +125,22 @@ jobs:

- id: smoke
name: Run CLI and Electron startup smoke
if: always() && steps.install.outcome == 'success'
if: always() && steps.build.outcome == 'success'
continue-on-error: true
shell: pwsh
run: |
npm.cmd run smoke:windows *> "$env:WINDOWS_BASELINE_LOG_DIR/smoke.log"
npm.cmd --workspace @maka/desktop run build:smoke *> "$env:WINDOWS_BASELINE_LOG_DIR/smoke-build.log"
$exitCode = $LASTEXITCODE
Get-Content "$env:WINDOWS_BASELINE_LOG_DIR/smoke-build.log"
if ($exitCode -ne 0) { exit $exitCode }
npm.cmd run smoke:windows:dist *> "$env:WINDOWS_BASELINE_LOG_DIR/smoke.log"
$exitCode = $LASTEXITCODE
Get-Content "$env:WINDOWS_BASELINE_LOG_DIR/smoke.log"
exit $exitCode

- id: runtime_pty_input
name: Run Runtime PTY input gates
if: always() && steps.build.outcome == 'success'
if: always() && steps.build.outcome == 'success' && needs.changes.outputs.windows_runtime == 'true'
continue-on-error: true
timeout-minutes: 5
shell: pwsh
Expand All @@ -129,7 +157,7 @@ jobs:

- id: storage
name: Capture storage test baseline
if: always() && steps.build.outcome == 'success'
if: always() && steps.build.outcome == 'success' && needs.changes.outputs.windows_storage == 'true'
continue-on-error: true
shell: pwsh
run: |
Expand All @@ -140,7 +168,7 @@ jobs:

- id: managed_workspace_crash
name: Run managed workspace crash recovery gates
if: always() && steps.build.outcome == 'success'
if: always() && steps.build.outcome == 'success' && needs.changes.outputs.windows_storage == 'true'
continue-on-error: true
shell: pwsh
run: |
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"build": "npm run build:resources && npm run build:main && npm run build:preload && npm run build:overlay && npm run build:renderer",
"build:resources": "node scripts/copy-runtime-filesystem-worker.mjs",
"build:test": "npm run build:main && npm run build:preload && npm run build:overlay",
"build:smoke": "npm run build:resources && npm run build:renderer",
"clean:main": "node ../../scripts/clean-paths.mjs dist/main tsconfig.main.tsbuildinfo",
"build:main": "tsc -p tsconfig.main.json",
"build:preload": "esbuild src/preload/preload.ts --bundle --platform=node --format=cjs --outfile=dist/preload/preload.cjs --external:electron",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
"prepare:maka-cu": "node scripts/prepare-maka-cu.mjs",
"windows:inventory": "node scripts/windows-test-inventory.mjs --check",
"windows:inventory:write": "node scripts/windows-test-inventory.mjs --write",
"smoke:windows": "npm run build && node scripts/windows-smoke.mjs",
"smoke:windows": "npm run build && npm run smoke:windows:dist",
"smoke:windows:dist": "node scripts/windows-smoke.mjs",
"prepare:bundled-git": "node scripts/prepare-bundled-git.mjs",
"prepare:bundled-git-source": "node scripts/prepare-bundled-git-source.mjs",
"e2e:computer-use-real": "node scripts/cu-real-ax-model-e2e-launcher.mjs",
Expand Down
43 changes: 31 additions & 12 deletions scripts/ci-test-plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ const TYPECHECK_ONLY_FILES = new Set([
'tsconfig.lib.json',
]);

const WINDOWS_BASELINE_FILES = new Set([
'.github/workflows/windows-baseline.yml',
'scripts/windows-baseline-workflow.test.mjs',
'scripts/windows-process-identity.ps1',
'scripts/windows-smoke.mjs',
'scripts/windows-smoke.test.mjs',
]);

const DEDICATED_WORKSPACE_LANES = new Set(['packages/runtime-host', 'packages/headless']);

// Scripts the Electron e2e job runs. Editing one of these changes what that
Expand Down Expand Up @@ -219,6 +227,9 @@ export function planTests(changedFiles, options = {}) {
// every unrelated merge into a 10K-chunk pressure run.
storageStress: false,
storybook: true,
windows: true,
windowsRuntime: true,
windowsStorage: true,
workspaces,
...workspaceLanes(workspaces),
};
Expand Down Expand Up @@ -278,6 +289,10 @@ export function planTests(changedFiles, options = {}) {

const workspaces = reverseDependencyClosure(directWorkspaces, graph);
const storageStress = files.some((path) => STORAGE_STRESS_FILES.has(path));
const windowsBaselineChanged = files.includes('.github/workflows/windows-baseline.yml');
const windows = code || files.some((path) => WINDOWS_BASELINE_FILES.has(path));
const windowsRuntime = windowsBaselineChanged || workspaces.includes('packages/runtime');
const windowsStorage = windowsBaselineChanged || workspaces.includes('packages/storage');

return {
code,
Expand All @@ -297,6 +312,9 @@ export function planTests(changedFiles, options = {}) {
// PR — product ship gates are typecheck, unit, and Electron e2e. See
// isStorybookPath.
storybook: files.some((path) => isStorybookPath(path)),
windows,
windowsRuntime,
windowsStorage,
workspaces,
...workspaceLanes(workspaces),
};
Expand All @@ -314,6 +332,9 @@ export function formatGitHubOutputs(plan) {
`storage_stress=${plan.storageStress}`,
`storybook=${plan.storybook}`,
`unit=${plan.workspaces.length > 0}`,
`windows=${plan.windows}`,
`windows_runtime=${plan.windowsRuntime}`,
`windows_storage=${plan.windowsStorage}`,
`standard_workspaces=${plan.standardWorkspaces.join(',')}`,
`workspaces=${plan.workspaces.join(',')}`,
].join('\n');
Expand All @@ -334,20 +355,18 @@ function parseArgs(args) {
return parsed;
}

export function changedFilesBetween(base, head, exec = execFileSync) {
return exec('git', ['diff', '--no-renames', '--name-only', '--diff-filter=ACMRD', base, head], {
cwd: defaultRepoRoot,
encoding: 'utf8',
})
.split('\n')
.filter(Boolean);
}

function main(args) {
const parsed = parseArgs(args);
const changedFiles = parsed.forceFull
? []
: execFileSync(
'git',
['diff', '--name-only', '--diff-filter=ACMRD', parsed.base, parsed.head],
{
cwd: defaultRepoRoot,
encoding: 'utf8',
},
)
.split('\n')
.filter(Boolean);
const changedFiles = parsed.forceFull ? [] : changedFilesBetween(parsed.base, parsed.head);
const plan = planTests(changedFiles, { forceFull: parsed.forceFull });
process.stdout.write(`${formatGitHubOutputs(plan)}\n`);
process.stderr.write(
Expand Down
59 changes: 58 additions & 1 deletion scripts/ci-test-plan.test.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { formatGitHubOutputs, loadWorkspaceGraph, planTests } from './ci-test-plan.mjs';
import {
changedFilesBetween,
formatGitHubOutputs,
loadWorkspaceGraph,
planTests,
} from './ci-test-plan.mjs';

const graph = loadWorkspaceGraph();

test('impact planning distinguishes docs, UI, and backend changes', () => {
const docs = planTests(['README.md', 'docs/testing.md'], { graph });
assert.equal(docs.code, false);
assert.equal(docs.windows, false);
assert.deepEqual(docs.workspaces, []);

const ui = planTests(['packages/ui/src/button.tsx'], { graph });
assert.equal(ui.e2e, true);
assert.equal(ui.windows, true);
assert.equal(ui.windowsRuntime, false);
assert.equal(ui.windowsStorage, false);
// Product UI is mounted by the catalog. Runtime export and render changes can
// break Storybook even when its story files themselves are unchanged.
assert.equal(ui.storybook, true);
Expand Down Expand Up @@ -73,13 +82,58 @@ test('impact planning distinguishes docs, UI, and backend changes', () => {
assert.equal(alignment.storybook, false);

const backend = planTests(['packages/storage/src/session-store.ts'], { graph });
assert.equal(backend.windowsRuntime, true);
assert.equal(backend.windowsStorage, true);
assert.equal(backend.e2e, false);
assert.equal(backend.storybook, false);
for (const workspace of ['packages/storage', 'packages/runtime', 'apps/desktop']) {
assert.ok(backend.workspaces.includes(workspace));
}
});

test('Windows planning runs workflow changes fully and helper changes narrowly', () => {
const workflow = planTests(['.github/workflows/windows-baseline.yml'], { graph });
assert.equal(workflow.windows, true);
assert.equal(workflow.windowsRuntime, true);
assert.equal(workflow.windowsStorage, true);

for (const path of [
'scripts/windows-baseline-workflow.test.mjs',
'scripts/windows-process-identity.ps1',
'scripts/windows-smoke.mjs',
]) {
const plan = planTests([path], { graph });
assert.equal(plan.windows, true, path);
assert.equal(plan.windowsRuntime, false, path);
assert.equal(plan.windowsStorage, false, path);
}
});

test('cross-surface renames retain both paths for impact planning', () => {
let invocation;
const files = changedFilesBetween('base-sha', 'head-sha', (command, args, options) => {
invocation = { command, args, options };
return 'packages/storage/src/legacy.ts\napps/desktop/src/main/replacement.ts\n';
});

assert.deepEqual(files, [
'packages/storage/src/legacy.ts',
'apps/desktop/src/main/replacement.ts',
]);
assert.deepEqual(invocation.args, [
'diff',
'--no-renames',
'--name-only',
'--diff-filter=ACMRD',
'base-sha',
'head-sha',
]);

const plan = planTests(files, { graph });
assert.equal(plan.windows, true);
assert.equal(plan.windowsStorage, true);
});

test('stress and specialized script checks run only for their owning surfaces', () => {
for (const path of [
'packages/storage/src/agent-run-store.ts',
Expand Down Expand Up @@ -192,6 +246,9 @@ test('heavy workspaces are projected onto dedicated CI lanes', () => {
assert.ok(outputs.includes('runtime_host=true'));
assert.ok(outputs.includes('headless=false'));
assert.ok(outputs.includes('standard_workspaces=packages/cli,apps/desktop'));
assert.ok(outputs.includes('windows=true'));
assert.ok(outputs.includes('windows_runtime=false'));
assert.ok(outputs.includes('windows_storage=false'));
});

test('global and unknown production changes fail safe to the complete suite', () => {
Expand Down
13 changes: 11 additions & 2 deletions scripts/windows-baseline-workflow.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,14 @@ test('Windows baseline workflow keeps its non-blocking evidence contract', async
assert.match(workflow, /^\s+runs-on: windows-latest$/mu);
assert.match(workflow, /^\s+continue-on-error: true$/mu);
assert.match(workflow, /^\s+timeout-minutes: 45$/mu);
assert.match(workflow, /^\s+needs: changes$/mu);
assert.match(workflow, /if: needs\.changes\.outputs\.windows == 'true'/u);
assert.match(workflow, /windows_runtime: \$\{\{ steps\.plan\.outputs\.windows_runtime \}\}/u);
assert.match(workflow, /windows_storage: \$\{\{ steps\.plan\.outputs\.windows_storage \}\}/u);

const stepIds = [...workflow.matchAll(/^\s+- id: ([a-z_]+)$/gmu)].map((match) => match[1]);
const stepIds = [...workflow.matchAll(/^\s+- id: ([a-z_]+)$/gmu)]
.map((match) => match[1])
.filter((stepId) => stepId !== 'plan');
assert.deepEqual(stepIds, [
'install',
'build',
Expand All @@ -79,13 +85,16 @@ test('Windows baseline workflow keeps its non-blocking evidence contract', async
'npm.cmd run build:test',
'npm.cmd run windows:inventory',
'npm.cmd run test:scripts',
'npm.cmd run smoke:windows',
'npm.cmd --workspace @maka/desktop run build:smoke',
'npm.cmd run smoke:windows:dist',
'node.exe scripts/run-workspace-tests-parallel.mjs --concurrency=1 --workspaces=packages/storage',
'node.exe --test --test-force-exit --test-timeout=15000 --test-reporter=tap --test-concurrency=1 --test-name-pattern="semantic text and Enter actions|terminal mode parsed before the control cut" packages/runtime/dist/__tests__/shell-run-manager.test.js',
'node.exe --test --test-concurrency=1 --test-name-pattern="real process crash|real crash|real-process crash|crash after baseline ref publication" packages/storage/dist/__tests__/managed-workspace-baseline.test.js packages/storage/dist/__tests__/git-workspace-service.test.js',
]) {
assert.ok(workflow.includes(command), command);
}
assert.equal(workflow.match(/needs\.changes\.outputs\.windows_storage == 'true'/gu)?.length, 2);
assert.equal(workflow.match(/needs\.changes\.outputs\.windows_runtime == 'true'/gu)?.length, 1);
assert.match(workflow, /Runtime PTY input gate did not run exactly two passing tests/u);

assert.match(workflow, /Get-CimInstance Win32_Process/u);
Expand Down
17 changes: 16 additions & 1 deletion scripts/windows-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ const REPO_ROOT = resolve(dirname(SCRIPT_PATH), '..');
const CLI_PATH = join(REPO_ROOT, 'packages', 'cli', 'dist', 'cli.js');
const DESKTOP_MAIN = join(REPO_ROOT, 'apps', 'desktop', 'dist', 'main', 'main.js');
const DESKTOP_PRELOAD = join(REPO_ROOT, 'apps', 'desktop', 'dist', 'preload', 'preload.cjs');
const DESKTOP_RENDERER = join(REPO_ROOT, 'apps', 'desktop', 'dist-renderer', 'index.html');
const DESKTOP_RUNTIME_WORKER = join(
REPO_ROOT,
'apps',
'desktop',
'resources',
'workers',
'filesystem-worker.js',
);
const DESKTOP_SMOKE = join(REPO_ROOT, 'scripts', 'desktop-real-window-smoke.mjs');
const CLI_TIMEOUT_MS = 15_000;
const DESKTOP_TIMEOUT_MS = 45_000;
Expand All @@ -21,7 +30,13 @@ export function runWindowsSmoke(options = {}) {
const exists = options.existsSync ?? existsSync;
if (platform !== 'win32') throw new Error('Windows smoke must run on Windows');

for (const artifact of [CLI_PATH, DESKTOP_MAIN, DESKTOP_PRELOAD]) {
for (const artifact of [
CLI_PATH,
DESKTOP_MAIN,
DESKTOP_PRELOAD,
DESKTOP_RENDERER,
DESKTOP_RUNTIME_WORKER,
]) {
if (!exists(artifact)) {
throw new Error(`Missing build artifact: ${artifact}. Run npm run build first.`);
}
Expand Down
17 changes: 17 additions & 0 deletions scripts/windows-smoke.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ test('Windows smoke refuses to claim evidence on another platform', () => {
assert.throws(() => runWindowsSmoke({ platform: 'linux' }), /must run on Windows/u);
});

test('Windows smoke requires renderer and runtime resource artifacts', () => {
for (const missingSuffix of [
'apps/desktop/dist-renderer/index.html',
'apps/desktop/resources/workers/filesystem-worker.js',
]) {
assert.throws(
() =>
runWindowsSmoke({
platform: 'win32',
existsSync: (path) => !path.replaceAll('\\', '/').endsWith(missingSuffix),
}),
new RegExp(`Missing build artifact: .*${missingSuffix.replaceAll('/', '.*')}`, 'u'),
missingSuffix,
);
}
});

test('Windows smoke rejects an Electron command that exits without exercising a window', () => {
assert.throws(
() =>
Expand Down
Loading