diff --git a/internal/deploy/ownership.go b/internal/deploy/ownership.go index f54e218..ce55293 100644 --- a/internal/deploy/ownership.go +++ b/internal/deploy/ownership.go @@ -9,7 +9,6 @@ import ( "os" "os/user" "strconv" - "syscall" ) // runningAsRoot reports whether the current process has uid 0. @@ -198,12 +197,13 @@ func lookupGID(groupname string) (int, error) { // unixOwnerFromStat extracts (uid, gid) from a Unix-style FileInfo. // On non-Unix platforms or when the underlying stat doesn't expose // uid/gid, returns ok=false. -func unixOwnerFromStat(fi os.FileInfo) (uid int, gid int, ok bool) { - if fi == nil { - return -1, -1, false - } - if sysStat, isUnix := fi.Sys().(*syscall.Stat_t); isUnix { - return int(sysStat.Uid), int(sysStat.Gid), true - } - return -1, -1, false -} +// +// Platform-specific implementations live in: +// - ownership_unix.go (//go:build unix — uses *syscall.Stat_t) +// - ownership_windows.go (//go:build windows — stub returns false) +// +// The split exists because syscall.Stat_t is Unix-only — Windows +// has no equivalent shape, so any production tsx that names it +// fails to compile on GOOS=windows. The cross-platform-build CI +// matrix caught this at Hotfix #16; the function was originally +// in this file pre-split. diff --git a/internal/deploy/ownership_unix.go b/internal/deploy/ownership_unix.go new file mode 100644 index 0000000..e4eb210 --- /dev/null +++ b/internal/deploy/ownership_unix.go @@ -0,0 +1,33 @@ +// Copyright 2026 certctl LLC. All rights reserved. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build unix + +// Unix-side implementation of unixOwnerFromStat. The `unix` build +// constraint (Go 1.19+) covers linux / darwin / freebsd / openbsd / +// netbsd / dragonfly / solaris — every GOOS where *syscall.Stat_t +// is a valid type assertion target for os.FileInfo.Sys(). +// +// Hotfix #16 (2026-05-14): pre-split, this function lived inline in +// ownership.go with an unconditional `syscall.Stat_t` reference. That +// failed `GOOS=windows go build` because the type is undefined on +// that platform. The split is the standard Go pattern — the same +// function name + signature is satisfied by either build of the +// package, callers don't know or care which. + +package deploy + +import ( + "os" + "syscall" +) + +func unixOwnerFromStat(fi os.FileInfo) (uid int, gid int, ok bool) { + if fi == nil { + return -1, -1, false + } + if sysStat, isUnix := fi.Sys().(*syscall.Stat_t); isUnix { + return int(sysStat.Uid), int(sysStat.Gid), true + } + return -1, -1, false +} diff --git a/internal/deploy/ownership_windows.go b/internal/deploy/ownership_windows.go new file mode 100644 index 0000000..de98777 --- /dev/null +++ b/internal/deploy/ownership_windows.go @@ -0,0 +1,35 @@ +// Copyright 2026 certctl LLC. All rights reserved. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build windows + +// Windows stub for unixOwnerFromStat. Windows has no uid/gid concept +// the way Unix does — file ownership is expressed via SIDs (Security +// Identifiers) and ACLs (Access Control Lists), and os.FileInfo.Sys() +// returns *syscall.Win32FileAttributeData which carries no +// ownership data the deploy package's existing call sites can use. +// +// All four callers — applyOwnership at ownership.go:75, +// preserveSourceOwner at atomic.go:237, and two test sites — already +// handle the ok=false return path by falling back to Plan.Defaults +// or the runtime's umask. Returning false here is the correct +// platform contract: "no native ownership available on this +// platform; use the supplied defaults." +// +// Hotfix #16 (2026-05-14): created to unblock the +// cross-platform-build Windows matrix in CI, which had been +// red since the agent's deploy package gained ownership- +// preservation semantics. The agent binary still compiles for +// Windows; ownership operations on Windows are no-ops (which +// matches operator expectations — the certctl-agent's +// chown/chmod codepaths gate on `runningAsRoot()` and Windows +// runs the agent as a service under a SID that doesn't +// translate to a uid anyway). + +package deploy + +import "os" + +func unixOwnerFromStat(_ os.FileInfo) (uid int, gid int, ok bool) { + return -1, -1, false +}