Skip to content

feat!: dockur/windows based runners with self-healing install - #6

Merged
vaggeliskls merged 13 commits into
mainfrom
feat/dockur-windows
Oct 7, 2026
Merged

vaggeliskls merged 13 commits into
mainfrom
feat/dockur-windows

Conversation

@vaggeliskls

Copy link
Copy Markdown
Owner

Current Behaviour

Releases up to 1.3.0 ship a container image that runs the Windows VM through Vagrant and libvirt. It needs a privileged container with cgroup: host, re-provisions the VM on every start (20 to 40 minutes), and the runner run.cmd processes live in an interactive session. A wrong token, a missing .env or a changed runner count all need a manual RDP session or a full re-provision to recover, and the install log is only visible inside the VM.

Expected Behaviour

Version 2: the VM is dockur/windows driven by docker-compose.yml and two scripts in oem/. No custom image, no Vagrant, no privileged container. Windows is installed once from the evaluation ISO and persisted in storage/; later starts just boot it.

Deployment

  • docker-compose.yml runs dockurr/windows, bind-mounts oem/ (install scripts), storage/ (disk + ISO), shared/ (log and markers) and .env as /shared/runner.env, which the VM reads as \host.lan\Data\runner.env. Nothing secret goes into oem/, since dockur bakes that folder into the install ISO.
  • .env.example documents every setting with its default. VM sizing (RAM_SIZE, CPU_CORES, DISK_SIZE) applies at the next restart; DISK_SIZE can only grow.

oem/install.ps1

  • Installs PowerShell 7, Chocolatey, Git, jq, 7-Zip, Visual Studio 2022 (edition and workloads configurable) and Rtools 4.0, then registers RUNNERS runners as Windows services (default) or as scheduled tasks in the desktop session (RUNNER_MODE=interactive).
  • Registers a "GitHub runner setup" scheduled task that re-runs the script at every logon. It checks each toolchain part on the machine and installs only what is missing, and re-registers the runners only when runner.env changed since they were last registered (SHA-256 marker). Fixing .env on the host and restarting the container is therefore enough to recover from a wrong PAT or URL, change the runner count, labels or version. Lowering RUNNERS removes the surplus runners from GitHub. With nothing to do, the task exits without touching shared/.
  • A missing .env at docker compose up makes Docker create an empty directory in its place; the script now explains that instead of "Access to the path is denied".
  • Logging: the console, C:\OEM\install.log and shared/install.log on the host carry the same text. Native command output (choco, pacman, config.cmd) is routed through Invoke-Native so the transcript records stdout and stderr; a background job appends the transcript to the host copy every 2 seconds, so tail -f shared/install.log is a live feed. install.done or install.failed marks the end.

CI

  • lint.yml validates docker compose config, parses install.ps1 and runs PSScriptAnalyzer with the repo settings, plus actionlint. The old image build workflow is removed.

Verified

  • Full install on a Linux KVM host with the branch's scripts (first start, runner registration).
  • Scheduled-task objects, transcript switching, native output capture, the append-only mirror and the tool detection were exercised in Windows PowerShell 5.1, the version in the VM, without registering anything.

Not covered

  • Changing VS_EDITION or VS_WORKLOADS after Visual Studio is installed is not detected by the logon check (any VS 17.x counts as present); it needs a manual run or a wipe of storage/.
  • Workflows using the old windows_vagrant_action label should switch to windows_x64 or add the label to GITHUB_RUNNER_LABELS; see "Migrating from 1.x" in the README.

Runs the Windows Server 2022 VM on dockurr/windows instead of the custom
Vagrant + libvirt image. Windows is installed once from the evaluation ISO
and persisted in ./storage; oem/install.ps1 installs the toolchain and
registers the runners at the end of the unattended install.

- Remove Dockerfile, Vagrantfile and the image release workflow
- Add oem/install.bat + oem/install.ps1 (service or interactive runners,
  configurable VS edition/workloads, -RunnersOnly re-registration)
- Settings reach the VM through .env mounted into the dockur shared folder
- Lint: compose config, PowerShell parse + PSScriptAnalyzer, actionlint
- Dependabot tracks the dockurr/windows tag
- README rewritten with deprecation notice and 1.x migration guide
install.ps1
- Register a "GitHub runner setup" scheduled task on the first run. It re-runs
  the script with -AtLogon at every logon: installs toolchain parts that are
  missing (checked per tool, not via a marker) and re-registers the runners
  when runner.env changed since they were last registered. Exits quietly
  otherwise, so fixing .env on the host and restarting the container is enough.
- Find-Config rejects a directory named runner.env with an explanation: Docker
  creates one when .env is missing at 'docker compose up', and Get-Content on
  it only said "Access to the path is denied".
- Initialize-System runs on every real run so a larger DISK_SIZE grows C:.

.env.example: note that RAM/CPU apply at restart and DISK_SIZE can only grow.
README: restart-based update flow, recovery from a missing .env.
… first

- One transcript per run instead of appending forever; a logon check that
  finds nothing to do writes to install-logon.log so it never overwrites the
  log of the last real run.
- Lowering RUNNERS now removes the runners above the new count from GitHub
  and deletes their folders.
- Remove-Runner stops the interactive-mode task and listener before
  config.cmd remove and the folder deletion, which otherwise hit open files.
- Invoke-Native runs choco, git, pacman and config.cmd so that their stdout
  and stderr are recorded in the transcript. Windows PowerShell does not
  record output a program writes straight to the console, and 2>&1 under
  ErrorActionPreference=Stop would abort at the first stderr line.
- A background job copies the transcript to the shared folder every 10 s
  while a real run lasts, replacing the per-line mirroring of Write-Log.
Copy-Item rewrote shared/install.log every 2 s, which tail -f reported as
"file truncated" on every pass. The mirror job now remembers how much of
the transcript it has sent and appends only the new bytes. The final
overwrite is gone too; the job gets one more pass after Stop-Transcript.
The unauthenticated API allows 60 calls per hour per public IP, shared with
everything else behind that IP, and a first install failed on it at the
very last step. The releases/latest page redirects to the tag URL, which
carries the version and is not rate limited.
@vaggeliskls
vaggeliskls merged commit 586ebce into main Oct 7, 2026
1 check passed
@vaggeliskls
vaggeliskls deleted the feat/dockur-windows branch October 7, 2026 12:28
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