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
17 changes: 16 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,19 @@ jobs:
- name: Run script tests
run: make script-test

shim-test-windows:
needs: fast-gate
runs-on: windows-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Run npm shim tests on Windows
run: node --test scripts/run.test.js

deterministic-gate:
needs: fast-gate
runs-on: ubuntu-latest
Expand Down Expand Up @@ -520,7 +533,7 @@ jobs:
# ── Results Gate (single required check for branch protection) ─────
results:
if: ${{ always() }}
needs: [fast-gate, unit-test, lint, script-test, deterministic-gate, coverage, deadcode, e2e-dry-run, e2e-live, security, license-header, plugin-integration, sidecar-integration]
needs: [fast-gate, unit-test, lint, script-test, shim-test-windows, deterministic-gate, coverage, deadcode, e2e-dry-run, e2e-live, security, license-header, plugin-integration, sidecar-integration]
runs-on: ubuntu-latest
steps:
- name: Evaluate results
Expand All @@ -533,6 +546,7 @@ jobs:
echo "| L2 | unit-test | ${{ needs.unit-test.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | lint | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | script-test | ${{ needs.script-test.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | shim-test-windows | ${{ needs.shim-test-windows.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | deterministic-gate | ${{ needs.deterministic-gate.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | coverage | ${{ needs.coverage.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| L2 | deadcode | ${{ needs.deadcode.result }} |" >> $GITHUB_STEP_SUMMARY
Expand All @@ -559,6 +573,7 @@ jobs:
"${{ needs.unit-test.result }}" \
"${{ needs.lint.result }}" \
"${{ needs.script-test.result }}" \
"${{ needs.shim-test-windows.result }}" \
"${{ needs.deterministic-gate.result }}" \
"${{ needs.coverage.result }}" \
"${{ needs.deadcode.result }}" \
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ script-test:
bash scripts/resolve-changed-from.test.sh
bash scripts/ci-workflow.test.sh
bash scripts/semantic-review-workflow.test.sh
$(NODE) --test scripts/e2e_domains.test.js scripts/fetch_e2e_tat.test.js scripts/install.test.js scripts/release-preflight.test.js scripts/semantic-review-verify-artifact.test.js scripts/pr-quality-summary.test.js scripts/semantic-review-publish.test.js scripts/ci-quality-summary-publish.test.js
$(NODE) --test scripts/e2e_domains.test.js scripts/fetch_e2e_tat.test.js scripts/install.test.js scripts/release-preflight.test.js scripts/semantic-review-verify-artifact.test.js scripts/pr-quality-summary.test.js scripts/semantic-review-publish.test.js scripts/ci-quality-summary-publish.test.js scripts/run.test.js

# ./extension/... keeps the public plugin SDK in the default test matrix.
unit-test: fetch_meta
Expand Down
30 changes: 30 additions & 0 deletions scripts/ci-workflow.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ lint_section="$(awk '
/^ script-test:/ { exit }
' "$workflow")"
script_test_section="$(job_section script-test)"
shim_test_windows_section="$(job_section shim-test-windows)"
deterministic_section="$(awk '
/^ deterministic-gate:/ { in_job = 1 }
in_job { print }
Expand Down Expand Up @@ -170,6 +171,34 @@ if grep -Fq '${{ secrets.' <<<"$script_test_section"; then
exit 1
fi

if ! grep -Fq "runs-on: windows-latest" <<<"$shim_test_windows_section"; then
echo "shim-test-windows must run on windows-latest so the npm shim is exercised on the platform it broke on"
exit 1
fi
if ! grep -Fq "node --test scripts/run.test.js" <<<"$shim_test_windows_section"; then
echo "shim-test-windows must run the npm shim tests"
exit 1
fi
if ! grep -Fq "node-version: '22'" <<<"$shim_test_windows_section"; then
echo "shim-test-windows must pin node-version so process.execPath fixtures stay reproducible"
exit 1
fi
if ! grep -Fq '"${{ needs.shim-test-windows.result }}"' <<<"$results_section"; then
echo "shim-test-windows must sit inside the results FAILED loop; needs and the summary table alone leave it non-blocking"
exit 1
fi

results_needs_line="$(grep -m1 '^ needs:' <<<"$results_section")"
if ! grep -Fq "shim-test-windows" <<<"$results_needs_line"; then
echo "shim-test-windows must be listed in the results job's needs: array; GitHub returns an empty string for an undeclared dependency's result, so dropping it here would silently make it non-blocking even though the FAILED loop still references it"
exit 1
fi

if grep -Fq '${{ secrets.' <<<"$shim_test_windows_section"; then
echo "shim-test-windows must not reference secrets"
exit 1
fi

if grep -Fq "metadata-gate:" "$workflow"; then
echo "metadata-gate should not run alongside deterministic-gate because both would upload the same facts artifact"
exit 1
Expand All @@ -184,6 +213,7 @@ for full_job in \
"$unit_test_section" \
"$lint_section" \
"$script_test_section" \
"$shim_test_windows_section" \
"$deterministic_section" \
"$coverage_job_section" \
"$dry_run_section" \
Expand Down
65 changes: 64 additions & 1 deletion scripts/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,69 @@ if (args[0] === "install") {
try {
execFileSync(bin, args, { stdio: "inherit" });
} catch (e) {
process.exit(e.status || 1);
// Every branch below that prints a diagnostic ends with
// `process.exitCode = 1` and a plain return/fallthrough, never
// `process.exit(1)`. On POSIX, writes to a piped stderr are asynchronous
// (unlike a TTY); `process.exit()` tears the process down immediately and
// can drop a write that has not finished flushing yet. That is exactly
// what happens when the child has just filled a piped stderr (the
// AI-agent/log-wrapper case this CLI is built for): `process.exit(1)`
// right after `console.error(...)` can lose the entire diagnostic.
// Leaving `process.exitCode` set and returning naturally lets the event
// loop drain pending writes before the process actually exits, so the
// diagnostic survives; the exit code is still 1 either way. The silent
// branches (numeric-status forward, quiet SIGINT/SIGTERM) print nothing,
// so they keep using `process.exit` directly.
// The binary ran and chose its own status — forward it untouched.
if (typeof e.status === "number") {
// Windows has no signals: a crashing native binary (access violation
// 0xC0000005, missing DLL 0xC0000135, killed by endpoint protection) shows
// up as an NTSTATUS number here, not via e.signal. Surface the error-severity
// range (0xC0000000+) as a crash instead of forwarding it silently. Exclude
// 0xC000013A (STATUS_CONTROL_C_EXIT), the Windows Ctrl+C code, to stay
// symmetric with the quiet SIGINT/SIGTERM allowlist. A Go binary never exits
// with a code in this range deliberately.
if (
process.platform === "win32" &&
e.status >= 0xc0000000 &&
e.status !== 0xc000013a
) {
console.error(
`\nlark-cli: the native binary crashed (status 0x${(e.status >>> 0).toString(16)}).\n` +
` path: ${bin}`
);
process.exitCode = 1;
return;
}
process.exit(e.status);
}
// SIGINT and SIGTERM are the explicit quiet allowlist for intentional
// interruption (Ctrl+C during `auth login`, for one). Other signals are
// crash evidence worth surfacing, but do not prove the binary failed to
// launch. Only print e.signal and the known bin path: e.message and related
// error fields can contain the caller's full argv.
if (e.signal) {
if (e.signal === "SIGINT" || e.signal === "SIGTERM") {
process.exit(1);
}
console.error(
`\nlark-cli: the native binary was terminated by signal ${e.signal}.\n` +
` path: ${bin}`
);
process.exitCode = 1;
return;
}
// Neither: the launch itself failed. Report only what is actually known —
// permissions, file format, CPU architecture and endpoint policy all land
// here, and the errno is the only evidence. Print e.code and never
// e.message: when the child does run, e.message carries the full argv,
// which can contain values the caller passed on the command line.
const reason = typeof e.code === "string" ? e.code : "UNKNOWN";
console.error(
`\nlark-cli: failed to launch the native binary.\n` +
` path: ${bin}\n` +
` error: ${reason}`
);
process.exitCode = 1;
}
}
Loading
Loading