Files
fips/.github/workflows/package-windows.yml
Johnathan Corgan 4590b85e62 Fix package upgrade defects on Debian, FreeBSD and Windows
Each left a host misconfigured or without its daemon after an upgrade
or install.

Keep fips-dns working on a renamed TUN and respect a disabled fips-dns.

fips-dns-setup waited for an interface named fips0 before trying any
resolver backend, so on a host whose config sets tun.name to anything
else it timed out after 30 seconds and exited 1. Debian's postinst
starts fips-dns on upgrade and fails the configure when it does not come
up, so upgrading such a host left the package half-configured with apt
exiting 100. postinst also re-enabled fips-dns.service on every
configure, undoing an operator's disable on each upgrade or reinstall.

fips-dns-setup now reads the interface name from the daemon config
(tun.name under the top-level tun: block, falling back to fips0) and
waits for it only in the resolvectl backend, the one backend that uses
the interface; that backend records the interface in its state file and
fips-dns-teardown reverts it from there. postinst enables fips-dns only
on first install, or when the package is installed again after a remove
that disabled it while it was enabled: prerm's remove records under
/var/lib/fips whether fips-dns was enabled, postinst consumes the record,
and purge removes it. An older package's remove left no record, and its
postinst enabled fips-dns on every configure, so preinst treats an install
from config-files state with no record as enabled at remove; otherwise
removing 0.5.3 and installing this package would leave .fips names
unresolved.

Tests: a dns-resolver scenario with the TUN renamed to mesh0, a
container-free test of the name lookup run with the other static checks
in both CI runners, and two Debian upgrade scenarios covering the
renamed-TUN upgrade, a disable surviving a reinstall, and enablement
across remove and reinstall, including a remove that left no record.

Leave the running daemon alone on a FreeBSD package upgrade.

On an upgrade the old package's pre-deinstall stopped fips and fips_dns,
and the new package's post-install started them again when enabled. pkg
2.8.4 ends a process that a package script starts once pkg returns, even
under daemon(8), so the started daemon was gone by the time pkg exited:
every upgrade left the node with no daemon and without its .fips
resolver drop-in until the operator ran service fips restart.

pre-deinstall now stops the services and tears down the drop-in only on
removal, and post-install no longer starts anything. The running daemon
keeps its replaced binary until the operator restarts it, and the
package prints an upgrade message saying to run service fips restart
(which also starts a daemon that is not running) and service fips_dns
restart if it is used. Upgrades from a package before this one still
run that package's pre-deinstall, which stops the daemon, so they still
need the restart; the README says so.

packaging/freebsd/test-upgrade.sh upgrades over a running, enabled
daemon to a package built from the same binaries at version 99.0.0 and
checks that a daemon is still running up to 10 seconds after pkg
returns, that the restart instruction was printed, and that the restart
moves the daemon onto the new binary with one process. The FreeBSD
Package workflow runs it in its VM after the log rotation check.

Repoint an existing Windows service registration at the installed fips.exe.

When the fips service already existed, --install-service opened it and
set only its description, then reported success. A service registered
from another folder, by hand or by an older install, therefore kept
starting that folder's fips.exe after install-service.ps1 had copied the
new release into Program Files.

On ERROR_SERVICE_EXISTS, --install-service now reads the existing
configuration and rewrites it with this executable and --service,
keeping the service's start type and account, prints the old and new
binary paths when they differ, and says "updated" rather than
"installed". The service commands it, install-service.ps1 and the ZIP
README print are now sc.exe: in PowerShell a bare sc is the Set-Content
alias.

The Windows Package workflow gains a step that registers the service
from C:\fips-old, runs install-service.ps1 under Windows PowerShell, and
requires the registration to name Program Files' fips.exe with
--service, and the ZIP check now fails on a README line that names a
bare sc.
2026-10-23 00:33:15 +00:00

272 lines
9.6 KiB
YAML

name: Windows Package
on:
push:
branches:
- master
- maint
- next
tags:
- "v*"
pull_request:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
determine-versioning:
runs-on: windows-latest
outputs:
package_version: ${{ steps.version.outputs.package_version }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Derive package version
id: version
shell: pwsh
run: |
$cargoToml = Get-Content Cargo.toml -Raw
if ($cargoToml -match 'version\s*=\s*"([^"]+)"') {
$baseVersion = $Matches[1]
} else {
throw "Could not determine version from Cargo.toml"
}
if ($env:GITHUB_REF -like "refs/tags/*") {
$version = $env:GITHUB_REF_NAME -replace '^v', ''
} else {
$branch = $env:GITHUB_REF_NAME -replace '[^A-Za-z0-9]', '.' -replace '\.\.+', '.' -replace '^\.|\.$$', ''
$height = git rev-list --count HEAD
$hash = git rev-parse --short HEAD
if (-not $branch) { $branch = "ref" }
$version = "$baseVersion+$branch.$height.$hash"
}
echo "package_version=$version" >> $env:GITHUB_OUTPUT
build:
name: Build Windows package
runs-on: windows-latest
needs: determine-versioning
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Set SOURCE_DATE_EPOCH from git
shell: pwsh
run: |
$epoch = git log -1 --format=%ct
echo "SOURCE_DATE_EPOCH=$epoch" >> $env:GITHUB_ENV
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
with:
cache: false
rustflags: ''
- name: Cache Cargo registry + build
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: windows-release-x86_64-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
windows-release-x86_64-
- name: Build release binaries
run: cargo build --release
- name: Build Windows package
shell: pwsh
run: |
powershell -File packaging/windows/build-zip.ps1 `
-Version "${{ needs.determine-versioning.outputs.package_version }}" `
-NoBuild
- name: Verify ZIP structural correctness
shell: pwsh
run: |
$zip = Get-ChildItem deploy\fips-*-windows-*.zip | Select-Object -First 1
if (-not $zip) { Write-Error "No ZIP artifact found in deploy\"; exit 1 }
Write-Host "Verifying: $($zip.Name)"
$extractDir = "verify-extract"
if (Test-Path $extractDir) { Remove-Item -Recurse -Force $extractDir }
Expand-Archive -Path $zip.FullName -DestinationPath $extractDir
# Expected top-level files (flat ZIP, no wrapper directory).
# Source of truth: packaging/windows/build-zip.ps1
$expected = @(
"fips.exe",
"fipsctl.exe",
"fipstop.exe",
"fips.yaml",
"hosts",
"install-service.ps1",
"uninstall-service.ps1",
"README.txt"
)
$missing = @()
foreach ($f in $expected) {
$path = Join-Path $extractDir $f
if (Test-Path -LiteralPath $path -PathType Leaf) {
Write-Host "PASS: $f"
} else {
Write-Host "FAIL: $f"
$missing += $f
}
}
Write-Host ""
Write-Host "ZIP contents:"
Get-ChildItem -Path $extractDir -Recurse | ForEach-Object {
Write-Host " $($_.FullName.Substring((Resolve-Path $extractDir).Path.Length + 1))"
}
if ($missing.Count -gt 0) {
Write-Error "Missing expected files: $($missing -join ', ')"
exit 1
}
Write-Host ""
Write-Host "All expected files present."
# The service control command is sc.exe. In PowerShell, where the
# README tells the operator to run these, a bare `sc` is the
# Set-Content alias, so `sc stop fips` writes a file instead.
$bareSc = Select-String -Path (Join-Path $extractDir "README.txt") -Pattern '^\s*sc (start|stop|query)\b'
if ($bareSc) {
$bareSc | ForEach-Object { Write-Host "FAIL: README.txt line $($_.LineNumber): $($_.Line)" }
Write-Error "README.txt names the service command as a bare sc, not sc.exe"
exit 1
}
Write-Host "PASS: README.txt names sc.exe, not a bare sc"
# A service registered from another folder must be repointed at the
# installed fips.exe when install-service.ps1 runs, or the node keeps
# starting the old binary. Runner jobs are elevated and the VM is thrown
# away, so this registers and removes a real service. The installer runs
# under Windows PowerShell, the edition it targets. It runs whenever the
# ZIP was extracted, so a failed README check above does not hide it.
- name: Installer repoints an existing service registration at the installed fips.exe
if: ${{ !cancelled() && hashFiles('verify-extract/fips.exe') != '' }}
shell: pwsh
run: |
function Get-BinaryPath {
$qc = & sc.exe qc fips
if ($LASTEXITCODE -ne 0) {
Write-Error "sc.exe qc fips exited $LASTEXITCODE`n$($qc -join "`n")"
exit 1
}
$line = $qc | Where-Object { $_ -match '^\s*BINARY_PATH_NAME\s*:' } | Select-Object -First 1
if (-not $line) {
Write-Error "sc.exe qc fips printed no BINARY_PATH_NAME`n$($qc -join "`n")"
exit 1
}
return ($line -replace '^\s*BINARY_PATH_NAME\s*:\s*', '')
}
$old = "C:\fips-old"
New-Item -ItemType Directory -Force -Path $old | Out-Null
Copy-Item verify-extract\fips.exe "$old\fips.exe" -Force
& "$old\fips.exe" --install-service
if ($LASTEXITCODE -ne 0) { Write-Error "registering the service from $old exited $LASTEXITCODE"; exit 1 }
$before = Get-BinaryPath
Write-Host "Registered from ${old}: $before"
# Precondition: the registration this check repoints exists and
# names the old folder.
if ($before -notlike "*$old\fips.exe*") {
Write-Error "precondition failed: the service does not name $old\fips.exe"
exit 1
}
powershell -File verify-extract\install-service.ps1
$installRc = $LASTEXITCODE
$after = Get-BinaryPath
Write-Host "After install-service.ps1: $after"
$want = "$env:ProgramFiles\fips\fips.exe"
& "$want" --uninstall-service
if ($installRc -ne 0) { Write-Error "install-service.ps1 exited $installRc"; exit 1 }
if (($after -notlike "*$want*") -or ($after -notlike "*--service*")) {
Write-Error "install-service.ps1 left the service at $after; want $want --service"
exit 1
}
Write-Host "PASS: install-service.ps1 repointed the service at $after"
- name: SHA-256 hash
shell: pwsh
run: |
Write-Host "==> Windows release asset:"
Get-ChildItem deploy\fips-*-windows-*.zip | ForEach-Object {
Get-FileHash $_.FullName -Algorithm SHA256 | Format-Table -AutoSize
}
- name: Upload artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: fips_${{ needs.determine-versioning.outputs.package_version }}_x86_64_windows
path: deploy/fips-*-windows-*.zip
retention-days: 30
- name: Build summary
shell: pwsh
run: |
$pkg = Get-ChildItem deploy\fips-*-windows-*.zip | Select-Object -First 1
Write-Host "Build Summary for Windows/x86_64:"
Write-Host " Package: $($pkg.Name)"
Write-Host " Size: $([math]::Round($pkg.Length / 1MB, 2)) MB"
release:
name: Publish Windows assets to GitHub Release
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download Windows artifacts
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
with:
path: dist
merge-multiple: true
- name: Generate Windows release checksums
run: |
cd dist
find . -maxdepth 1 -type f -name '*.zip' -printf '%P\n' \
| LC_ALL=C sort \
| xargs sha256sum \
> checksums-windows.txt
- name: Wait for tag release
env:
GH_TOKEN: ${{ github.token }}
run: |
for attempt in $(seq 1 20); do
if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
exit 0
fi
echo "Release ${GITHUB_REF_NAME} not available yet; waiting..."
sleep 15
done
echo "Timed out waiting for release ${GITHUB_REF_NAME}" >&2
exit 1
- name: Upload Windows assets
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${GITHUB_REF_NAME}" \
dist/*.zip \
dist/checksums-windows.txt \
--clobber \
--repo "${GITHUB_REPOSITORY}"