Skip to content

feat: add windows support - #96

Merged
amannocci merged 15 commits into
mainfrom
feat/windows-support
Oct 7, 2026
Merged

amannocci merged 15 commits into
mainfrom
feat/windows-support

Conversation

@amannocci

@amannocci amannocci commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

🧑‍💻What is the change being made?

Adds Windows support: runtime fixes, portable tests, a Windows bundle and installer, and CI that tests it against a real terraform.

  • Forward the variables Windows processes need to start (SYSTEMROOT, USERPROFILE, TEMP, TMP, PATHEXT, COMSPEC, APPDATA, LOCALAPPDATA, PROGRAMDATA, SYSTEMDRIVE) to terraform, only when running on Windows.
  • Create directory symlinks in init with the right target_is_directory flag, and fail with an explained error (pointing to Developer Mode) when symlink creation is not permitted.
  • Force LF line endings through .gitattributes so fixtures and scripts do not turn into CRLF on Windows checkouts.
  • Give the fake terraform test fixture a terraform.cmd shim on Windows, and add a posix_only marker for tests that depend on sh, sleep, cat, shebangs or POSIX file modes.
  • Run the test CI job on both ubuntu-latest and windows-latest.
  • Make resource group relative paths and the terraform backend key always /-separated, so state keys and .tnplan files match across hosts, and close the plan/apply temp files before terraform opens them.
  • Rewrite the POSIX-only tests with sys.executable helpers and generated runbook fixtures, so they run on Windows too.
  • Fix terranova apply <plan> --auto-approve on every platform: terraform only accepts options before the positional saved plan, so the plan path now goes last.
  • Fix a Windows crash on piped or redirected output: such streams default to the ANSI code page, which cannot encode the ⇒/✓ markers, so they are reconfigured to UTF-8 and rich's legacy Windows renderer is disabled when the output is not a terminal.
  • Add a real-terraform e2e test (tests/e2e/test_real_terraform.py) driving init, validate, plan, apply, output and destroy with only built-in resources. A session-scoped fixture installs a pinned terraform through the EngineManager (cache shared by the xdist workers; skipped locally when offline, failing on CI), so it runs with the rest of the suite.
  • Build a Windows bundle: PyInstaller spec, .exe path and zip in the build task, a windows-2025 build job and sanitize-zip install check in CI, and contrib/install.ps1 with README instructions.
  • Keep the platform handling in one place: a single IS_WINDOWS flag in utils.py (with the Windows environment variables helper next to it) replaces the scattered os.name/sys.platform checks, the Windows-safe temporary file used by plan and apply becomes an io.temp_file() context manager, and the build task uses a match on the system.

❓ Why is the change being made?

Terranova is only built and tested on macOS and Linux. Terraform cannot even start on Windows with the current environment allow-list, state keys and plan files would differ between hosts, and there is no CI signal or released Windows binary. This PR removes the known runtime blockers, ships a Windows bundle, and adds Windows test and real-terraform jobs so regressions show up in CI. Running against a real terraform also surfaced two bugs, one of which affects every platform.

✅ How has this been tested?

ruff, basedpyright and the full test suite pass locally on macOS (487 passed, including the real-terraform e2e, which downloads terraform on first use). In CI:

  • test (ubuntu-latest) and test (windows-latest) pass, including the real-terraform e2e: on Windows it runs the full lifecycle against a real terraform, which is what found the apply argument order and redirected output bugs above (the one Windows skip is the POSIX file-mode test).
  • The windows-2025 build produces a bundle, and sanitize-zip extracts the zip and runs terranova.exe --version.

Not covered by CI: install.ps1, Windows arm64, and a runbook run against a real terraform. Terraform is also not stopped gracefully on Windows yet (terminate() equals kill()), so it cannot release its state lock if terranova stops it.

Forward the Windows variables terraform needs to start, create
directory symlinks with the right flag and explain permission
failures, keep LF endings, mark POSIX-only tests, and run the test
job on windows-latest.
Resource group rel paths and the backend state key are now always
slash-separated, so plan files and state keys match across hosts.
Plan and apply temp files are closed before terraform opens them,
which Windows requires. The fake terraform env capture is looked up
case-insensitively on Windows.
Run python through sys.executable instead of echo, sleep, cat and sh,
generate the runbook e2e conf dirs with python entrypoints, and fake
the permission probe for the unreadable manifest test. Runbooks now also
receive the variables a Windows process needs to start.
Add a windows PyInstaller spec, pick the right spec, exe name and
architecture in the build task, and zip the bundle. The CI build matrix
gets a windows-2025 runner with a zip install check, and contrib adds an
install.ps1 installer documented in the README.
@amannocci amannocci changed the title feat: add windows support groundwork feat: add windows support Oct 6, 2026
Add an opt-in e2e test driving init, validate, plan, apply, output and
destroy through a real terraform with only built-in resources, and a CI
job running it on Linux from the sources and on Windows against the
frozen terranova.exe bundle.
Terraform only accepts options before the positional plan file, so
`terranova apply <plan> --auto-approve` failed with "Too many command
line arguments" against a real terraform. The fake terraform used by the
other tests never checked argument order, so nothing caught it.
Piped or redirected streams default to the ANSI code page there, which
cannot encode the markers terranova prints, and rich took its legacy
windows renderer for them. Reconfigure such streams to UTF-8 and disable
the legacy renderer when the output is not a terminal.
@amannocci amannocci self-assigned this Oct 6, 2026
Add session-scoped fixtures that install a pinned terraform through
EngineManager into a cache shared by the xdist workers, and a function
fixture that puts it first on PATH. The real-terraform e2e now runs with
the rest of the suite, so the opt-in variable, the setup-terraform step
and the dedicated CI job go away. On Windows the test job points the
e2e at the frozen terranova.exe bundle.
Drop the steps that unpacked the frozen bundle for the test job, and
the TERRANOVA_BIN switch they were the only user of. The packaged build
keeps its own --version check in the sanitize-zip job.
Replace the early return and the conditional expression with a match on
the system, and move the shared renaming of the bundle into a helper.
plan and apply both created a closed-but-kept temporary file so terraform
could open it, which Windows requires. Share it as a context manager and
cover it with tests.
Add IS_WINDOWS and use it instead of repeated os.name and sys.platform
checks. The Windows environment variables helper moves next to it, which
also removes the import cycle between resources and binds.
Replace zip(chain, chain[1:]) with pairwise(chain), as ruff RUF007
recommends.
The module already postpones annotation evaluation with
`from __future__ import annotations`, so the quotes are redundant.
@amannocci
amannocci merged commit 14040eb into main Oct 7, 2026
17 checks passed
@amannocci
amannocci deleted the feat/windows-support branch October 7, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant