From f8e3e651e6ac67e7af0e1adb5ffb1251c76281a5 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 7 Sep 2026 03:28:50 +0000 Subject: [PATCH] Reduce supported platform matrix --- .woodpecker/verify.yml | 2 +- docs/development.md | 6 +- docs/internal/adapters.md | 9 +- docs/operations.md | 11 +- docs/policy/architecture.md | 11 +- docs/release.md | 8 +- docs/roadmap/implementation.md | 5 +- internal/adapters/audita/subprocess_test.go | 37 ----- .../adapters/scriptorium/subprocess_test.go | 16 -- internal/adapters/seriatim/subprocess_test.go | 67 -------- .../subprocess/process_tree_supported_test.go | 2 +- .../subprocess/process_tree_unsupported.go | 3 +- .../subprocess/process_tree_windows.go | 145 ------------------ internal/adapters/subprocess/run_test.go | 4 - internal/app/secrets_env_posix.go | 2 +- internal/app/secrets_env_test.go | 10 -- internal/app/secrets_env_windows.go | 11 -- internal/artifacts/extraction_catalog_test.go | 4 - internal/artifacts/lock_unsupported.go | 3 +- internal/artifacts/lock_windows.go | 29 ---- internal/config/pipeline_composition_test.go | 4 - internal/fileops/modes_posix_test.go | 2 +- internal/releasecheck/assets_test.go | 3 - internal/stage/extract_test.go | 4 - internal/stage/publish_test.go | 13 -- scripts/build-release-assets.sh | 8 +- 26 files changed, 32 insertions(+), 387 deletions(-) delete mode 100644 internal/adapters/subprocess/process_tree_windows.go delete mode 100644 internal/app/secrets_env_windows.go delete mode 100644 internal/artifacts/lock_windows.go diff --git a/.woodpecker/verify.yml b/.woodpecker/verify.yml index e2523cd..92b0ed4 100644 --- a/.woodpecker/verify.yml +++ b/.woodpecker/verify.yml @@ -40,7 +40,7 @@ steps: set -eu output_dir="$(mktemp -d)" 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%/*}" goarch="${target#*/}" CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" go build -o "$output_dir/narratio-$goos-$goarch" ./cmd/narratio diff --git a/docs/development.md b/docs/development.md index 1069693..c3b7d9a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -58,7 +58,7 @@ in the [Release Procedure](release.md). Woodpecker also runs `go test -race -shuffle=on -count=3 ./...` on its scheduled job to expose ordering and repeatability defects. Current runners cross-compile -for macOS and Windows, but do not provide native macOS or Windows execution. -Those cross-builds establish compilation only, not platform-equivalent runtime -evidence. Add native checks only when official runner labels and successful +the supported Linux amd64 and arm64 targets and the best-effort macOS arm64 +target. The macOS cross-build establishes compilation only, not native runtime +evidence. Add a native check only when an official runner label and successful native-run evidence are available. diff --git a/docs/internal/adapters.md b/docs/internal/adapters.md index a04c784..10a901e 100644 --- a/docs/internal/adapters.md +++ b/docs/internal/adapters.md @@ -78,11 +78,10 @@ in-memory backend without changing package state. - Subprocess adapters persist stage logs/generated configs through stage-managed paths. - Shared subprocess execution keeps private lifecycle mechanics separate from Narratio's environment, diagnostics, redaction, and error policy. It starts - an owned process group on Linux/macOS or a - kill-on-close job object on Windows. Every terminal path disposes of that - owned tree before returning. After a natural leader exit, Unix checks for - remaining group members and uses bounded graceful then forceful termination; - Windows closes the job so kill-on-close applies. Cancellation, deadlines, and + an owned process group on Linux and macOS. Every terminal path disposes of + that owned tree before returning. After a natural leader exit, the runner checks + for remaining group members and uses bounded graceful then forceful termination. + Cancellation, deadlines, and diagnostic limits use the same terminal disposal path without losing their original result classification. Child environments contain only the execution baseline and adapter-specified values; configured credentials are explicit diff --git a/docs/operations.md b/docs/operations.md index 3d222cc..72ac160 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -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 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 shared workspace or in a separately restricted credential location; ordinary workspace group access must never be treated as authorization to read keys. -On POSIX, provision a credential directory as `0700` and credential files as -`0600`; Narratio rejects group- or other-readable configured credential paths. -On Windows, restrict the directory and files with ACLs to the credential owner. +Provision a credential directory as `0700` and credential files as `0600`; +Narratio rejects group- or other-readable configured credential paths. External adapter results are individually bounded before Narratio validates or materializes them. These per-file limits do not reserve disk space: prevent hard diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index 10ec800..7fc9bf6 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -234,13 +234,16 @@ configuration, logs, reports, diagnostics, and Notarius artifacts—is intentionally shareable with the configured workspace group. API-key material 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 umask is restrictive. This preserves the existing workspace group for nested creation and atomic replacements without changing ownership. API-key storage -uses a separate restrictive contract. On Windows, POSIX mode bits and setgid -are not authoritative; operators must provide the equivalent shared-group and -credential-restricted ACLs described in [Operations](../operations.md#workspace-permissions). +uses a separate restrictive contract. Raw secrets must not be stored in pipeline, campaign, or session YAML or written to manifests, logs, generated configuration, reports, publish metadata, diff --git a/docs/release.md b/docs/release.md index f9b0a32..4789654 100644 --- a/docs/release.md +++ b/docs/release.md @@ -58,14 +58,14 @@ scripts/check-release-candidate.sh vMAJOR.MINOR.PATCH It validates the version and matching note, module hygiene, formatting, 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 source. Fix any failure on `main`, commit it, push it normally, and rerun the checker. -The checker builds Linux, macOS, and Windows assets for `amd64` and `arm64`. -Cross-builds prove compilation; they are not native macOS or Windows runtime -evidence. +The checker builds Linux assets for `amd64` and `arm64` and a macOS asset for +`arm64`. The macOS cross-build proves compilation; it is not native macOS +runtime evidence. ## Publish the tag diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index efdcf72..8ea509a 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -89,7 +89,10 @@ make that scope concrete: ## 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`, `scripts/check-release-candidate.sh`, `.woodpecker/`, `internal/releasecheck`, diff --git a/internal/adapters/audita/subprocess_test.go b/internal/adapters/audita/subprocess_test.go index ac43216..3ae8bf9 100644 --- a/internal/adapters/audita/subprocess_test.go +++ b/internal/adapters/audita/subprocess_test.go @@ -6,16 +6,12 @@ import ( "fmt" "os" "path/filepath" - "runtime" "strings" "testing" "time" ) 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("AUDITA_HELPER_MODE", "success") @@ -133,9 +129,6 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) { } func TestSubprocessRunnerMissingConfiguredCredentialFails(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } runner := mustAuditaRunner(t, SubprocessRunnerConfig{ Binary: writeAuditaHelperWrapper(t), Timeout: mustParseAuditaDuration(t, "2s"), @@ -157,9 +150,6 @@ func TestSubprocessRunnerMissingConfiguredCredentialFails(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("AUDITA_HELPER_MODE", "success") recordPath := filepath.Join(t.TempDir(), "record.json") @@ -190,9 +180,6 @@ func TestSubprocessRunnerUnconfiguredCredentialEnvOmitsCredential(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("AUDITA_HELPER_MODE", "success") t.Setenv("AUDITA_INHERITED_MARKER", "inherited-from-parent") @@ -220,9 +207,6 @@ func TestSubprocessRunnerOmitsUnspecifiedParentEnvironment(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("AUDITA_HELPER_MODE", "success") recordPath := filepath.Join(t.TempDir(), "record.json") @@ -249,9 +233,6 @@ func TestSubprocessRunnerOmitsModulesFlagWhenNotConfigured(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("AUDITA_HELPER_MODE", "success") recordPath := filepath.Join(t.TempDir(), "record.json") @@ -279,9 +260,6 @@ func TestSubprocessRunnerOmitsBaseURLAndModelFlagsWhenNotConfigured(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("AUDITA_HELPER_MODE", "fail") t.Setenv("OPENAI_KEY_SOURCE", "super-secret") @@ -313,9 +291,6 @@ func TestSubprocessRunnerSubprocessFailure(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("AUDITA_HELPER_MODE", "fail_bad_fd") t.Setenv("OPENAI_KEY_SOURCE", "super-secret") @@ -341,9 +316,6 @@ func TestSubprocessRunnerSubprocessFailureAddsStderrDescriptorHint(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("AUDITA_HELPER_MODE", "missing_output") t.Setenv("OPENAI_KEY_SOURCE", "super-secret") @@ -369,9 +341,6 @@ func TestSubprocessRunnerMissingOutputFails(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("AUDITA_HELPER_MODE", "invalid_output") t.Setenv("OPENAI_KEY_SOURCE", "super-secret") @@ -397,9 +366,6 @@ func TestSubprocessRunnerInvalidOutputJSONFails(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("AUDITA_HELPER_MODE", "segments_missing") t.Setenv("OPENAI_KEY_SOURCE", "super-secret") @@ -425,9 +391,6 @@ func TestSubprocessRunnerSegmentsMissingFails(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("AUDITA_HELPER_MODE", "invalid_report") t.Setenv("OPENAI_KEY_SOURCE", "super-secret") diff --git a/internal/adapters/scriptorium/subprocess_test.go b/internal/adapters/scriptorium/subprocess_test.go index 862309f..ce21b73 100644 --- a/internal/adapters/scriptorium/subprocess_test.go +++ b/internal/adapters/scriptorium/subprocess_test.go @@ -6,16 +6,12 @@ import ( "fmt" "os" "path/filepath" - "runtime" "strings" "testing" "time" ) 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("SCRIPTORIUM_HELPER_MODE", "run_success") @@ -95,9 +91,6 @@ func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *tes } 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("SCRIPTORIUM_HELPER_MODE", "fail_exit1") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1") t.Setenv("SCRIPTORIUM_HELPER_MODE", "fail_exit2_with_output") t.Setenv("SCRIPTORIUM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) @@ -144,9 +134,6 @@ func TestSubprocessRunnerRunExitCodeTwoReturnsValidationFailureAndPreservesOutpu } 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("SCRIPTORIUM_HELPER_MODE", "run_success_missing_output") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SCRIPTORIUM_HELPER", "1") t.Setenv("SCRIPTORIUM_HELPER_MODE", "render_success") recordPath := filepath.Join(t.TempDir(), "record.json") diff --git a/internal/adapters/seriatim/subprocess_test.go b/internal/adapters/seriatim/subprocess_test.go index 42e7af8..1880aec 100644 --- a/internal/adapters/seriatim/subprocess_test.go +++ b/internal/adapters/seriatim/subprocess_test.go @@ -6,16 +6,12 @@ import ( "fmt" "os" "path/filepath" - "runtime" "strings" "testing" "time" ) 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("SERIATIM_HELPER_MODE", "success") @@ -131,9 +127,6 @@ func TestSubprocessRunnerSuccessWithReportArgsAndEnv(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("SERIATIM_HELPER_MODE", "fail") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "missing_output") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "invalid_output") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "invalid_report") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "trim_success") @@ -297,9 +278,6 @@ func TestSubprocessRunnerTrimSuccessInvocationAndProvenance(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("SERIATIM_HELPER_MODE", "fail") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "missing_output") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "invalid_output") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "trim_missing_segments") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "normalize_success") @@ -453,9 +419,6 @@ func TestSubprocessRunnerNormalizeSuccessInvocationAndProvenance(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("SERIATIM_HELPER_MODE", "fail") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "normalize_missing_output") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "normalize_invalid_output") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "normalize_missing_segments") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "normalize_report_missing") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "normalize_invalid_report") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "render_success") @@ -639,9 +584,6 @@ func TestSubprocessRunnerRenderSuccessInvocationAndProvenance(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("SERIATIM_HELPER_MODE", "render_success") recordPath := filepath.Join(t.TempDir(), "record.json") @@ -663,9 +605,6 @@ func TestSubprocessRunnerRenderWithoutTitleOmitsTitleArg(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("SERIATIM_HELPER_MODE", "fail") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "missing_output") 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) { - if runtime.GOOS == "windows" { - t.Skip("helper wrapper script uses /bin/sh") - } t.Setenv("GO_WANT_SERIATIM_HELPER", "1") t.Setenv("SERIATIM_HELPER_MODE", "render_empty_output") t.Setenv("SERIATIM_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json")) diff --git a/internal/adapters/subprocess/process_tree_supported_test.go b/internal/adapters/subprocess/process_tree_supported_test.go index dd09826..7de56c5 100644 --- a/internal/adapters/subprocess/process_tree_supported_test.go +++ b/internal/adapters/subprocess/process_tree_supported_test.go @@ -1,4 +1,4 @@ -//go:build linux || darwin || windows +//go:build linux || darwin package subprocess diff --git a/internal/adapters/subprocess/process_tree_unsupported.go b/internal/adapters/subprocess/process_tree_unsupported.go index f644320..cebd71e 100644 --- a/internal/adapters/subprocess/process_tree_unsupported.go +++ b/internal/adapters/subprocess/process_tree_unsupported.go @@ -1,10 +1,9 @@ -//go:build !linux && !darwin && !windows +//go:build !linux && !darwin package subprocess import ( "fmt" - "runtime" ) func newOwnedProcessTree() (ownedProcessTree, error) { diff --git a/internal/adapters/subprocess/process_tree_windows.go b/internal/adapters/subprocess/process_tree_windows.go deleted file mode 100644 index 384b4e8..0000000 --- a/internal/adapters/subprocess/process_tree_windows.go +++ /dev/null @@ -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) -} diff --git a/internal/adapters/subprocess/run_test.go b/internal/adapters/subprocess/run_test.go index 325673d..d4e710b 100644 --- a/internal/adapters/subprocess/run_test.go +++ b/internal/adapters/subprocess/run_test.go @@ -8,7 +8,6 @@ import ( "os/exec" "os/signal" "path/filepath" - "runtime" "strconv" "strings" "syscall" @@ -137,9 +136,6 @@ func TestRunFailureRedactsSensitiveTail(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() if err != nil { t.Fatalf("os.Executable() error = %v", err) diff --git a/internal/app/secrets_env_posix.go b/internal/app/secrets_env_posix.go index 2ca971b..b76aa38 100644 --- a/internal/app/secrets_env_posix.go +++ b/internal/app/secrets_env_posix.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build linux || darwin package app diff --git a/internal/app/secrets_env_test.go b/internal/app/secrets_env_test.go index f526d12..1b09455 100644 --- a/internal/app/secrets_env_test.go +++ b/internal/app/secrets_env_test.go @@ -3,7 +3,6 @@ package app import ( "os" "path/filepath" - "runtime" "strings" "testing" @@ -120,9 +119,6 @@ func TestLoadSecretsFromConfigMissingDirFails(t *testing.T) { } func TestLoadSecretsFromConfigUnreadableValidEntryFails(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("symlink behavior differs on windows") - } dir := t.TempDir() ensureSecretDirectory(t, dir) @@ -152,9 +148,6 @@ func TestLoadSecretsFromConfigUnreadableValidEntryFails(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 { name string directory os.FileMode @@ -213,9 +206,6 @@ func TestLoadSecretsFromConfigRejectsNonRegularAndOversizedEntries(t *testing.T) } func TestLoadSecretsFromConfigRejectsSymlinkAndAncestorReplacement(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("symlink behavior differs on windows") - } dir := t.TempDir() outside := t.TempDir() ensureSecretDirectory(t, dir) diff --git a/internal/app/secrets_env_windows.go b/internal/app/secrets_env_windows.go deleted file mode 100644 index 2c2510a..0000000 --- a/internal/app/secrets_env_windows.go +++ /dev/null @@ -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 } diff --git a/internal/artifacts/extraction_catalog_test.go b/internal/artifacts/extraction_catalog_test.go index 503fa97..d763fd7 100644 --- a/internal/artifacts/extraction_catalog_test.go +++ b/internal/artifacts/extraction_catalog_test.go @@ -4,7 +4,6 @@ import ( "errors" "os" "path/filepath" - "runtime" "strings" "testing" "time" @@ -117,9 +116,6 @@ func TestInspectExtractionEvidenceClassifiesBundleStates(t *testing.T) { paths.Root = t.TempDir() }}, {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 outside := filepath.Join(t.TempDir(), "outside.json") writeExtractionFixtureFile(t, outside, `{"outside":true}`) diff --git a/internal/artifacts/lock_unsupported.go b/internal/artifacts/lock_unsupported.go index 87f0abf..2438b59 100644 --- a/internal/artifacts/lock_unsupported.go +++ b/internal/artifacts/lock_unsupported.go @@ -1,11 +1,10 @@ -//go:build !linux && !darwin && !windows +//go:build !linux && !darwin package artifacts import ( "fmt" "os" - "runtime" ) func acquireHeldFileLock(_ *os.File) error { diff --git a/internal/artifacts/lock_windows.go b/internal/artifacts/lock_windows.go deleted file mode 100644 index 1ba35d2..0000000 --- a/internal/artifacts/lock_windows.go +++ /dev/null @@ -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{}) -} diff --git a/internal/config/pipeline_composition_test.go b/internal/config/pipeline_composition_test.go index 81d3c9f..31257f4 100644 --- a/internal/config/pipeline_composition_test.go +++ b/internal/config/pipeline_composition_test.go @@ -4,7 +4,6 @@ import ( "os" "path/filepath" "reflect" - "runtime" "slices" "strings" "testing" @@ -277,9 +276,6 @@ func TestLoadPipelineCompositionRejectsUnsafeOrInvalidImports(t *testing.T) { func TestLoadPipelineCompositionRejectsSameFileAliasesAndImportedComposition(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() 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 { diff --git a/internal/fileops/modes_posix_test.go b/internal/fileops/modes_posix_test.go index 78f17c1..a57c391 100644 --- a/internal/fileops/modes_posix_test.go +++ b/internal/fileops/modes_posix_test.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build linux || darwin package fileops diff --git a/internal/releasecheck/assets_test.go b/internal/releasecheck/assets_test.go index 64b0555..c5e6534 100644 --- a/internal/releasecheck/assets_test.go +++ b/internal/releasecheck/assets_test.go @@ -124,12 +124,9 @@ func TestAssetBuilderBuildsNamedAssetsAndChecksEmbeddedVersion(t *testing.T) { } sort.Strings(got) want := []string{ - "narratio-v0.0.0-test-darwin-amd64", "narratio-v0.0.0-test-darwin-arm64", "narratio-v0.0.0-test-linux-amd64", "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) { t.Fatalf("asset names = %#v, want %#v", got, want) diff --git a/internal/stage/extract_test.go b/internal/stage/extract_test.go index 25b451b..9181d50 100644 --- a/internal/stage/extract_test.go +++ b/internal/stage/extract_test.go @@ -8,7 +8,6 @@ import ( "os" "path/filepath" "reflect" - "runtime" "strings" "testing" "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") }}, {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 outside := filepath.Join(t.TempDir(), "outside.json") if err := os.WriteFile(outside, []byte(`{"outside":true}`), 0o644); err != nil { diff --git a/internal/stage/publish_test.go b/internal/stage/publish_test.go index 5ffa5b7..0df7efd 100644 --- a/internal/stage/publish_test.go +++ b/internal/stage/publish_test.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "reflect" - "runtime" "sort" "strings" "testing" @@ -231,9 +230,6 @@ func TestPublishToleratesMissingPreviousCache(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) sentinel := filepath.Join(t.TempDir(), "outside.txt") writeStageTestFile(t, sentinel, "outside secret\n") @@ -253,9 +249,6 @@ func TestPublishRejectsDeclaredLeafSymlinkBeforeUploading(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) actualRoot := runRoot + "-actual" if err := os.Rename(runRoot, actualRoot); err != nil { @@ -273,9 +266,6 @@ func TestPublishRejectsSymlinkedRunRootBeforeUploading(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) outside := t.TempDir() 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) { - if runtime.GOOS == "windows" { - t.Skip("symlink creation requires privileges on Windows") - } env, m, _ := publishFixture(t) previousDir := filepath.Join( artifacts.SessionWorkDirForCampaign( diff --git a/scripts/build-release-assets.sh b/scripts/build-release-assets.sh index 5317a53..0d29a17 100755 --- a/scripts/build-release-assets.sh +++ b/scripts/build-release-assets.sh @@ -69,14 +69,10 @@ host_os=$(go env GOOS) host_arch=$(go env GOARCH) 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_arch=${target#*/} - suffix= - if [ "$target_os" = windows ]; then - suffix=.exe - fi - asset_path="$output_dir/narratio-$release_version-$target_os-$target_arch$suffix" + asset_path="$output_dir/narratio-$release_version-$target_os-$target_arch" 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 host_asset=$asset_path