Skip to content

Instantly share code, notes, and snippets.

@YoraiLevi
Created August 11, 2026 11:24
Show Gist options
  • Select an option

  • Save YoraiLevi/62767615b0264571b742bca56f32971b to your computer and use it in GitHub Desktop.

Select an option

Save YoraiLevi/62767615b0264571b742bca56f32971b to your computer and use it in GitHub Desktop.
Run act (GitHub Actions locally) on Windows using Podman as the Linux runtime while Docker Desktop stays in Windows-containers mode - step-by-step guide, PowerShell installer, edge cases and smoke test

Run act (GitHub Actions locally) on Windows using Podman as the Linux runtime, while Docker Desktop stays in Windows-containers mode

act only ships Linux runner images. If Docker Desktop is set to Windows containers, its Linux engine is stopped, so act has no runtime and every run fails. This guide points act at Podman — which serves a Docker-API-compatible named pipe — without changing Docker Desktop's mode and without setting a global DOCKER_HOST that would break your Windows-container work.

The result: docker keeps talking to Docker Desktop (Windows containers), act talks to Podman (Linux containers), and neither knows about the other.

Tradeoff to know before you start: the only lever act offers is the DOCKER_HOST environment variable — it ignores Docker contexts (verified below). So act must always be launched through a small wrapper. This guide installs that wrapper as a script on PATH, not a shell function, so it also works under pwsh -NoProfile, from cmd.exe, and from editors or CI that spawn a bare shell. Nothing here modifies Docker Desktop, the Podman machine, or your Docker contexts.

Tested on one machine only. Everything below was verified end-to-end, but on a single Windows 11 + WSL2 + Podman 6.0.2 setup — never reproduced on independent hardware. Before you rely on it, skim the footnote: it names the specific conditions (multiple Podman machines, Hyper-V machines, rootful machines, Windows PowerShell 5.1) where this could break and how you would recognise each.

Sources:


Quick install

Open a non-elevated PowerShell 7 (pwsh) and:

$url  = 'https://gist.githubusercontent.com/YoraiLevi/62767615b0264571b742bca56f32971b/raw/setup-act-podman.ps1'
$dest = "$env:TEMP\setup-act-podman.ps1"
Invoke-WebRequest -UseBasicParsing $url -OutFile $dest
Unblock-File $dest          # strip Mark-of-the-Web so PowerShell doesn't block it
Get-Content $dest           # inspect before running
& $dest                     # add -DockerInWorkflow if workflows run `docker` commands

No administrator rights are needed. The script is idempotent — re-run it any time to change options.

Script flags:

Flag Effect
-DockerInWorkflow Let workflow steps run docker build / docker run against Podman
-InstallDir <path> Where to put the wrapper scripts (default $HOME\.local\bin, added to your user PATH)
-NoProfileHook Don't add the gh act router to your PowerShell profile (act still works)
-SkipSmokeTest Don't run the end-to-end check (which pulls ~500 MB of runner image)
-Uninstall Remove everything and restore the previous actrc

What a successful run looks like

Verified on Windows 11 Pro build 26200, Podman 6.0.2 (WSL2 machine), Docker Desktop 29.6.2 in Windows-containers mode, gh 2.94.0, gh-act v0.2.89:

This is a real run of .\setup-act-podman.ps1 -DockerInWorkflow on a machine with no prior act configuration (runner-image pull lines trimmed):

==> Checking gh and the act extension
[OK] gh act	nektos/gh-act	v0.2.89
==> Checking Podman
[OK] C:\Users\devic\AppData\Local\Programs\Podman\podman.exe
[OK] podman machine: running
==> Baseline: what each endpoint reports right now
    docker CLI default  -> windows
    podman pipe         -> linux   (npipe:////./pipe/podman-machine-default)
==> Writing C:\Users\devic\AppData\Local\act\actrc
[OK] actrc written (docker-in-workflow: enabled)
==> Installing wrapper scripts into C:\Users\devic\.local\bin
[OK] C:\Users\devic\.local\bin\act.ps1
[OK] C:\Users\devic\.local\bin\act-doctor.ps1
[OK] C:\Users\devic\.local\bin already on user PATH
==> Adding `gh act` router to your PowerShell profile
[OK] hook added to C:\Users\devic\Documents\PowerShell\profile.ps1 (backup: ...profile.ps1.bak-act)
==> Smoke test (first run pulls ~500 MB of runner image)
level=info msg="Using docker host 'npipe:////./pipe/podman-machine-default', and daemon socket '/var/run/docker.sock'"
[smoke/hello] 🚀  Start image=catthehacker/ubuntu:act-latest
[smoke/hello]   ✅  Success - Main actions/checkout@v4 [631.0067ms]
[smoke/hello]   | act reached
[smoke/hello]   | Linux
[smoke/hello]   | PRETTY_NAME="Ubuntu 24.04.4 LTS"
[smoke/hello] 🏁  Job succeeded

==> After-state (compare with the baseline above)
    docker CLI default  -> windows   (unchanged)
    act ran against     -> linux   (npipe:////./pipe/podman-machine-default)

[OK] act is working on Podman. Run `act -l` in a repo to list its workflows.

Ubuntu 24.04.4 LTS printed from a docker version → windows host is the proof: two container runtimes, side by side, neither disturbed.

The two lines that matter are the baseline and the after-state: docker still reports windows, and act reached a linux engine. That is the whole point of the setup.


Why act fails here

Docker Desktop registers several named pipes. docker_engine is an alias that follows whichever mode the UI is in; the per-engine pipes let a client pin one engine explicitly. Switching to Windows containers does not merely re-point the alias — it stops the Linux backend, so the Linux pipes are registered but dead.

Measured with docker -H npipe:////./pipe/<name> version on the reference machine, in Windows-containers mode:

Pipe Reports
docker_engine windows (follows the active mode)
docker_engine_windows windows
docker_engine_linux hangs — registered, nothing accepting
dockerDesktopLinuxEngine HTTP 500 — backend stopped
podman-machine-default linux, API 1.44 ← what we want

act is an ordinary Docker-API client: it does not care who implements the API, only that Server.Os == linux. Podman's compat endpoint satisfies that, so it is a drop-in with no patching.

If you point act at the Windows engine, this is what you get — worth recognising, because it names neither Podman nor the engine:

failed to create container: 'Error response from daemon: invalid mount config for
type "volume": invalid mount path: '/opt/hostedtoolcache''

Requirements

Requirement Why How to check
Windows 10 build 19043+ / Windows 11 WSL2 backend for the Podman machine winver
Podman installed, machine initialised Provides the Linux engine podman machine list shows a machine
GitHub CLI Hosts the act extension gh --version
gh-act extension The act binary itself gh extension list shows nektos/gh-act
PowerShell 7 (pwsh) The wrapper scripts $PSVersionTable.PSVersion
~2 GB free disk Runner image catthehacker/ubuntu:act-latest

If you have no Podman machine yet:

podman machine init
podman machine start

Install the extension if missing (the setup script does this for you):

gh extension install https://github.com/nektos/gh-act

Manual setup

Do this if you would rather not run the script. Four steps.

1. Confirm the two engines, and record the baseline

This is the before-probe you will compare against at the end.

docker version --format '{{.Server.Os}}'

Expected on this setup: windows. That is exactly why plain act fails.

Now find your Podman pipe — do not copy the name below, the machine name is part of it:

podman machine inspect --format '{{.ConnectionInfo.PodmanPipe.Path}}'

On the reference machine this printed \\.\pipe\podman-machine-default. Convert it to a DOCKER_HOST URI by replacing the \\.\pipe\ prefix with npipe:////./pipe/, then verify it serves Linux:

docker -H npipe:////./pipe/podman-machine-default version --format '{{.Server.Os}} (API {{.Server.APIVersion}})'

Expected: linux (API 1.44). If this does not say linux, stop — nothing downstream will work. Check podman machine list shows the machine Currently running.

2. Write act's config file

On Windows this lives at %LOCALAPPDATA%\act\actrcnot ~/.actrc.

$dir = "$env:LOCALAPPDATA\act"
New-Item -ItemType Directory -Force -Path $dir | Out-Null
@'
-P ubuntu-latest=catthehacker/ubuntu:act-latest
-P ubuntu-24.04=catthehacker/ubuntu:act-24.04
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
--container-daemon-socket -
'@ | Set-Content "$dir\actrc" -Encoding utf8

The -P lines pin the runner images. They also permanently answer act's interactive first-run image picker, which otherwise aborts in any non-interactive shell with:

? Please choose the default image you want to use with act:
level=fatal msg="Incorrect function."

--container-daemon-socket - disables mounting a daemon socket into jobs. Without it, act mounts the value of DOCKER_HOST — here a Windows named pipe — into a Linux container, which is meaningless. See Docker inside workflows to turn this on deliberately.

3. Install the wrapper on PATH

Save this as act.ps1 in a directory on your user PATH (e.g. $HOME\.local\bin). It resolves the pipe dynamically, so it keeps working if you rename or recreate the machine.

$ErrorActionPreference = 'Stop'

$podman = (Get-Command podman.exe -ErrorAction SilentlyContinue).Source
if (-not $podman) { Write-Error 'podman.exe not found on PATH.'; exit 1 }

$state = (& $podman machine inspect --format '{{.State}}' 2>$null | Out-String).Trim()
if (-not $state) { Write-Error 'No podman machine found. Run: podman machine init'; exit 1 }
if ($state -ne 'running') {
    Write-Host "podman machine is '$state'; starting it..." -ForegroundColor Yellow
    & $podman machine start | Out-Null
}

$pipe = (& $podman machine inspect --format '{{.ConnectionInfo.PodmanPipe.Path}}' 2>$null | Out-String).Trim()
if (-not $pipe) { Write-Error 'Could not resolve the podman pipe path.'; exit 1 }

$prev = $env:DOCKER_HOST
$had  = Test-Path Env:\DOCKER_HOST
try {
    $env:DOCKER_HOST = 'npipe:////./pipe/' + (Split-Path $pipe -Leaf)
    & gh.exe act @args
    $code = $LASTEXITCODE
}
finally {
    if ($had) { $env:DOCKER_HOST = $prev }
    else { Remove-Item Env:\DOCKER_HOST -ErrorAction SilentlyContinue }
}
exit $code

If the directory is not yet on your PATH:

$dir  = "$HOME\.local\bin"
$user = [Environment]::GetEnvironmentVariable('Path','User')
if (($user -split ';') -notcontains $dir) {
    [Environment]::SetEnvironmentVariable('Path', $user.TrimEnd(';') + ';' + $dir, 'User')
}

Open a new shell afterwards.

4. Verify — the same probe as step 1

cd <any repo with .github/workflows>
act -l                                    # lists workflows; proves act reached a Linux daemon
docker version --format '{{.Server.Os}}'  # still `windows` — unchanged

act -l succeeding and docker still reporting windows together mean the split is working.


Using it

You type Works without profile? Notes
act push Yes The PATH script. Preferred.
act -l Yes List workflows in the current repo
act-doctor Yes Diagnostic — prints every endpoint
gh act push No Needs the profile hook (a gh function)

Under pwsh -NoProfile, or in cmd.exe, or anywhere the wrapper isn't installed, set the variable inline — it is the only thing the wrapper really does:

# PowerShell
$env:DOCKER_HOST='npipe:////./pipe/podman-machine-default'; gh act push
:: cmd.exe
set DOCKER_HOST=npipe:////./pipe/podman-machine-default && gh act push

Do not make that permanent with setx. See Design notes.


Edge cases

Docker inside workflows

If your workflow steps run docker build, docker run, or Testcontainers, re-run the installer with -DockerInWorkflow, or set this in actrc:

--container-daemon-socket /var/run/docker.sock

The value must be the path as the daemon sees it. Bind-mount sources are resolved server-side, inside the Podman VM — not on Windows — so it is a Linux path, never the npipe. Podman's machine already provides /var/run/docker.sock there as a symlink to podman.sock, so there is nothing to create. Verified inside a job:

server=6.0.2 os=linux      <- Podman answering as "docker"
via-buildx                 <- docker build --load  + docker run
via-classic                <- DOCKER_BUILDKIT=0 docker build + docker run

Security: this hands every workflow you run full control of your Podman engine. Leave it off unless you need it.

Gotcha — docker build then docker run fails with denied. Inside the runner image, docker build uses buildx's docker-container driver, which leaves the result in the build cache instead of the image store:

WARNING: No output specified with docker-container driver. Build result will only remain in the build cache.
Unable to find image 'my-image:latest' locally
docker: Error response from daemon: denied: requested access to the resource is denied

It then tries to pull your locally-built tag. This is buildx behaviour, not Podman. Fix with either:

- run: docker build --load -t my-image .    # load into the image store
- run: DOCKER_BUILDKIT=0 docker build -t my-image .   # or use the classic builder

--bind and Windows paths

act copies your working directory into the container by default. --bind mounts it instead, and it works here: the Podman WSL machine exposes your drive, so the job's working directory becomes /mnt/c/Users/.... Writes propagate back to the Windows directory — verified by having a workflow create a file and finding it on the host afterwards.

Use --bind when you want build outputs to land in your real working tree; leave it off when you want workflow runs to be disposable.

Service containers

services: blocks work, but the service hostname does not resolve by default. act puts the job container on --network host while each service goes on its own network, so a step doing curl http://web/ fails even though the service is healthy. The mapped port on localhost does work:

services:
  web:
    image: nginx:alpine
    ports: ['8080:80']    # <- reach it as http://localhost:8080, not http://web/

This is act behaviour (--network defaults to host), not a Podman limitation, and it is a real difference from GitHub-hosted runners, where the service hostname resolves.

Do not try to fix it with --network. Tested on gh-act v0.2.89: act always puts each service on its own generated network (act-<workflow>-<job>-…-network) and never joins the job container to it. Passing --network '' changed nothing — the job still got network="host". Passing --network podman moved the job onto the podman network while the service stayed on its own, which breaks the localhost:8080 route as well and leaves you with no path to the service at all.

The working approach on this version is to publish the port and address it on localhost, as above. If your workflow hardcodes the hostname, make it configurable:

- run: |
    URL=http://web/
    [ "$ACT" = "true" ] && URL=http://localhost:8080/
    curl -sf "$URL"

act sets ACT=true — verified both as a shell variable and in the expression context (${{ env.ACT }}) — which lets a workflow branch on "running locally" without affecting real CI, where the variable is absent.

Podman machine resources

The default machine is modest (2 GB RAM on the reference setup). Heavy builds get OOM-killed. Check and raise:

podman machine inspect --format 'mem={{.Resources.Memory}}MB cpus={{.Resources.CPUs}}'
podman machine stop
podman machine set --memory 8192 --cpus 6
podman machine start

Rootless vs rootful

The machine is rootless by default. Workflows needing privileged operations or ports below 1024 need a rootful machine. The pipe name does not change, so the wrapper keeps working:

podman machine stop
podman machine set --rootful
podman machine start

First run is slow

catthehacker/ubuntu:act-latest is ~500 MB and actions/setup-node downloads a toolchain on top. Subsequent runs reuse both. act re-pulls the runner image by default; add --pull=false to actrc to stop that once you have it.


Troubleshooting

Symptom Cause and fix
invalid mount config for type "volume": invalid mount path: '/opt/hostedtoolcache' act reached the Windows engine. DOCKER_HOST wasn't set — use the act wrapper, not bare gh act.
no matching manifest for windows/amd64 Same cause as above.
level=fatal msg="Incorrect function." after an image-picker prompt actrc is missing its -P lines, and the picker can't run in a non-interactive shell. Re-run the installer.
Cannot connect to the Docker daemon / hangs Podman machine isn't running: podman machine start. Confirm with act-doctor.
act works but gh act doesn't The gh router lives in your PowerShell profile. You're under -NoProfile, or need a new shell. Use act.
docker commands suddenly hit Podman A global DOCKER_HOST got set. Clear it: [Environment]::SetEnvironmentVariable('DOCKER_HOST',$null,'User'), then open a new shell.
docker: ... denied: requested access to the resource is denied after a successful build buildx docker-container driver. Use docker build --load or DOCKER_BUILDKIT=0.
Service reachable on localhost:PORT but not by hostname act never joins the job to the service's network. Address the mapped port; do not "fix" it with --network, which removes the working route too. See Edge cases.
Workflow OOM-killed / very slow Raise the Podman machine's memory (see Edge cases).
podman machine inspect prints nothing No machine exists: podman machine init.

Run act-doctor first for anything not listed — it prints what every endpoint currently reports.


Uninstall

& "$env:TEMP\setup-act-podman.ps1" -Uninstall

That removes act.ps1, act-doctor.ps1, the profile hook, and restores any actrc it backed up. It does not touch Podman, Docker Desktop, or the gh-act extension. To remove those too:

gh extension remove act
podman machine stop

Your InstallDir stays on the user PATH; remove it by hand if you added it only for this.


Design notes

Four alternatives were considered and rejected. If you are tempted by any of them, here is what happens.

Set DOCKER_HOST globally with setx. Simplest, and it does make act work — but it also redirects the docker CLI to Podman, which breaks the Windows-container work this machine exists to do. The wrapper scopes the variable to the act process and restores it on exit, so both runtimes stay usable. (The companion Podman gist suggests setx DOCKER_HOST for a general Docker/Podman clash; that advice is right when you want Podman everywhere — it is the wrong trade when you are deliberately keeping Docker Desktop on Windows containers.)

Use a Docker context instead. Podman registers one automatically, and it is genuinely the right tool for the docker CLI:

docker -c podman-machine-default ps

But act ignores DOCKER_CONTEXT — verified: with only the context set and no DOCKER_HOST, act logged Using docker host 'npipe:////./pipe/docker_engine' and went to the Windows engine. Contexts are a CLI-layer feature; act reads the environment variable directly.

Make Podman serve \\.\pipe\docker_engine_linux. Windows does allow several server instances under one pipe name, but clients then land on whichever instance is free — nondeterministic — and Docker Desktop re-registers the name on restart or mode switch. Worse, it destroys the property that makes this setup debuggable: right now docker → windows, act → podman is unambiguous. Podman only claims a Docker-named pipe when Docker Desktop is absent.

Run act inside WSL instead. Legitimate, and a good choice if you already live in WSL: install act in the distro and point it at Podman's Unix socket directly. It is a different setup rather than a fix for this one — your workflows then see WSL paths, and gh act on Windows still fails.



Footnote — what was verified, and where this may still bite you

Everything above was developed and tested on one machine. No second machine was available, so nothing here has been reproduced on independent hardware. That is the single biggest caveat: the usual way a runbook fails is on the first box that isn't the author's, and that test has not been run. Treat the list below as "known places to look" rather than "known bugs".

Verified end-to-end on Windows 11 Pro build 26200 · Podman 6.0.2 (WSL2 machine, rootless) · Docker Desktop 29.6.2 in Windows-containers mode · gh 2.94.0 · gh-act v0.2.89 · PowerShell 7: plain workflows; actions/checkout@v4 and actions/setup-node@v4; docker-in-workflow with both buildx --load and DOCKER_BUILDKIT=0; --bind including write-back to the Windows directory; service containers over mapped ports; the --network non-fix described above; act and gh act entrypoints; operation under pwsh -NoProfile; DOCKER_HOST restoration after every run; and a full uninstall/reinstall cycle of the installer.

Not verified — and why each might break:

Untested condition What could go wrong How you'd know / what to do
More than one Podman machine The wrapper runs podman machine inspect with no machine name. With several machines defined, that may emit one record per machine, so the state check and the pipe path could be read from the wrong one — or from concatenated output. If act targets the wrong machine, hardcode it: replace podman machine inspect with podman machine inspect <name> in act.ps1. Only one machine existed on the reference box.
Hyper-V-backed machine (podman machine init --provider hyperv) Two assumptions are WSL-specific: that /var/run/docker.sock exists inside the VM (docker-in-workflow), and that your Windows drive is visible as /mnt/c (--bind). Neither is guaranteed on a Hyper-V machine. act-doctor will still pass — it only checks the pipe. Docker-in-workflow and --bind are what to re-test.
Rootful machine (podman machine set --rootful) The rootful socket is /run/podman/podman.sock. Whether /var/run/docker.sock still points somewhere useful was not checked, so --container-daemon-socket /var/run/docker.sock may need updating. Run a workflow with docker ps in it. If it fails, set the socket line to the rootful path.
Windows PowerShell 5.1 instead of pwsh 7 The scripts avoid PowerShell 7-only syntax but were never run on 5.1. The specific hazard: Set-Content -Encoding utf8 writes a BOM on 5.1 but not on 7, and a BOM on the first line of actrc may stop act parsing the leading -P entry — which would resurface the interactive image picker. Symptom is the Incorrect function. picker error despite an actrc existing. Re-save actrc without a BOM. Requirements list PowerShell 7 for this reason.
Windows 10, or arm64 hosts Only Windows 11 x64 was used. Runner images are linux/amd64; on arm64 they would run emulated or not at all.
Podman versions other than 6.0.2 The wrapper depends on the field .ConnectionInfo.PodmanPipe.Path existing in podman machine inspect. Older or newer releases may rename or drop it. The script fails loudly with Could not resolve the podman pipe path rather than doing something wrong. Get the value from podman system connection list and set DOCKER_HOST by hand.
An existing gh function or alias The profile hook defines function gh, which shadows the binary. If you already wrap gh, the two will conflict, and shell completion for gh may stop working. Install with -NoProfileHook and just use act.
A very long user PATH The installer appends InstallDir to the user PATH. Environments near the legacy length limit can truncate. Check act resolves in a new shell: Get-Command act.

If you hit any of these, act-doctor plus the step-4 probe (docker version --format '{{.Server.Os}}' should stay windows; the Podman pipe should report linux) will localise the problem to a layer quickly. Corrections welcome — they are defects in this document, not user error.

<#
.SYNOPSIS
Point nektos/act at Podman on a Windows host, without disturbing Docker Desktop.
.DESCRIPTION
act only ships Linux runner images. If Docker Desktop is running Windows
containers, its Linux engine is stopped and act cannot use it. Podman's
machine serves a Docker-API-compatible named pipe that act drives fine.
This script installs:
* <InstallDir>\act.ps1 - wrapper that scopes DOCKER_HOST to act
* <InstallDir>\act-doctor.ps1 - diagnostic
* %LOCALAPPDATA%\act\actrc - runner images (+ optional docker-in-workflow)
* an optional `gh act` hook in your PowerShell profile
DOCKER_HOST is deliberately NOT set globally. A global DOCKER_HOST would
redirect the `docker` CLI to Podman and break Windows-container work.
.PARAMETER InstallDir
Directory for the wrapper scripts. Must be (or will be added to) your user
PATH. Default: $HOME\.local\bin
.PARAMETER DockerInWorkflow
Bind-mount the Podman socket into job containers so workflow steps can run
`docker build` / `docker run`. This gives every workflow you execute full
control of your Podman engine. Off by default.
.PARAMETER NoProfileHook
Skip adding the `gh act` router to your PowerShell profile. The `act`
command still works: it is a PATH script, not a shell function.
.PARAMETER SkipSmokeTest
Do not run the end-to-end verification (which pulls ~500 MB on first use).
.PARAMETER Uninstall
Remove everything this script installed and restore any backed-up actrc.
.EXAMPLE
.\setup-act-podman.ps1
.EXAMPLE
.\setup-act-podman.ps1 -DockerInWorkflow
.EXAMPLE
.\setup-act-podman.ps1 -Uninstall
#>
[CmdletBinding()]
param(
[string]$InstallDir = "$HOME\.local\bin",
[switch]$DockerInWorkflow,
[switch]$NoProfileHook,
[switch]$SkipSmokeTest,
[switch]$Uninstall
)
$ErrorActionPreference = 'Stop'
function Step($m) { Write-Host "==> $m" -ForegroundColor Cyan }
function Ok($m) { Write-Host "[OK] $m" -ForegroundColor Green }
function Warn($m) { Write-Host "[!!] $m" -ForegroundColor Yellow }
function Die($m) { Write-Host "[XX] $m" -ForegroundColor Red; exit 1 }
$ActRcDir = Join-Path $env:LOCALAPPDATA 'act'
$ActRc = Join-Path $ActRcDir 'actrc'
$WrapperPs1 = Join-Path $InstallDir 'act.ps1'
$DoctorPs1 = Join-Path $InstallDir 'act-doctor.ps1'
$HookStart = '# >>> act-podman gh hook >>>'
$HookEnd = '# <<< act-podman gh hook <<<'
# --------------------------------------------------------------------------
# Uninstall
# --------------------------------------------------------------------------
if ($Uninstall) {
Step 'Removing wrapper scripts'
foreach ($f in @($WrapperPs1, $DoctorPs1)) {
if (Test-Path $f) { Remove-Item $f -Force; Ok "removed $f" } else { Write-Host " not present: $f" }
}
Step 'Restoring actrc'
if (Test-Path "$ActRc.bak") {
Move-Item "$ActRc.bak" $ActRc -Force; Ok "restored $ActRc from backup"
}
elseif (Test-Path $ActRc) {
Remove-Item $ActRc -Force; Ok "removed $ActRc (no backup existed)"
}
Step 'Removing profile hook'
$prof = $PROFILE.CurrentUserAllHosts
if (Test-Path $prof) {
$lines = Get-Content $prof
$s = ($lines | Select-String -SimpleMatch $HookStart | Select-Object -First 1).LineNumber
$e = ($lines | Select-String -SimpleMatch $HookEnd | Select-Object -First 1).LineNumber
if ($s -and $e -and $e -ge $s) {
Copy-Item $prof "$prof.bak-act" -Force
$keep = @()
if ($s -gt 1) { $keep += $lines[0..($s - 2)] }
if ($e -lt $lines.Count) { $keep += $lines[$e..($lines.Count - 1)] }
$keep | Set-Content $prof -Encoding utf8
Ok "removed hook from $prof (backup: $prof.bak-act)"
}
else { Write-Host ' no hook found' }
}
Write-Host ''
Ok 'Uninstalled. Podman, Docker Desktop and the gh-act extension were not touched.'
exit 0
}
# --------------------------------------------------------------------------
# 1. Preconditions
# --------------------------------------------------------------------------
Step 'Checking gh and the act extension'
$gh = (Get-Command gh.exe -ErrorAction SilentlyContinue).Source
if (-not $gh) { Die 'gh.exe not found on PATH. Install GitHub CLI: winget install GitHub.cli' }
if (-not (& $gh extension list 2>$null | Select-String -SimpleMatch 'nektos/gh-act')) {
Warn 'gh-act extension not installed; installing it now'
& $gh extension install https://github.com/nektos/gh-act
}
Ok ((& $gh extension list 2>$null | Select-String -SimpleMatch 'nektos/gh-act') -join ' ').Trim()
Step 'Checking Podman'
$podman = (Get-Command podman.exe -ErrorAction SilentlyContinue).Source
if (-not $podman) {
Die 'podman.exe not found on PATH. Install Podman first - act has no Linux runtime without it.'
}
Ok $podman
$machineState = (& $podman machine inspect --format '{{.State}}' 2>$null | Out-String).Trim()
if (-not $machineState) {
Die 'No podman machine exists. Create one with: podman machine init'
}
if ($machineState -ne 'running') {
Step "Podman machine is '$machineState'; starting it"
& $podman machine start | Out-Null
}
Ok "podman machine: $((& $podman machine inspect --format '{{.State}}' 2>$null | Out-String).Trim())"
# --------------------------------------------------------------------------
# 2. Baseline probe -- record what the runtimes report BEFORE the change
# --------------------------------------------------------------------------
Step 'Baseline: what each endpoint reports right now'
$dockerOs = (& docker version --format '{{.Server.Os}}' 2>&1 | Out-String).Trim()
Write-Host " docker CLI default -> $dockerOs"
$pipePath = (& $podman machine inspect --format '{{.ConnectionInfo.PodmanPipe.Path}}' 2>$null | Out-String).Trim()
if (-not $pipePath) { Die 'Could not resolve the podman pipe from `podman machine inspect`.' }
$dockerHost = 'npipe:////./pipe/' + (Split-Path $pipePath -Leaf)
$podmanOs = (& docker -H $dockerHost version --format '{{.Server.Os}}' 2>&1 | Out-String).Trim()
Write-Host " podman pipe -> $podmanOs ($dockerHost)"
if ($podmanOs -ne 'linux') {
Die "The podman pipe did not report a Linux engine (got: '$podmanOs'). act cannot work until it does."
}
if ($dockerOs -eq 'linux') {
Warn 'Your docker CLI already reaches a Linux engine, so plain act may already work.'
Warn 'Installing anyway is still safe: it pins act to Podman explicitly.'
}
# --------------------------------------------------------------------------
# 3. actrc -- runner images, and the docker-in-workflow socket
# --------------------------------------------------------------------------
Step "Writing $ActRc"
New-Item -ItemType Directory -Force -Path $ActRcDir | Out-Null
if ((Test-Path $ActRc) -and -not (Test-Path "$ActRc.bak")) {
Copy-Item $ActRc "$ActRc.bak" -Force
Warn "existing actrc backed up to $ActRc.bak"
}
$socketLine = if ($DockerInWorkflow) {
@'
# Docker-in-workflow ENABLED. act bind-mounts this into the job as its docker
# socket. The path is resolved by the DAEMON, inside the Podman VM -- not on
# Windows -- so it is a Linux path, not the npipe. Podman's machine already
# symlinks /var/run/docker.sock to podman.sock there.
# SECURITY: every workflow you run gets full control of your Podman engine.
--container-daemon-socket /var/run/docker.sock
'@
}
else {
@'
# Docker-in-workflow DISABLED. Without this, act would mount the value of
# DOCKER_HOST into the job -- here a Windows named pipe, meaningless to a Linux
# container. Re-run this installer with -DockerInWorkflow to enable it.
--container-daemon-socket -
'@
}
@"
# act configuration -- Windows host, Podman as the Linux container runtime.
# Generated by setup-act-podman.ps1. Safe to edit by hand.
#
# The daemon CONNECTION is not configured here: act reads it only from the
# DOCKER_HOST environment variable (it ignores docker contexts). The act.ps1
# wrapper sets that per-invocation.
# Runner images. This also answers act's interactive first-run image picker,
# which otherwise aborts with `level=fatal msg="Incorrect function."` in any
# non-interactive shell.
-P ubuntu-latest=catthehacker/ubuntu:act-latest
-P ubuntu-24.04=catthehacker/ubuntu:act-24.04
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
$socketLine
"@ | Set-Content $ActRc -Encoding utf8
Ok "actrc written (docker-in-workflow: $(if ($DockerInWorkflow) { 'enabled' } else { 'disabled' }))"
# --------------------------------------------------------------------------
# 4. Wrapper scripts on PATH
# --------------------------------------------------------------------------
Step "Installing wrapper scripts into $InstallDir"
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
@'
# act.ps1 -- run nektos/act against Podman on a Windows host.
# Generated by setup-act-podman.ps1.
#
# Lives on PATH rather than in a PowerShell profile so `act` also works under
# `pwsh -NoProfile`, from cmd.exe, from CI, and from editors that spawn a bare
# shell. DOCKER_HOST is scoped to this invocation and restored on exit, so the
# `docker` CLI keeps talking to Docker Desktop.
$ErrorActionPreference = 'Stop'
$podman = (Get-Command podman.exe -ErrorAction SilentlyContinue).Source
if (-not $podman) {
Write-Error 'podman.exe not found on PATH. act has no Linux container runtime without it.'
exit 1
}
$state = (& $podman machine inspect --format '{{.State}}' 2>$null | Out-String).Trim()
if (-not $state) { Write-Error 'No podman machine found. Run: podman machine init'; exit 1 }
if ($state -ne 'running') {
Write-Host "podman machine is '$state'; starting it..." -ForegroundColor Yellow
& $podman machine start | Out-Null
}
$pipe = (& $podman machine inspect --format '{{.ConnectionInfo.PodmanPipe.Path}}' 2>$null | Out-String).Trim()
if (-not $pipe) { Write-Error 'Could not resolve the podman pipe path.'; exit 1 }
$prev = $env:DOCKER_HOST
$had = Test-Path Env:\DOCKER_HOST
try {
$env:DOCKER_HOST = 'npipe:////./pipe/' + (Split-Path $pipe -Leaf)
& gh.exe act @args
$code = $LASTEXITCODE
}
finally {
if ($had) { $env:DOCKER_HOST = $prev }
else { Remove-Item Env:\DOCKER_HOST -ErrorAction SilentlyContinue }
}
exit $code
'@ | Set-Content $WrapperPs1 -Encoding utf8
Ok $WrapperPs1
@'
# act-doctor.ps1 -- diagnostic for the act-on-Podman setup.
# Generated by setup-act-podman.ps1.
$podman = (Get-Command podman.exe -ErrorAction SilentlyContinue).Source
$actrc = "$env:LOCALAPPDATA\act\actrc"
Write-Output ("docker CLI default -> " + (docker version --format '{{.Server.Os}}' 2>&1))
Write-Output ("podman on PATH -> " + $(if ($podman) { $podman } else { 'NOT FOUND' }))
if ($podman) {
Write-Output ("podman machine -> " + (& $podman machine inspect --format '{{.State}}' 2>&1))
$pipe = (& $podman machine inspect --format '{{.ConnectionInfo.PodmanPipe.Path}}' 2>$null | Out-String).Trim()
if ($pipe) {
$h = 'npipe:////./pipe/' + (Split-Path $pipe -Leaf)
Write-Output ("act DOCKER_HOST -> $h")
Write-Output (" engine reports -> " + (docker -H $h version --format '{{.Server.Os}} (API {{.Server.APIVersion}})' 2>&1))
}
else { Write-Output 'act DOCKER_HOST -> COULD NOT RESOLVE PIPE' }
}
Write-Output ("actrc -> $actrc " + $(if (Test-Path $actrc) { '[present]' } else { '[MISSING]' }))
if (Test-Path $actrc) {
(Get-Content $actrc) | Where-Object { $_ -match '^\s*-' } | ForEach-Object { Write-Output " $_" }
}
Write-Output ("act wrapper -> " + ((Get-Command act -ErrorAction SilentlyContinue).Source | Select-Object -First 1))
Write-Output ("gh act extension -> " + ((gh extension list 2>&1 | Select-String 'gh-act') -join ' '))
'@ | Set-Content $DoctorPs1 -Encoding utf8
Ok $DoctorPs1
# Ensure InstallDir is on the user PATH
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if (($userPath -split ';') -notcontains $InstallDir) {
[Environment]::SetEnvironmentVariable('Path', ($userPath.TrimEnd(';') + ';' + $InstallDir), 'User')
$env:Path = $env:Path + ';' + $InstallDir
Warn "$InstallDir was added to your user PATH. Open a new shell for it to stick."
}
else { Ok "$InstallDir already on user PATH" }
# --------------------------------------------------------------------------
# 5. Optional profile hook so `gh act` keeps working
# --------------------------------------------------------------------------
if (-not $NoProfileHook) {
Step 'Adding `gh act` router to your PowerShell profile'
$prof = $PROFILE.CurrentUserAllHosts
New-Item -ItemType Directory -Force -Path (Split-Path $prof) | Out-Null
if (-not (Test-Path $prof)) { New-Item -ItemType File -Path $prof | Out-Null }
if (Select-String -Path $prof -SimpleMatch $HookStart -Quiet) {
Ok 'hook already present'
}
else {
Copy-Item $prof "$prof.bak-act" -Force
Add-Content -Path $prof -Encoding utf8 -Value @"
$HookStart
# `act` itself is a PATH script and needs no profile. This only keeps the
# `gh act ...` spelling working; every other gh subcommand passes through.
function gh {
if (`$args.Count -gt 0 -and `$args[0] -eq 'act') {
& "$WrapperPs1" @(@(`$args) | Select-Object -Skip 1)
}
else { & gh.exe @args }
}
$HookEnd
"@
Ok "hook added to $prof (backup: $prof.bak-act)"
}
}
else { Write-Host ' skipped (-NoProfileHook)' }
# --------------------------------------------------------------------------
# 6. Smoke test -- the same probe as the baseline, now through act
# --------------------------------------------------------------------------
if ($SkipSmokeTest) {
Write-Host ''
Ok 'Installed. Smoke test skipped (-SkipSmokeTest). Run `act-doctor` to check.'
exit 0
}
Step 'Smoke test (first run pulls ~500 MB of runner image)'
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("act-smoke-" + [Guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Force -Path (Join-Path $tmp '.github\workflows') | Out-Null
@'
name: smoke
on: [push]
jobs:
hello:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "act reached"; uname -s; cat /etc/os-release | head -1
'@ | Set-Content (Join-Path $tmp '.github\workflows\smoke.yml') -Encoding utf8
Push-Location $tmp
try {
& git init -q 2>&1 | Out-Null
& git add -A 2>&1 | Out-Null
& git -c user.email=act@local -c user.name=act commit -qm smoke 2>&1 | Out-Null
& $WrapperPs1 push
$smokeCode = $LASTEXITCODE
}
finally {
Pop-Location
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Host ''
if ($smokeCode -eq 0) {
Step 'After-state (compare with the baseline above)'
Write-Host " docker CLI default -> $((& docker version --format '{{.Server.Os}}' 2>&1 | Out-String).Trim()) (unchanged)"
Write-Host " act ran against -> $podmanOs ($dockerHost)"
Write-Host ''
Ok 'act is working on Podman. Run `act -l` in a repo to list its workflows.'
}
else {
Die "Smoke test failed (exit $smokeCode). Run `act-doctor` and see the Troubleshooting table."
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment