Repository navigation
feat(cli): add Windows support and automated CI test coverage - #2350
mbiernacik wants to merge 11 commits into
Conversation
- Enforce canonical forward slash '/' separators across all platforms via Path.toPosixPath in core/path.ts - Use Path.toPosixPath in core/session.ts, core/utils.ts, and core/main.ts instead of inline regex replacements - Support case-insensitive Windows drive letters in relativePath and caller file detection - Allow file: tarball dependencies in workflow_settings.yaml in core/workflow_settings.ts - Remove unsupported stdio pipe and SIGKILL in cli/api/commands/base_worker.ts - Add maxRetries and recursive cleanup to cli/api/commands/compile.ts to prevent Windows file locking issues - Add CRLF normalization helper to cli/commands/format_command.ts - Add scripts/smoke_test_windows.js testing all primary CLI flows under PowerShell and cmd.exe - Add .github/workflows/test.yaml with windows-latest runner and status checks - Update contributing.md with Windows prerequisites and smoke testing instructions
| return `dataform.sqlxAction({ | ||
| sqlxConfig: { | ||
| name: "${Path.escapedBasename(path)}", | ||
| name: "${Path.basename(path)}", |
There was a problem hiding this comment.
The basename is interpolated into a JS string literal. Backslashes can no longer appear here, but a " in a filename (legal on POSIX) would break the generated code. Consider name: ${JSON.stringify(Path.basename(path))}, to make it robust regardless of filename contents.
| * UNC root such as `\\server\share\proj` becomes `//server/share/proj` rather than a POSIX-looking | ||
| * `/server/share/proj`. | ||
| */ | ||
| export function toPosixPath(path: string): string { |
There was a problem hiding this comment.
[Nice to have] toPosixPath now runs on all platforms (via normalize, join, filename, relativePath). On POSIX, \\ is a legal filename character, so e.g. definitions/a\\b.sqlx would now be treated as a subdirectory. Probably acceptable, but worth making this an explicit, documented decision (or gating conversion to Windows-style inputs).
| return toPosixPath(path).split("/").slice(-1)[0]; | ||
| } | ||
|
|
||
| export function basename(path: string) { |
There was a problem hiding this comment.
Behavior change: basename(".gitignore") was "" and is now ".gitignore". Seems more correct, but could you double-check callers (e.g. action naming in core/utils.ts) aren't relying on the old behavior?
| return basename(path).replace(/\\/g, "\\\\"); | ||
| } | ||
|
|
||
| export function fileExtension(fullPath: string) { |
There was a problem hiding this comment.
Behavior change: fileExtension("noext") used to return "noext", now "". Worth verifying callers that dispatch on extension (sqlx/js detection) are unaffected.
Lets make sure we have a test for that.
| } | ||
| if (!nextLastfile.includes(rootDir)) { | ||
| const nextLastfile = Path.toPosixPath(rawNextLastfile); | ||
| if (!Path.startsWithPath(nextLastfile, rootDir)) { |
There was a problem hiding this comment.
[Nice to have] This is stricter than the previous includes(rootDir): it now requires rootDir to be absolute and in the same form as stack-trace filenames. Can we confirm projectDir is always resolved to an absolute path before reaching core (incl. VSCode extension / JiT callers)? Otherwise caller resolution would silently fall back to __dataform_current_file or throw.
|
|
||
| if (workflowSettingsDataformCoreVersion) { | ||
| fs.rmSync(temporaryProjectPath, { recursive: true }); | ||
| fs.rmSync(temporaryProjectPath, { recursive: true, force: true, maxRetries: 3 }); |
There was a problem hiding this comment.
[Nice to have] force: true will also mask genuine cleanup failures (not only the Windows EBUSY/EPERM races). Fine for a temp dir, just flagging; a debug log on failure might help diagnose leaks.
| const forkScript = this.resolveScript(); | ||
| const child = fork(forkScript, [], { | ||
| stdio: [0, 1, 2, "ipc", "pipe"], | ||
| stdio: [0, 1, 2, "ipc"], |
There was a problem hiding this comment.
Removing the extra "pipe" looks safe from the parent side (nothing reads child.stdio[4]). Could you confirm the worker/child script doesn't use fd 4 either?
| @@ -0,0 +1 @@ | |||
| * text=auto eol=lf | |||
There was a problem hiding this comment.
[Nice to have] Repo-wide * text=auto eol=lf will likely trigger a one-time renormalization diff for existing files, and may need exceptions for any files that must keep CRLF. Maybe worth a note in the PR or a follow-up git add --renormalize ..
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
|
|
||
| - name: Setup Bazel | ||
| uses: bazelbuild/setup-bazelisk@b39c379c82683a5f25d34f0d062761f62693e0b2 # v3.0.0 |
There was a problem hiding this comment.
Not blocking here, but lets keep in mind that no Bazel cache is configured, so every PR does a cold build (60 min timeout). Let's consider adding a disk/repository cache (e.g. bazel-contrib/setup-bazel with disk-cache) to cut CI time.
| - name: Setup Node.js | ||
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | ||
| with: | ||
| node-version: 20 |
There was a problem hiding this comment.
Is Node 20 the minimum supported version? Consider a small matrix (min + latest LTS). Also, dataform init in the smoke test installs @dataform/core from the npm registry, so that flow is network-dependent and doesn't exercise this PR's core; maybe use the local core tarball there too.
|
One small request on the PR description, which seems to have drifted from final diff:
|
Summary
This PR brings full, first-class support for the Dataform CLI (
@dataform/cli) and Core (@dataform/core) on Windows (supporting both PowerShell and Command Promptcmd.exe). It resolves all known historical Windows issues (#1565, #510, #1813), fixes platform incompatibilities, and introduces an automated GitHub Actions CI workflow running onwindows-latestto prevent future regressions.What was introduced
.github/workflows/test.yaml):test-linuxjob: builds package tarballs and runs core unit tests via Bazel.test-windowsjob (windows-latest): downloads the packaged CLI tarball, installs it globally via npm, and runs an end-to-end smoke test suite under both PowerShell and cmd.exe.scripts/smoke_test_windows.js):init,installwithworkflow_settings.yamlandpackage.json,compile,compile --json, JiT actions,compile --watch,run --dry-run,test,format, andinit-creds).core/path.ts,core/session.ts,core/utils.ts):Path.toPosixPath()and canonicalPath.separator = "/"across all platforms.C:\vsc:/) inrelativePathand caller file resolution.getContents.cli/api/commands/base_worker.ts,compile.ts):stdio).SIGKILLwith graceful process termination.maxRetries: 3) tofs.rmSyncincompile.tsto prevent Windows file-locking race conditions (EBUSY/EPERM).cli/commands/format_command.ts):dataform format --checkfalse positives when Git checks out files with CRLF on Windows.contributing.md):Issues Identified & Solutions
dataform compilecompiled 0 actions on Windows projects.Path.separatorwas set to\\on Windows (core/path.ts), but file globbing incli/vm/compile.tsalways emits/.file.path.startsWith("definitions\\")never matcheddefinitions/my_table.sqlx.Path.separator = "/"globally; addedPath.toPosixPath()to normalize backslashes.missing dependency detected;ref()resolution broke.fileNameand target identifiers, creating mismatched keys in Dataform's dependency graph./formatting across all action targets and proto fields.dataform format --checkexited with code1on cleanly formatted files.\r\n), which differed from Prettier's LF (\n) output.normalizeLineEndings()helper before comparing files informat_command.ts..cmd/.batfiles incmd.exefailed with'\"C:\...\"' is not recognized.{ shell: true }. Manually invokingcmd.exe /cwith quoted arguments caused Node's argument serializer to escape quotes to\".{ shell: true }on Windows in CLI runners and test harnesses to avoid argument double-escaping.spawn EINVAL) in VSCode extension / background compilation.stdiowas set to[0, 1, 2, "ipc", "pipe"]. Windows does not support anonymous pipes alongside an IPC channel. Windows also lacksSIGKILL.stdioto["ipc"]and replacedSIGKILLwith platform-neutralchild.kill().fs.rmSyncfailed withEBUSY/EPERMduring cleanup of compilation temp directories.{ recursive: true, force: true, maxRetries: 3 }tofs.rmSyncincli/api/commands/compile.ts.getCallerFileandsession.getContentsfailed withUnable to find valid caller file./(\C:\...), and drive letter comparisons were case-sensitive (C:vsc:).workflow_settings.yamlfailed validation.dataformCoreVersionvalidation only allowed semver strings.file:tarball paths incore/workflow_settings.ts.How it was tested
test-windowsjob running onwindows-latestunder both PowerShell and Command Prompt (cmd.exe).scripts/smoke_test_windows.js(18/18 tests passed).Note
After merge: Required Status Check
To ensure Windows regressions are permanently prevented, please enable
CLI smoke & unit tests (Windows)as a Required Status Check in the repository settings