Skip to content

fix(task): declare create-only required fields #241

fix(task): declare create-only required fields

fix(task): declare create-only required fields #241

name: Windows Installer
on:
push:
branches:
- main
paths:
- 'scripts/**'
- 'packaging/**'
- 'desktop/windows/**'
- 'cmd/**'
- 'internal/**'
- 'go.mod'
- 'go.sum'
- '.github/workflows/windows-installer.yml'
workflow_dispatch:
inputs:
test_tag:
description: 'Optional temporary prerelease tag, for example v0.5.4-windows-test-20260802-1'
required: false
type: string
permissions:
contents: write
jobs:
validate:
name: Validate installer on Windows PowerShell 5.1
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Test Windows private storage
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
go test ./internal/envstore ./internal/fs/securepath
- name: Download and validate exact installer bytes
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$downloadedInstaller = Join-Path $env:RUNNER_TEMP 'install-downloaded.ps1'
$installerUrl = 'https://raw.githubusercontent.com/${{ github.repository }}/${{ github.sha }}/scripts/install/install.ps1'
& .\scripts\test\test-install-windows-download.ps1 `
-Url $installerUrl `
-ExpectedInstallerPath .\scripts\install\install.ps1 `
-OutputPath $downloadedInstaller
"DOWNLOADED_INSTALLER=$downloadedInstaller" | Out-File -FilePath $env:GITHUB_ENV -Encoding ascii -Append
- name: Run full install and in-place upgrade as standard user
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$releaseRoot = Join-Path $env:RUNNER_TEMP 'agentdock-current-release'
$distRoot = Join-Path $releaseRoot 'dist'
$bundleRoot = Join-Path $distRoot 'share\agentdock\core-skills'
New-Item -ItemType Directory -Path $distRoot -Force | Out-Null
$env:CGO_ENABLED = '0'
$env:GOOS = 'windows'
$env:GOARCH = 'amd64'
go build -trimpath -o (Join-Path $distRoot 'agentdock.exe') .\cmd\agentdock
$controlPanelOutput = Join-Path $distRoot 'control-panel'
dotnet publish .\desktop\windows\control-panel\AgentDock.ControlPanel.csproj `
-c Release `
-r win-x64 `
--self-contained true `
-o $controlPanelOutput
Copy-Item (Join-Path $controlPanelOutput 'agentdock-tray.exe') (Join-Path $distRoot 'agentdock-tray.exe') -Force
Copy-Item .\packaging\windows\assets\agentdock.ico (Join-Path $distRoot 'agentdock.ico') -Force
Copy-Item .\scripts\install\manage-windows.ps1 (Join-Path $distRoot 'manage-windows.ps1') -Force
python .\packaging\build-core-skill-bundle.py --output $bundleRoot
$assetName = 'agentdock_windows_amd64.zip'
$archivePath = Join-Path $releaseRoot $assetName
Compress-Archive `
-Path (Join-Path $distRoot 'agentdock.exe'), (Join-Path $distRoot 'agentdock-tray.exe'), (Join-Path $distRoot 'agentdock.ico'), (Join-Path $distRoot 'manage-windows.ps1'), (Join-Path $distRoot 'share') `
-DestinationPath $archivePath `
-Force
$hash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
[IO.File]::WriteAllText(
"$archivePath.sha256",
"$hash $assetName`n",
[Text.UTF8Encoding]::new($false)
)
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
$listener.Start()
$port = ([Net.IPEndPoint] $listener.LocalEndpoint).Port
$listener.Stop()
$releaseBaseUrl = "http://127.0.0.1:$port"
$server = Start-Process `
-FilePath 'python' `
-ArgumentList @('-m', 'http.server', $port, '--bind', '127.0.0.1', '--directory', $releaseRoot) `
-WindowStyle Hidden `
-PassThru
try {
$deadline = [DateTime]::UtcNow.AddSeconds(15)
do {
Start-Sleep -Milliseconds 250
try {
Invoke-WebRequest -UseBasicParsing -Uri "$releaseBaseUrl/$assetName.sha256" -TimeoutSec 2 | Out-Null
break
} catch {
if ([DateTime]::UtcNow -ge $deadline) { throw }
}
} while ($true)
& .\scripts\test\run-windows-installer-e2e-as-standard-user.ps1 `
-InstallerPath $env:DOWNLOADED_INSTALLER `
-Version latest `
-ReleaseBaseUrl $releaseBaseUrl
} finally {
Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue
}
- name: Build current Windows binary
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Path dist -Force | Out-Null
$env:CGO_ENABLED = '0'
$env:GOOS = 'windows'
$env:GOARCH = 'amd64'
go build -trimpath -o dist\agentdock.exe .\cmd\agentdock
dotnet publish .\desktop\windows\control-panel\AgentDock.ControlPanel.csproj `
-c Release `
-r win-x64 `
--self-contained true `
-o dist\control-panel
Copy-Item .\dist\control-panel\agentdock-tray.exe .\dist\agentdock-tray.exe -Force
Copy-Item .\scripts\install\manage-windows.ps1 .\dist\manage-windows.ps1 -Force
- name: Test configured Authenticode certificate
shell: pwsh
env:
WINDOWS_SIGNING_CERT_BASE64: ${{ secrets.WINDOWS_SIGNING_CERT_BASE64 }}
WINDOWS_SIGNING_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERT_PASSWORD }}
run: |
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERT_BASE64) -or
[string]::IsNullOrWhiteSpace($env:WINDOWS_SIGNING_CERT_PASSWORD)) {
if (-not [string]::IsNullOrWhiteSpace('${{ inputs.test_tag }}')) {
throw 'Windows signing secrets are required for a test package.'
}
Write-Host 'Windows signing secrets are not configured; skipping Authenticode integration test.'
exit 0
}
$paths = @('.\dist\agentdock.exe', '.\dist\agentdock-tray.exe')
.\packaging\windows\sign-windows.ps1 -Path $paths
.\packaging\windows\sign-windows.ps1 -Path $paths -VerifyOnly
- name: Prepare offline AMD64 payload
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$payloadRoot = Join-Path $env:RUNNER_TEMP 'agentdock-offline-amd64'
Remove-Item -LiteralPath $payloadRoot -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $payloadRoot -Force | Out-Null
python .\packaging\build-core-skill-bundle.py --output .\dist\share\agentdock\core-skills
Copy-Item .\packaging\windows\assets\agentdock.ico .\dist\agentdock.ico -Force
$archivePath = Join-Path $payloadRoot 'agentdock_windows_amd64.zip'
Compress-Archive `
-Path .\dist\agentdock.exe, .\dist\agentdock-tray.exe, .\dist\agentdock.ico, .\dist\manage-windows.ps1, .\dist\share `
-DestinationPath $archivePath `
-Force
$hash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
$checksumPath = "$archivePath.sha256"
[IO.File]::WriteAllText($checksumPath, "$hash agentdock_windows_amd64.zip`n", [Text.UTF8Encoding]::new($false))
$cloudflaredPath = Join-Path $payloadRoot 'cloudflared-windows-amd64.exe'
Invoke-WebRequest `
-UseBasicParsing `
-Uri 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe' `
-OutFile $cloudflaredPath
$cloudflaredSignature = Get-AuthenticodeSignature -LiteralPath $cloudflaredPath
if ($cloudflaredSignature.Status -ne [Management.Automation.SignatureStatus]::Valid) {
throw "cloudflared Authenticode signature is invalid: $($cloudflaredSignature.StatusMessage)"
}
"AMD64_ARCHIVE=$archivePath" | Out-File -FilePath $env:GITHUB_ENV -Encoding ascii -Append
"AMD64_CHECKSUM=$checksumPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding ascii -Append
"AMD64_CLOUDFLARED=$cloudflaredPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding ascii -Append
- name: Test Quick Tunnel restart lifecycle
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$fakeCloudflared = Join-Path $env:RUNNER_TEMP 'fake-cloudflared.exe'
$env:CGO_ENABLED = '0'
$env:GOOS = 'windows'
$env:GOARCH = 'amd64'
go build -trimpath -o $fakeCloudflared .\scripts\test\testdata\fake-cloudflared
& .\scripts\test\test-windows-quick-tunnel-lifecycle.ps1 `
-InstallerPath .\scripts\install\install.ps1 `
-UninstallerPath .\scripts\install\uninstall-windows.ps1 `
-AgentDockArchive $env:AMD64_ARCHIVE `
-AgentDockChecksumFile $env:AMD64_CHECKSUM `
-FakeCloudflaredBinary $fakeCloudflared
- name: Compile offline AMD64 Setup
shell: pwsh
env:
WINDOWS_SIGNING_CERT_BASE64: ${{ secrets.WINDOWS_SIGNING_CERT_BASE64 }}
WINDOWS_SIGNING_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERT_PASSWORD }}
run: |
$ErrorActionPreference = 'Stop'
$testTag = '${{ inputs.test_tag }}'
if ([string]::IsNullOrWhiteSpace($testTag)) {
$buildInfo = Get-Content -LiteralPath '.\internal\buildinfo\buildinfo.go' -Raw
$match = [regex]::Match($buildInfo, 'const\s+Version\s*=\s*"([^"]+)"')
if (-not $match.Success) { throw 'Could not read buildinfo.Version.' }
$version = $match.Groups[1].Value
} else {
if (-not $testTag.StartsWith('v')) { throw 'The test tag must start with v.' }
$version = $testTag.Substring(1)
}
$outputDir = Join-Path $env:RUNNER_TEMP 'agentdock-setup'
$parameters = @{
Version = $version
Architecture = 'amd64'
AgentDockArchive = $env:AMD64_ARCHIVE
AgentDockChecksumFile = $env:AMD64_CHECKSUM
CloudflaredBinary = $env:AMD64_CLOUDFLARED
OutputDirectory = $outputDir
}
if (-not [string]::IsNullOrWhiteSpace($testTag)) {
$parameters.SignedBuild = $true
}
& .\packaging\windows\build-windows-offline-setup.ps1 @parameters
- name: Run Setup install and uninstall E2E
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
& .\scripts\test\test-windows-setup-e2e.ps1 `
-SetupPath (Join-Path $env:RUNNER_TEMP 'agentdock-setup\AgentDockSetup-amd64.exe') `
-AllowLegacyTaskMutation
- name: Smoke test current binary as standard user
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
& .\scripts\test\test-windows-current-binary-as-standard-user.ps1 `
-BinaryPath .\dist\agentdock.exe
- name: Build and publish signed Windows test package
if: ${{ inputs.test_tag != '' }}
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
WINDOWS_SIGNING_CERT_BASE64: ${{ secrets.WINDOWS_SIGNING_CERT_BASE64 }}
WINDOWS_SIGNING_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERT_PASSWORD }}
TEST_TAG: ${{ inputs.test_tag }}
run: |
$ErrorActionPreference = 'Stop'
$releaseDir = Join-Path $env:RUNNER_TEMP 'agentdock-windows-test-release'
Remove-Item -LiteralPath $releaseDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $releaseDir -Force | Out-Null
Copy-Item -LiteralPath $env:AMD64_ARCHIVE -Destination $releaseDir -Force
Copy-Item -LiteralPath $env:AMD64_CHECKSUM -Destination $releaseDir -Force
function New-WindowsAsset {
param(
[Parameter(Mandatory = $true)][string] $Architecture,
[Parameter(Mandatory = $true)][string] $SourceDirectory
)
$assetName = "agentdock_windows_$Architecture.zip"
$assetPath = Join-Path $releaseDir $assetName
Compress-Archive `
-Path (Join-Path $SourceDirectory 'agentdock.exe'), (Join-Path $SourceDirectory 'agentdock-tray.exe'), (Join-Path $SourceDirectory 'agentdock.ico'), (Join-Path $SourceDirectory 'manage-windows.ps1'), (Join-Path $SourceDirectory 'share') `
-DestinationPath $assetPath `
-Force
$hash = (Get-FileHash -LiteralPath $assetPath -Algorithm SHA256).Hash.ToLowerInvariant()
[IO.File]::WriteAllText("$assetPath.sha256", "$hash $assetName`n", [Text.UTF8Encoding]::new($false))
}
$arm64Dir = Join-Path $env:RUNNER_TEMP 'agentdock-windows-arm64'
New-Item -ItemType Directory -Path $arm64Dir -Force | Out-Null
$env:CGO_ENABLED = '0'
$env:GOOS = 'windows'
$env:GOARCH = 'arm64'
go build -trimpath -o (Join-Path $arm64Dir 'agentdock.exe') .\cmd\agentdock
$arm64ControlPanelOutput = Join-Path $arm64Dir 'control-panel'
dotnet publish .\desktop\windows\control-panel\AgentDock.ControlPanel.csproj `
-c Release `
-r win-arm64 `
--self-contained true `
-o $arm64ControlPanelOutput
Copy-Item (Join-Path $arm64ControlPanelOutput 'agentdock-tray.exe') (Join-Path $arm64Dir 'agentdock-tray.exe') -Force
Copy-Item .\scripts\install\manage-windows.ps1 (Join-Path $arm64Dir 'manage-windows.ps1') -Force
.\packaging\windows\sign-windows.ps1 -Path @(
(Join-Path $arm64Dir 'agentdock.exe'),
(Join-Path $arm64Dir 'agentdock-tray.exe')
)
python .\packaging\build-core-skill-bundle.py --output (Join-Path $arm64Dir 'share\agentdock\core-skills')
Copy-Item .\packaging\windows\assets\agentdock.ico (Join-Path $arm64Dir 'agentdock.ico') -Force
New-WindowsAsset -Architecture 'arm64' -SourceDirectory $arm64Dir
$arm64Archive = Join-Path $releaseDir 'agentdock_windows_arm64.zip'
& .\packaging\windows\build-windows-offline-setup.ps1 `
-Version ($env:TEST_TAG.TrimStart('v')) `
-Architecture arm64 `
-AgentDockArchive $arm64Archive `
-AgentDockChecksumFile "$arm64Archive.sha256" `
-CloudflaredBinary $env:AMD64_CLOUDFLARED `
-OutputDirectory $releaseDir `
-SignedBuild
$amd64Setup = Join-Path $env:RUNNER_TEMP 'agentdock-setup\AgentDockSetup-amd64.exe'
Copy-Item -LiteralPath $amd64Setup -Destination $releaseDir -Force
foreach ($setupName in @('AgentDockSetup-amd64.exe', 'AgentDockSetup-arm64.exe')) {
$setupPath = Join-Path $releaseDir $setupName
$setupHash = (Get-FileHash -LiteralPath $setupPath -Algorithm SHA256).Hash.ToLowerInvariant()
[IO.File]::WriteAllText(
"$setupPath.sha256",
"$setupHash $setupName`n",
[Text.UTF8Encoding]::new($false)
)
}
gh release delete $env:TEST_TAG --repo $env:GITHUB_REPOSITORY --cleanup-tag --yes 2>$null
gh release create $env:TEST_TAG `
(Get-ChildItem -LiteralPath $releaseDir -File | ForEach-Object FullName) `
--repo $env:GITHUB_REPOSITORY `
--target $env:GITHUB_SHA `
--prerelease `
--title "AgentDock Windows test $env:TEST_TAG" `
--notes "Temporary Windows test build from commit $env:GITHUB_SHA. Uses the existing AgentDock self-signed Authenticode certificate."