diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c00fb52..4d40bc8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,6 +21,18 @@ jobs: - name: Test run: go test ./... -v + # The release builds for every OS/arch; the host-only build above can miss + # platform-specific code (e.g. syscall fields). Cross-compile the same + # targets goreleaser does so a release-breaking compile error fails CI first. + - name: Cross-compile release targets + run: | + for os in linux windows darwin; do + for arch in amd64 arm64; do + echo "build $os/$arch" + CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build ./... || exit 1 + done + done + crd-drift: name: Embedded CRDs in sync with butler-api runs-on: ubuntu-latest diff --git a/internal/ctl/cluster/detach_unix.go b/internal/ctl/cluster/detach_unix.go new file mode 100644 index 0000000..78d94cb --- /dev/null +++ b/internal/ctl/cluster/detach_unix.go @@ -0,0 +1,28 @@ +/* +Copyright 2026 The Butler Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//go:build !windows + +package cluster + +import "syscall" + +// detachSysProcAttr puts the child in a new session so it survives the parent +// process exiting (the kubeconfig command returns; the port-forward keeps running +// for the follow-up kubectl invocation). +func detachSysProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{Setsid: true} +} diff --git a/internal/ctl/cluster/detach_windows.go b/internal/ctl/cluster/detach_windows.go new file mode 100644 index 0000000..b1168fb --- /dev/null +++ b/internal/ctl/cluster/detach_windows.go @@ -0,0 +1,33 @@ +/* +Copyright 2026 The Butler Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//go:build windows + +package cluster + +import "syscall" + +// detachSysProcAttr starts the child in a new process group, detached from the +// parent's console, so it survives the parent process exiting (the kubeconfig +// command returns; the port-forward keeps running for the follow-up kubectl +// invocation). +func detachSysProcAttr() *syscall.SysProcAttr { + const ( + createNewProcessGroup = 0x00000200 // CREATE_NEW_PROCESS_GROUP + detachedProcess = 0x00000008 // DETACHED_PROCESS + ) + return &syscall.SysProcAttr{CreationFlags: createNewProcessGroup | detachedProcess} +} diff --git a/internal/ctl/cluster/portforward.go b/internal/ctl/cluster/portforward.go index d9e6501..577edf7 100644 --- a/internal/ctl/cluster/portforward.go +++ b/internal/ctl/cluster/portforward.go @@ -185,9 +185,10 @@ func ensureLocalForward(clusterName, tenantNS, service, mgmtKubeconfigPath, mgmt cmd.Stdout = logFile cmd.Stderr = logFile } - // Detach into a new session so the forward survives the CLI exiting; the - // follow-up `kubectl get nodes` runs as a separate process. - cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} + // Detach the forward so it survives the CLI exiting; the follow-up + // `kubectl get nodes` runs as a separate process. The detach mechanism is + // platform-specific (see detach_unix.go / detach_windows.go). + cmd.SysProcAttr = detachSysProcAttr() if err := cmd.Start(); err != nil { return 0, fmt.Errorf("starting tunnel (kubectl is required to use local clusters): %w", err) }