Reduce supported platform matrix

This commit is contained in:
2026-09-07 03:28:50 +00:00
parent 1558c9ccae
commit f8e3e651e6
26 changed files with 32 additions and 387 deletions

View File

@@ -40,7 +40,7 @@ steps:
set -eu set -eu
output_dir="$(mktemp -d)" output_dir="$(mktemp -d)"
trap 'rm -rf "$output_dir"' EXIT trap 'rm -rf "$output_dir"' EXIT
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do for target in linux/amd64 linux/arm64 darwin/arm64; do
goos="${target%/*}" goos="${target%/*}"
goarch="${target#*/}" goarch="${target#*/}"
CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" go build -o "$output_dir/narratio-$goos-$goarch" ./cmd/narratio CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" go build -o "$output_dir/narratio-$goos-$goarch" ./cmd/narratio

View File

@@ -58,7 +58,7 @@ in the [Release Procedure](release.md).
Woodpecker also runs `go test -race -shuffle=on -count=3 ./...` on its scheduled Woodpecker also runs `go test -race -shuffle=on -count=3 ./...` on its scheduled
job to expose ordering and repeatability defects. Current runners cross-compile job to expose ordering and repeatability defects. Current runners cross-compile
for macOS and Windows, but do not provide native macOS or Windows execution. the supported Linux amd64 and arm64 targets and the best-effort macOS arm64
Those cross-builds establish compilation only, not platform-equivalent runtime target. The macOS cross-build establishes compilation only, not native runtime
evidence. Add native checks only when official runner labels and successful evidence. Add a native check only when an official runner label and successful
native-run evidence are available. native-run evidence are available.

View File

@@ -78,11 +78,10 @@ in-memory backend without changing package state.
- Subprocess adapters persist stage logs/generated configs through stage-managed paths. - Subprocess adapters persist stage logs/generated configs through stage-managed paths.
- Shared subprocess execution keeps private lifecycle mechanics separate from - Shared subprocess execution keeps private lifecycle mechanics separate from
Narratio's environment, diagnostics, redaction, and error policy. It starts Narratio's environment, diagnostics, redaction, and error policy. It starts
an owned process group on Linux/macOS or a an owned process group on Linux and macOS. Every terminal path disposes of
kill-on-close job object on Windows. Every terminal path disposes of that that owned tree before returning. After a natural leader exit, the runner checks
owned tree before returning. After a natural leader exit, Unix checks for for remaining group members and uses bounded graceful then forceful termination.
remaining group members and uses bounded graceful then forceful termination; Cancellation, deadlines, and
Windows closes the job so kill-on-close applies. Cancellation, deadlines, and
diagnostic limits use the same terminal disposal path without losing their diagnostic limits use the same terminal disposal path without losing their
original result classification. Child environments contain only the execution original result classification. Child environments contain only the execution
baseline and adapter-specified values; configured credentials are explicit baseline and adapter-specified values; configured credentials are explicit

View File

@@ -550,18 +550,11 @@ converge on `0664`. Narratio explicitly applies these modes so a restrictive
caller umask does not remove group write or setgid. It does not change file or caller umask does not remove group write or setgid. It does not change file or
directory ownership: the configured workspace's existing group is inherited. directory ownership: the configured workspace's existing group is inherited.
Windows does not implement POSIX mode bits or setgid semantics. Configure the
workspace, spool, and cache locations with an ACL that grants the collaborating
group read/write access, and configure credential locations with an ACL limited
to the intended credential owner. Do not use POSIX mode displays as evidence of
Windows access control.
API keys are credentials, not ordinary workspace data. Store them outside the API keys are credentials, not ordinary workspace data. Store them outside the
shared workspace or in a separately restricted credential location; ordinary shared workspace or in a separately restricted credential location; ordinary
workspace group access must never be treated as authorization to read keys. workspace group access must never be treated as authorization to read keys.
On POSIX, provision a credential directory as `0700` and credential files as Provision a credential directory as `0700` and credential files as `0600`;
`0600`; Narratio rejects group- or other-readable configured credential paths. Narratio rejects group- or other-readable configured credential paths.
On Windows, restrict the directory and files with ACLs to the credential owner.
External adapter results are individually bounded before Narratio validates or External adapter results are individually bounded before Narratio validates or
materializes them. These per-file limits do not reserve disk space: prevent hard materializes them. These per-file limits do not reserve disk space: prevent hard

View File

@@ -234,13 +234,16 @@ configuration, logs, reports, diagnostics, and Notarius artifacts—is
intentionally shareable with the configured workspace group. API-key material intentionally shareable with the configured workspace group. API-key material
is sensitive and is not covered by the ordinary workspace-sharing policy. is sensitive and is not covered by the ordinary workspace-sharing policy.
On POSIX systems, Narratio-created ordinary workspace directories converge on Narratio supports Linux on amd64 and arm64. macOS arm64 is best effort: it has
a cross-build gate and release asset, but no native runtime release gate.
Windows and macOS amd64 are unsupported and have no compatibility promise or
release asset.
On supported systems, Narratio-created ordinary workspace directories converge on
setgid `02775` and ordinary workspace files on `0664`, even when the caller's setgid `02775` and ordinary workspace files on `0664`, even when the caller's
umask is restrictive. This preserves the existing workspace group for nested umask is restrictive. This preserves the existing workspace group for nested
creation and atomic replacements without changing ownership. API-key storage creation and atomic replacements without changing ownership. API-key storage
uses a separate restrictive contract. On Windows, POSIX mode bits and setgid uses a separate restrictive contract.
are not authoritative; operators must provide the equivalent shared-group and
credential-restricted ACLs described in [Operations](../operations.md#workspace-permissions).
Raw secrets must not be stored in pipeline, campaign, or session YAML or written Raw secrets must not be stored in pipeline, campaign, or session YAML or written
to manifests, logs, generated configuration, reports, publish metadata, to manifests, logs, generated configuration, reports, publish metadata,

View File

@@ -58,14 +58,14 @@ scripts/check-release-candidate.sh vMAJOR.MINOR.PATCH
It validates the version and matching note, module hygiene, formatting, It validates the version and matching note, module hygiene, formatting,
whitespace, uncached tests, race tests, static checks, documentation, examples, whitespace, uncached tests, race tests, static checks, documentation, examples,
and six official cross-build assets. It uses `GOWORK=off`, does not contact and three official cross-build assets. It uses `GOWORK=off`, does not contact
application services, CI, or Gitea, and does not create tags or modify tracked application services, CI, or Gitea, and does not create tags or modify tracked
source. Fix any failure on `main`, commit it, push it normally, and rerun the source. Fix any failure on `main`, commit it, push it normally, and rerun the
checker. checker.
The checker builds Linux, macOS, and Windows assets for `amd64` and `arm64`. The checker builds Linux assets for `amd64` and `arm64` and a macOS asset for
Cross-builds prove compilation; they are not native macOS or Windows runtime `arm64`. The macOS cross-build proves compilation; it is not native macOS
evidence. runtime evidence.
## Publish the tag ## Publish the tag

View File

@@ -89,7 +89,10 @@ make that scope concrete:
## Stage 1 — Reduce the platform and release matrix ## Stage 1 — Reduce the platform and release matrix
Status: Pending Status: Complete — Reduced CI and release output to Linux amd64/arm64 and macOS
arm64, removed Windows-only runtime implementations and accommodations, and
verified focused tests, all three cross-builds, exact asset names, and embedded
version reporting.
Read: roadmap “Platform Contract”; `scripts/build-release-assets.sh`, Read: roadmap “Platform Contract”; `scripts/build-release-assets.sh`,
`scripts/check-release-candidate.sh`, `.woodpecker/`, `internal/releasecheck`, `scripts/check-release-candidate.sh`, `.woodpecker/`, `internal/releasecheck`,

View File

@@ -6,16 +6,12 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
) )
func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) { func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "success") t.Setenv("AUDITA_HELPER_MODE", "success")
@@ -133,9 +129,6 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
} }
func TestSubprocessRunnerMissingConfiguredCredentialFails(t *testing.T) { func TestSubprocessRunnerMissingConfiguredCredentialFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
runner := mustAuditaRunner(t, SubprocessRunnerConfig{ runner := mustAuditaRunner(t, SubprocessRunnerConfig{
Binary: writeAuditaHelperWrapper(t), Binary: writeAuditaHelperWrapper(t),
Timeout: mustParseAuditaDuration(t, "2s"), Timeout: mustParseAuditaDuration(t, "2s"),
@@ -157,9 +150,6 @@ func TestSubprocessRunnerMissingConfiguredCredentialFails(t *testing.T) {
} }
func TestSubprocessRunnerUnconfiguredCredentialEnvOmitsCredential(t *testing.T) { func TestSubprocessRunnerUnconfiguredCredentialEnvOmitsCredential(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "success") t.Setenv("AUDITA_HELPER_MODE", "success")
recordPath := filepath.Join(t.TempDir(), "record.json") recordPath := filepath.Join(t.TempDir(), "record.json")
@@ -190,9 +180,6 @@ func TestSubprocessRunnerUnconfiguredCredentialEnvOmitsCredential(t *testing.T)
} }
func TestSubprocessRunnerOmitsUnspecifiedParentEnvironment(t *testing.T) { func TestSubprocessRunnerOmitsUnspecifiedParentEnvironment(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "success") t.Setenv("AUDITA_HELPER_MODE", "success")
t.Setenv("AUDITA_INHERITED_MARKER", "inherited-from-parent") t.Setenv("AUDITA_INHERITED_MARKER", "inherited-from-parent")
@@ -220,9 +207,6 @@ func TestSubprocessRunnerOmitsUnspecifiedParentEnvironment(t *testing.T) {
} }
func TestSubprocessRunnerOmitsModulesFlagWhenNotConfigured(t *testing.T) { func TestSubprocessRunnerOmitsModulesFlagWhenNotConfigured(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "success") t.Setenv("AUDITA_HELPER_MODE", "success")
recordPath := filepath.Join(t.TempDir(), "record.json") recordPath := filepath.Join(t.TempDir(), "record.json")
@@ -249,9 +233,6 @@ func TestSubprocessRunnerOmitsModulesFlagWhenNotConfigured(t *testing.T) {
} }
func TestSubprocessRunnerOmitsBaseURLAndModelFlagsWhenNotConfigured(t *testing.T) { func TestSubprocessRunnerOmitsBaseURLAndModelFlagsWhenNotConfigured(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "success") t.Setenv("AUDITA_HELPER_MODE", "success")
recordPath := filepath.Join(t.TempDir(), "record.json") recordPath := filepath.Join(t.TempDir(), "record.json")
@@ -279,9 +260,6 @@ func TestSubprocessRunnerOmitsBaseURLAndModelFlagsWhenNotConfigured(t *testing.T
} }
func TestSubprocessRunnerSubprocessFailure(t *testing.T) { func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "fail") t.Setenv("AUDITA_HELPER_MODE", "fail")
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
@@ -313,9 +291,6 @@ func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
} }
func TestSubprocessRunnerSubprocessFailureAddsStderrDescriptorHint(t *testing.T) { func TestSubprocessRunnerSubprocessFailureAddsStderrDescriptorHint(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "fail_bad_fd") t.Setenv("AUDITA_HELPER_MODE", "fail_bad_fd")
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
@@ -341,9 +316,6 @@ func TestSubprocessRunnerSubprocessFailureAddsStderrDescriptorHint(t *testing.T)
} }
func TestSubprocessRunnerMissingOutputFails(t *testing.T) { func TestSubprocessRunnerMissingOutputFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "missing_output") t.Setenv("AUDITA_HELPER_MODE", "missing_output")
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
@@ -369,9 +341,6 @@ func TestSubprocessRunnerMissingOutputFails(t *testing.T) {
} }
func TestSubprocessRunnerInvalidOutputJSONFails(t *testing.T) { func TestSubprocessRunnerInvalidOutputJSONFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "invalid_output") t.Setenv("AUDITA_HELPER_MODE", "invalid_output")
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
@@ -397,9 +366,6 @@ func TestSubprocessRunnerInvalidOutputJSONFails(t *testing.T) {
} }
func TestSubprocessRunnerSegmentsMissingFails(t *testing.T) { func TestSubprocessRunnerSegmentsMissingFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "segments_missing") t.Setenv("AUDITA_HELPER_MODE", "segments_missing")
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
@@ -425,9 +391,6 @@ func TestSubprocessRunnerSegmentsMissingFails(t *testing.T) {
} }
func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) { func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_AUDITA_HELPER", "1") t.Setenv("GO_WANT_AUDITA_HELPER", "1")
t.Setenv("AUDITA_HELPER_MODE", "invalid_report") t.Setenv("AUDITA_HELPER_MODE", "invalid_report")
t.Setenv("OPENAI_KEY_SOURCE", "super-secret") t.Setenv("OPENAI_KEY_SOURCE", "super-secret")

View File

@@ -6,16 +6,12 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
) )
func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *testing.T) { func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1") t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1")
t.Setenv("SCRIPTORIUM_HELPER_MODE", "run_success") t.Setenv("SCRIPTORIUM_HELPER_MODE", "run_success")
@@ -95,9 +91,6 @@ func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *tes
} }
func TestSubprocessRunnerRunExitCodeOneFails(t *testing.T) { func TestSubprocessRunnerRunExitCodeOneFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1") t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1")
t.Setenv("SCRIPTORIUM_HELPER_MODE", "fail_exit1") t.Setenv("SCRIPTORIUM_HELPER_MODE", "fail_exit1")
t.Setenv("SCRIPTORIUM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SCRIPTORIUM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -118,9 +111,6 @@ func TestSubprocessRunnerRunExitCodeOneFails(t *testing.T) {
} }
func TestSubprocessRunnerRunExitCodeTwoReturnsValidationFailureAndPreservesOutput(t *testing.T) { func TestSubprocessRunnerRunExitCodeTwoReturnsValidationFailureAndPreservesOutput(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1") t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1")
t.Setenv("SCRIPTORIUM_HELPER_MODE", "fail_exit2_with_output") t.Setenv("SCRIPTORIUM_HELPER_MODE", "fail_exit2_with_output")
t.Setenv("SCRIPTORIUM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SCRIPTORIUM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -144,9 +134,6 @@ func TestSubprocessRunnerRunExitCodeTwoReturnsValidationFailureAndPreservesOutpu
} }
func TestSubprocessRunnerRunMissingOutputOnSuccessFails(t *testing.T) { func TestSubprocessRunnerRunMissingOutputOnSuccessFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1") t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1")
t.Setenv("SCRIPTORIUM_HELPER_MODE", "run_success_missing_output") t.Setenv("SCRIPTORIUM_HELPER_MODE", "run_success_missing_output")
t.Setenv("SCRIPTORIUM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SCRIPTORIUM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -166,9 +153,6 @@ func TestSubprocessRunnerRunMissingOutputOnSuccessFails(t *testing.T) {
} }
func TestSubprocessRunnerRenderSuccess(t *testing.T) { func TestSubprocessRunnerRenderSuccess(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1") t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1")
t.Setenv("SCRIPTORIUM_HELPER_MODE", "render_success") t.Setenv("SCRIPTORIUM_HELPER_MODE", "render_success")
recordPath := filepath.Join(t.TempDir(), "record.json") recordPath := filepath.Join(t.TempDir(), "record.json")

View File

@@ -6,16 +6,12 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
) )
func TestSubprocessRunnerSuccessWithReportArgsAndEnv(t *testing.T) { func TestSubprocessRunnerSuccessWithReportArgsAndEnv(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "success") t.Setenv("SERIATIM_HELPER_MODE", "success")
@@ -131,9 +127,6 @@ func TestSubprocessRunnerSuccessWithReportArgsAndEnv(t *testing.T) {
} }
func TestSubprocessRunnerSubprocessFailure(t *testing.T) { func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "fail") t.Setenv("SERIATIM_HELPER_MODE", "fail")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -153,9 +146,6 @@ func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
} }
func TestSubprocessRunnerMissingOutputFails(t *testing.T) { func TestSubprocessRunnerMissingOutputFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "missing_output") t.Setenv("SERIATIM_HELPER_MODE", "missing_output")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -172,9 +162,6 @@ func TestSubprocessRunnerMissingOutputFails(t *testing.T) {
} }
func TestSubprocessRunnerInvalidOutputJSONFails(t *testing.T) { func TestSubprocessRunnerInvalidOutputJSONFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "invalid_output") t.Setenv("SERIATIM_HELPER_MODE", "invalid_output")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -191,9 +178,6 @@ func TestSubprocessRunnerInvalidOutputJSONFails(t *testing.T) {
} }
func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) { func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "invalid_report") t.Setenv("SERIATIM_HELPER_MODE", "invalid_report")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -210,9 +194,6 @@ func TestSubprocessRunnerInvalidReportJSONFails(t *testing.T) {
} }
func TestSubprocessRunnerTrimSuccessInvocationAndProvenance(t *testing.T) { func TestSubprocessRunnerTrimSuccessInvocationAndProvenance(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "trim_success") t.Setenv("SERIATIM_HELPER_MODE", "trim_success")
@@ -297,9 +278,6 @@ func TestSubprocessRunnerTrimSuccessInvocationAndProvenance(t *testing.T) {
} }
func TestSubprocessRunnerTrimSubprocessFailure(t *testing.T) { func TestSubprocessRunnerTrimSubprocessFailure(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "fail") t.Setenv("SERIATIM_HELPER_MODE", "fail")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -319,9 +297,6 @@ func TestSubprocessRunnerTrimSubprocessFailure(t *testing.T) {
} }
func TestSubprocessRunnerTrimMissingOutputFails(t *testing.T) { func TestSubprocessRunnerTrimMissingOutputFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "missing_output") t.Setenv("SERIATIM_HELPER_MODE", "missing_output")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -338,9 +313,6 @@ func TestSubprocessRunnerTrimMissingOutputFails(t *testing.T) {
} }
func TestSubprocessRunnerTrimInvalidOutputJSONFails(t *testing.T) { func TestSubprocessRunnerTrimInvalidOutputJSONFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "invalid_output") t.Setenv("SERIATIM_HELPER_MODE", "invalid_output")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -357,9 +329,6 @@ func TestSubprocessRunnerTrimInvalidOutputJSONFails(t *testing.T) {
} }
func TestSubprocessRunnerTrimOutputMissingSegmentsFails(t *testing.T) { func TestSubprocessRunnerTrimOutputMissingSegmentsFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "trim_missing_segments") t.Setenv("SERIATIM_HELPER_MODE", "trim_missing_segments")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -376,9 +345,6 @@ func TestSubprocessRunnerTrimOutputMissingSegmentsFails(t *testing.T) {
} }
func TestSubprocessRunnerNormalizeSuccessInvocationAndProvenance(t *testing.T) { func TestSubprocessRunnerNormalizeSuccessInvocationAndProvenance(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "normalize_success") t.Setenv("SERIATIM_HELPER_MODE", "normalize_success")
@@ -453,9 +419,6 @@ func TestSubprocessRunnerNormalizeSuccessInvocationAndProvenance(t *testing.T) {
} }
func TestSubprocessRunnerNormalizeSubprocessFailure(t *testing.T) { func TestSubprocessRunnerNormalizeSubprocessFailure(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "fail") t.Setenv("SERIATIM_HELPER_MODE", "fail")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -475,9 +438,6 @@ func TestSubprocessRunnerNormalizeSubprocessFailure(t *testing.T) {
} }
func TestSubprocessRunnerNormalizeMissingOutputFails(t *testing.T) { func TestSubprocessRunnerNormalizeMissingOutputFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "normalize_missing_output") t.Setenv("SERIATIM_HELPER_MODE", "normalize_missing_output")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -494,9 +454,6 @@ func TestSubprocessRunnerNormalizeMissingOutputFails(t *testing.T) {
} }
func TestSubprocessRunnerNormalizeInvalidOutputJSONFails(t *testing.T) { func TestSubprocessRunnerNormalizeInvalidOutputJSONFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "normalize_invalid_output") t.Setenv("SERIATIM_HELPER_MODE", "normalize_invalid_output")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -513,9 +470,6 @@ func TestSubprocessRunnerNormalizeInvalidOutputJSONFails(t *testing.T) {
} }
func TestSubprocessRunnerNormalizeOutputMissingSegmentsFails(t *testing.T) { func TestSubprocessRunnerNormalizeOutputMissingSegmentsFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "normalize_missing_segments") t.Setenv("SERIATIM_HELPER_MODE", "normalize_missing_segments")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -532,9 +486,6 @@ func TestSubprocessRunnerNormalizeOutputMissingSegmentsFails(t *testing.T) {
} }
func TestSubprocessRunnerNormalizeReportMissingFails(t *testing.T) { func TestSubprocessRunnerNormalizeReportMissingFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "normalize_report_missing") t.Setenv("SERIATIM_HELPER_MODE", "normalize_report_missing")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -551,9 +502,6 @@ func TestSubprocessRunnerNormalizeReportMissingFails(t *testing.T) {
} }
func TestSubprocessRunnerNormalizeInvalidReportJSONFails(t *testing.T) { func TestSubprocessRunnerNormalizeInvalidReportJSONFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "normalize_invalid_report") t.Setenv("SERIATIM_HELPER_MODE", "normalize_invalid_report")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -570,9 +518,6 @@ func TestSubprocessRunnerNormalizeInvalidReportJSONFails(t *testing.T) {
} }
func TestSubprocessRunnerRenderSuccessInvocationAndProvenance(t *testing.T) { func TestSubprocessRunnerRenderSuccessInvocationAndProvenance(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "render_success") t.Setenv("SERIATIM_HELPER_MODE", "render_success")
@@ -639,9 +584,6 @@ func TestSubprocessRunnerRenderSuccessInvocationAndProvenance(t *testing.T) {
} }
func TestSubprocessRunnerRenderWithoutTitleOmitsTitleArg(t *testing.T) { func TestSubprocessRunnerRenderWithoutTitleOmitsTitleArg(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "render_success") t.Setenv("SERIATIM_HELPER_MODE", "render_success")
recordPath := filepath.Join(t.TempDir(), "record.json") recordPath := filepath.Join(t.TempDir(), "record.json")
@@ -663,9 +605,6 @@ func TestSubprocessRunnerRenderWithoutTitleOmitsTitleArg(t *testing.T) {
} }
func TestSubprocessRunnerRenderSubprocessFailure(t *testing.T) { func TestSubprocessRunnerRenderSubprocessFailure(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "fail") t.Setenv("SERIATIM_HELPER_MODE", "fail")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -682,9 +621,6 @@ func TestSubprocessRunnerRenderSubprocessFailure(t *testing.T) {
} }
func TestSubprocessRunnerRenderMissingOutputFails(t *testing.T) { func TestSubprocessRunnerRenderMissingOutputFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "missing_output") t.Setenv("SERIATIM_HELPER_MODE", "missing_output")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
@@ -701,9 +637,6 @@ func TestSubprocessRunnerRenderMissingOutputFails(t *testing.T) {
} }
func TestSubprocessRunnerRenderEmptyOutputFails(t *testing.T) { func TestSubprocessRunnerRenderEmptyOutputFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("helper wrapper script uses /bin/sh")
}
t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("GO_WANT_SERIATIM_HELPER", "1")
t.Setenv("SERIATIM_HELPER_MODE", "render_empty_output") t.Setenv("SERIATIM_HELPER_MODE", "render_empty_output")
t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))

View File

@@ -1,4 +1,4 @@
//go:build linux || darwin || windows //go:build linux || darwin
package subprocess package subprocess

View File

@@ -1,10 +1,9 @@
//go:build !linux && !darwin && !windows //go:build !linux && !darwin
package subprocess package subprocess
import ( import (
"fmt" "fmt"
"runtime"
) )
func newOwnedProcessTree() (ownedProcessTree, error) { func newOwnedProcessTree() (ownedProcessTree, error) {

View File

@@ -1,145 +0,0 @@
//go:build windows
package subprocess
import (
"errors"
"fmt"
"os/exec"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
type windowsProcessTree struct {
job windows.Handle
}
func newOwnedProcessTree() (ownedProcessTree, error) {
return &windowsProcessTree{}, nil
}
func (tree *windowsProcessTree) Start(cmd *exec.Cmd) error {
job, err := windows.CreateJobObject(nil, nil)
if err != nil {
return fmt.Errorf("create job object: %w", err)
}
limits := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{}
limits.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
if _, err := windows.SetInformationJobObject(job, windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&limits)), uint32(unsafe.Sizeof(limits))); err != nil {
_ = windows.CloseHandle(job)
return fmt.Errorf("configure job object: %w", err)
}
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: windows.CREATE_SUSPENDED}
if err := cmd.Start(); err != nil {
_ = windows.CloseHandle(job)
return err
}
var assignErr error
withHandleErr := cmd.Process.WithHandle(func(handle uintptr) {
assignErr = windows.AssignProcessToJobObject(job, windows.Handle(handle))
})
if withHandleErr != nil {
return failWindowsProcessStart(cmd, job, fmt.Errorf("acquire stable process handle for job assignment: %w", withHandleErr))
}
if assignErr != nil {
return failWindowsProcessStart(cmd, job, fmt.Errorf("assign process to job object: %w", assignErr))
}
if err := resumeInitialThread(uint32(cmd.Process.Pid)); err != nil {
return failWindowsProcessStart(cmd, job, fmt.Errorf("resume initial process thread: %w", err))
}
tree.job = job
return nil
}
func failWindowsProcessStart(cmd *exec.Cmd, job windows.Handle, setupErr error) error {
killErr := cmd.Process.Kill()
waitErr := cmd.Wait()
var exitErr *exec.ExitError
if killErr == nil && errors.As(waitErr, &exitErr) {
// A killed suspended child normally reports a non-zero exit from Wait;
// successful reaping is not an additional setup failure.
waitErr = nil
}
return joinErrors(
setupErr,
wrapWindowsProcessCleanupError("kill suspended child", killErr),
wrapWindowsProcessCleanupError("reap suspended child", waitErr),
wrapWindowsProcessCleanupError("close unowned job object", windows.CloseHandle(job)),
)
}
func wrapWindowsProcessCleanupError(operation string, err error) error {
if err == nil {
return nil
}
return fmt.Errorf("%s: %w", operation, err)
}
func resumeInitialThread(processID uint32) error {
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPTHREAD, 0)
if err != nil {
return fmt.Errorf("snapshot initial thread: %w", err)
}
defer func() { _ = windows.CloseHandle(snapshot) }()
entry := windows.ThreadEntry32{Size: uint32(unsafe.Sizeof(windows.ThreadEntry32{}))}
if err := windows.Thread32First(snapshot, &entry); err != nil {
return fmt.Errorf("find initial thread: %w", err)
}
for {
if entry.OwnerProcessID != processID {
// Keep enumerating until the suspended process's only initial thread
// is found.
} else {
thread, openErr := windows.OpenThread(windows.THREAD_SUSPEND_RESUME, false, entry.ThreadID)
if openErr != nil {
return fmt.Errorf("open initial thread: %w", openErr)
}
defer func() { _ = windows.CloseHandle(thread) }()
if _, resumeErr := windows.ResumeThread(thread); resumeErr != nil {
return fmt.Errorf("resume initial thread: %w", resumeErr)
}
return nil
}
if err := windows.Thread32Next(snapshot, &entry); err != nil {
if errors.Is(err, windows.ERROR_NO_MORE_FILES) {
break
}
return fmt.Errorf("find initial thread: %w", err)
}
}
return fmt.Errorf("find initial thread: no thread found for process %d", processID)
}
func (tree *windowsProcessTree) TerminateGracefully() error {
// Windows jobs have no portable graceful signal. Terminating the owned job
// is the safe fallback and prevents a descendant from escaping cleanup.
return tree.terminate()
}
func (tree *windowsProcessTree) TerminateForcefully() error {
return tree.terminate()
}
func (tree *windowsProcessTree) Dispose() error {
if tree.job == 0 {
return nil
}
err := windows.CloseHandle(tree.job)
tree.job = 0
return err
}
func (tree *windowsProcessTree) terminate() error {
if tree.job == 0 {
return nil
}
return windows.TerminateJobObject(tree.job, 1)
}

View File

@@ -8,7 +8,6 @@ import (
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
@@ -137,9 +136,6 @@ func TestRunFailureRedactsSensitiveTail(t *testing.T) {
} }
func TestRunRejectsSymlinkDiagnosticWithoutTruncatingTarget(t *testing.T) { func TestRunRejectsSymlinkDiagnosticWithoutTruncatingTarget(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("creating symlinks requires privileges that are not available on every Windows runner")
}
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
t.Fatalf("os.Executable() error = %v", err) t.Fatalf("os.Executable() error = %v", err)

View File

@@ -1,4 +1,4 @@
//go:build !windows //go:build linux || darwin
package app package app

View File

@@ -3,7 +3,6 @@ package app
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
@@ -120,9 +119,6 @@ func TestLoadSecretsFromConfigMissingDirFails(t *testing.T) {
} }
func TestLoadSecretsFromConfigUnreadableValidEntryFails(t *testing.T) { func TestLoadSecretsFromConfigUnreadableValidEntryFails(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink behavior differs on windows")
}
dir := t.TempDir() dir := t.TempDir()
ensureSecretDirectory(t, dir) ensureSecretDirectory(t, dir)
@@ -152,9 +148,6 @@ func TestLoadSecretsFromConfigUnreadableValidEntryFails(t *testing.T) {
} }
func TestLoadSecretsFromConfigRejectsUnsafeModes(t *testing.T) { func TestLoadSecretsFromConfigRejectsUnsafeModes(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows access control is verified with ACLs")
}
for _, tc := range []struct { for _, tc := range []struct {
name string name string
directory os.FileMode directory os.FileMode
@@ -213,9 +206,6 @@ func TestLoadSecretsFromConfigRejectsNonRegularAndOversizedEntries(t *testing.T)
} }
func TestLoadSecretsFromConfigRejectsSymlinkAndAncestorReplacement(t *testing.T) { func TestLoadSecretsFromConfigRejectsSymlinkAndAncestorReplacement(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink behavior differs on windows")
}
dir := t.TempDir() dir := t.TempDir()
outside := t.TempDir() outside := t.TempDir()
ensureSecretDirectory(t, dir) ensureSecretDirectory(t, dir)

View File

@@ -1,11 +0,0 @@
//go:build windows
package app
import "os"
// Windows access control is determined by ACLs rather than POSIX mode bits.
func validateSecretDirectoryPrivacy(_ os.FileInfo) error { return nil }
// Windows access control is determined by ACLs rather than POSIX mode bits.
func validateSecretFilePrivacy(_ os.FileInfo) error { return nil }

View File

@@ -4,7 +4,6 @@ import (
"errors" "errors"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -117,9 +116,6 @@ func TestInspectExtractionEvidenceClassifiesBundleStates(t *testing.T) {
paths.Root = t.TempDir() paths.Root = t.TempDir()
}}, }},
{name: "unsafe symlink", state: ExtractionEvidenceUnsafe, mutate: func(t *testing.T, _ *SessionPaths, m *manifest.Manifest) { {name: "unsafe symlink", state: ExtractionEvidenceUnsafe, mutate: func(t *testing.T, _ *SessionPaths, m *manifest.Manifest) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires privileges on Windows")
}
path := m.Stages["extract"].Outputs[0].LocalPath path := m.Stages["extract"].Outputs[0].LocalPath
outside := filepath.Join(t.TempDir(), "outside.json") outside := filepath.Join(t.TempDir(), "outside.json")
writeExtractionFixtureFile(t, outside, `{"outside":true}`) writeExtractionFixtureFile(t, outside, `{"outside":true}`)

View File

@@ -1,11 +1,10 @@
//go:build !linux && !darwin && !windows //go:build !linux && !darwin
package artifacts package artifacts
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
) )
func acquireHeldFileLock(_ *os.File) error { func acquireHeldFileLock(_ *os.File) error {

View File

@@ -1,29 +0,0 @@
//go:build windows
package artifacts
import (
"errors"
"os"
"golang.org/x/sys/windows"
)
func acquireHeldFileLock(file *os.File) error {
err := windows.LockFileEx(
windows.Handle(file.Fd()),
windows.LOCKFILE_EXCLUSIVE_LOCK|windows.LOCKFILE_FAIL_IMMEDIATELY,
0,
1,
0,
&windows.Overlapped{},
)
if errors.Is(err, windows.ERROR_LOCK_VIOLATION) {
return errHeldLockConflict
}
return err
}
func releaseHeldFileLock(file *os.File) error {
return windows.UnlockFileEx(windows.Handle(file.Fd()), 0, 1, 0, &windows.Overlapped{})
}

View File

@@ -4,7 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@@ -277,9 +276,6 @@ func TestLoadPipelineCompositionRejectsUnsafeOrInvalidImports(t *testing.T) {
func TestLoadPipelineCompositionRejectsSameFileAliasesAndImportedComposition(t *testing.T) { func TestLoadPipelineCompositionRejectsSameFileAliasesAndImportedComposition(t *testing.T) {
t.Run("same file through hard link", func(t *testing.T) { t.Run("same file through hard link", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("hard-link identity behavior is platform-specific")
}
dir := t.TempDir() dir := t.TempDir()
writePipelineSource(t, dir, "one.yml", "workspace:\n root: /work\n") writePipelineSource(t, dir, "one.yml", "workspace:\n root: /work\n")
if err := os.Link(filepath.Join(dir, "one.yml"), filepath.Join(dir, "two.yml")); err != nil { if err := os.Link(filepath.Join(dir, "one.yml"), filepath.Join(dir, "two.yml")); err != nil {

View File

@@ -1,4 +1,4 @@
//go:build !windows //go:build linux || darwin
package fileops package fileops

View File

@@ -124,12 +124,9 @@ func TestAssetBuilderBuildsNamedAssetsAndChecksEmbeddedVersion(t *testing.T) {
} }
sort.Strings(got) sort.Strings(got)
want := []string{ want := []string{
"narratio-v0.0.0-test-darwin-amd64",
"narratio-v0.0.0-test-darwin-arm64", "narratio-v0.0.0-test-darwin-arm64",
"narratio-v0.0.0-test-linux-amd64", "narratio-v0.0.0-test-linux-amd64",
"narratio-v0.0.0-test-linux-arm64", "narratio-v0.0.0-test-linux-arm64",
"narratio-v0.0.0-test-windows-amd64.exe",
"narratio-v0.0.0-test-windows-arm64.exe",
} }
if !slicesEqual(got, want) { if !slicesEqual(got, want) {
t.Fatalf("asset names = %#v, want %#v", got, want) t.Fatalf("asset names = %#v, want %#v", got, want)

View File

@@ -8,7 +8,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -875,9 +874,6 @@ func TestExtractStageResumeValidationRejectsUnsafePathWithError(t *testing.T) {
m.Stages["extract"].Outputs[0].LocalPath = filepath.Join(env.Config.Pipeline.Workspace.Root, "outside.json") m.Stages["extract"].Outputs[0].LocalPath = filepath.Join(env.Config.Pipeline.Workspace.Root, "outside.json")
}}, }},
{name: "symlink", mutate: func(t *testing.T, _ *Env, m *manifest.Manifest) { {name: "symlink", mutate: func(t *testing.T, _ *Env, m *manifest.Manifest) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires privileges on Windows")
}
path := m.Stages["extract"].Outputs[0].LocalPath path := m.Stages["extract"].Outputs[0].LocalPath
outside := filepath.Join(t.TempDir(), "outside.json") outside := filepath.Join(t.TempDir(), "outside.json")
if err := os.WriteFile(outside, []byte(`{"outside":true}`), 0o644); err != nil { if err := os.WriteFile(outside, []byte(`{"outside":true}`), 0o644); err != nil {

View File

@@ -7,7 +7,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime"
"sort" "sort"
"strings" "strings"
"testing" "testing"
@@ -231,9 +230,6 @@ func TestPublishToleratesMissingPreviousCache(t *testing.T) {
} }
func TestPublishRejectsDeclaredLeafSymlinkBeforeUploading(t *testing.T) { func TestPublishRejectsDeclaredLeafSymlinkBeforeUploading(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires privileges on Windows")
}
env, m, runRoot := publishFixture(t) env, m, runRoot := publishFixture(t)
sentinel := filepath.Join(t.TempDir(), "outside.txt") sentinel := filepath.Join(t.TempDir(), "outside.txt")
writeStageTestFile(t, sentinel, "outside secret\n") writeStageTestFile(t, sentinel, "outside secret\n")
@@ -253,9 +249,6 @@ func TestPublishRejectsDeclaredLeafSymlinkBeforeUploading(t *testing.T) {
} }
func TestPublishRejectsSymlinkedRunRootBeforeUploading(t *testing.T) { func TestPublishRejectsSymlinkedRunRootBeforeUploading(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires privileges on Windows")
}
env, m, runRoot := publishFixture(t) env, m, runRoot := publishFixture(t)
actualRoot := runRoot + "-actual" actualRoot := runRoot + "-actual"
if err := os.Rename(runRoot, actualRoot); err != nil { if err := os.Rename(runRoot, actualRoot); err != nil {
@@ -273,9 +266,6 @@ func TestPublishRejectsSymlinkedRunRootBeforeUploading(t *testing.T) {
} }
func TestPublishRejectsDeclaredSourceWithSymlinkedAncestor(t *testing.T) { func TestPublishRejectsDeclaredSourceWithSymlinkedAncestor(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires privileges on Windows")
}
env, m, runRoot := publishFixture(t) env, m, runRoot := publishFixture(t)
outside := t.TempDir() outside := t.TempDir()
writeStageTestFile(t, filepath.Join(outside, "audita.stderr.log"), "outside secret\n") writeStageTestFile(t, filepath.Join(outside, "audita.stderr.log"), "outside secret\n")
@@ -408,9 +398,6 @@ func TestPublishAllowsEmptyCurrentRunDiagnostics(t *testing.T) {
} }
func TestPublishRejectsDeclaredPreviousSymlinkBeforeUploading(t *testing.T) { func TestPublishRejectsDeclaredPreviousSymlinkBeforeUploading(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlink creation requires privileges on Windows")
}
env, m, _ := publishFixture(t) env, m, _ := publishFixture(t)
previousDir := filepath.Join( previousDir := filepath.Join(
artifacts.SessionWorkDirForCampaign( artifacts.SessionWorkDirForCampaign(

View File

@@ -69,14 +69,10 @@ host_os=$(go env GOOS)
host_arch=$(go env GOARCH) host_arch=$(go env GOARCH)
host_asset= host_asset=
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do for target in linux/amd64 linux/arm64 darwin/arm64; do
target_os=${target%%/*} target_os=${target%%/*}
target_arch=${target#*/} target_arch=${target#*/}
suffix= asset_path="$output_dir/narratio-$release_version-$target_os-$target_arch"
if [ "$target_os" = windows ]; then
suffix=.exe
fi
asset_path="$output_dir/narratio-$release_version-$target_os-$target_arch$suffix"
CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" go build -trimpath -ldflags "$build_flags" -o "$asset_path" ./cmd/narratio CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" go build -trimpath -ldflags "$build_flags" -o "$asset_path" ./cmd/narratio
if [ "$target_os" = "$host_os" ] && [ "$target_arch" = "$host_arch" ]; then if [ "$target_os" = "$host_os" ] && [ "$target_arch" = "$host_arch" ]; then
host_asset=$asset_path host_asset=$asset_path