Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/coverage-floors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,20 @@ internal/database/vectorsearch 94 # measured 96.9 2026-10-10
internal/deploy 53 # measured 55.2 2026-10-05
internal/deploy/archguard 76 # measured 78.6 2026-10-09
internal/deploy/bluegreen 44 # measured 46.3 2026-10-05
internal/deploy/pathmap 0 # measured 0.0 2026-10-29 (no test files yet)
internal/deploy/imagetags 98 # measured 100.0 2026-10-29
internal/deploy/pintest 57 # measured 59.5 2026-10-13 (P7-HYG-49)
internal/deploy/progress 86 # measured 88.8 2026-10-12
internal/deprecation 75 # measured 77.3 2026-10-05
internal/dirwritable 91 # measured 93.8 2026-10-29
internal/docker 52 # measured 54.7 2026-10-05
internal/docker/dockertest 85 # measured 89.5 (darwin, local) 2026-10-11; new package (D-0286)
internal/doctor 47 # measured 49.6 2026-10-05
internal/doctor/report 85 # measured 87.8 2026-10-11
internal/domain 60 # measured 62.1 2026-10-05
internal/embedded 78 # measured 80.8 2026-10-05
internal/env 19 # measured 21.7 2026-10-05
internal/envrestore 61 # measured 63.4 2026-10-29
internal/errs 93 # measured 95.8 2026-10-05
internal/eval 77 # measured 79.1 2026-10-05
internal/exporter 58 # measured 60.2 2026-10-12 (P7-ADOPT-17; snapshot, pg_dump and auth reads need a live Postgres, covered by scripts/e2e/export.sh)
Expand All @@ -81,6 +84,7 @@ internal/generate 65 # measured 67.5 2026-10-05
internal/hasura 87 # measured 89.1 2026-10-05
internal/health 26 # measured 28.1 2026-10-05
internal/hostcase 95 # measured 100.0 2026-10-17 (D-0881)
internal/hostpin 0 # measured 0.0 2026-10-29 (no test files yet)
internal/httpapi 93 # measured 95.1 2026-10-11 (P7-SURF-02)
internal/httptimeout 98 # measured 100.0 2026-10-05
internal/importer 47 # measured 49.7 2026-10-12
Expand Down Expand Up @@ -125,6 +129,7 @@ internal/oplock 74 # measured 79.3 2026-10-05
internal/oplock/oplockcmd 88 # measured 93.5 2026-10-05
internal/output 96 # measured 98.2 2026-10-05
internal/plugin 50 # measured 53.0 2026-10-05
internal/plugin/autoinstall 0 # measured 0.0 2026-10-30 (no test files yet)
internal/plugin/catalog 81 # measured 83.0 2026-10-13
internal/plugin/count 98 # measured 100.0 2026-10-05
internal/plugin/lifecycle 74 # measured 76.4 2026-10-05
Expand All @@ -137,6 +142,7 @@ internal/plugin/verify 85 # measured 87.2 2026-10-05
internal/plugin/verify/signature 95 # measured 99.1 2026-10-13 (P7-PLUG-23)
internal/plugin/verify/signature/signaturetest 0 # measured 0.0 2026-10-13; test-only key-table swap helper, exercised from other packages' tests (P7-PLUG-23)
internal/pluginname 85 # measured 90.0 2026-10-26 (D-0602)
internal/pluginperm 48 # measured 50.0 2026-10-29
internal/portable 85 # measured 87.5 2026-10-05
internal/ports 9 # measured 11.5 2026-10-05
internal/postgres 49 # measured 51.4 2026-10-05
Expand Down
9 changes: 7 additions & 2 deletions cmd/commands/config_explain_monorepo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,13 @@ func TestConfigExplainMonorepoExecution(t *testing.T) {
if tc.redirect {
wantDir = backend
}
if cwd, err := os.Getwd(); err != nil || cwd != wantDir {
t.Errorf("cwd = %q (%v), want %q", cwd, err, wantDir)
// Paths differ in form (macOS /var vs /private/var, Windows 8.3 short
// names), so compare the directories themselves.
cwdInfo, cwdErr := os.Stat(".")
wantInfo, wantErr := os.Stat(wantDir)
if cwdErr != nil || wantErr != nil || !os.SameFile(cwdInfo, wantInfo) {
cwd, _ := os.Getwd()
t.Errorf("cwd = %q (%v), want the directory %q (%v)", cwd, cwdErr, wantDir, wantErr)
}
})
}
Expand Down
8 changes: 7 additions & 1 deletion cmd/commands/next_step_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,13 @@ func TestNextStep(t *testing.T) {
t.Run(tc.stage+"_"+tc.mode, func(t *testing.T) {
nextStepFixture(t, tc.stage)
t.Setenv("NEXTSTEP_DOCKER", tc.mode)
ctx, cancel := context.WithTimeout(context.Background(), nextStepTimeout)
// Only the slow stub (it sleeps 10s) may reach the production deadline;
// a loaded Windows runner needs far more than 3s to start the helper.
timeout := 60 * time.Second
if tc.mode == "slow" {
timeout = nextStepTimeout
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if got := nextStep(ctx); got != "Next: nself "+tc.want {
t.Errorf("got %q, want %q", got, tc.want)
Expand Down
8 changes: 8 additions & 0 deletions internal/build/expect_publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ func (p *artifactPublisher) target(path string) (*os.Root, string, error) {
return root, filepath.FromSlash(rel), nil
}

func (p *artifactPublisher) releaseCleanupRoot(path string) {
if abs, err := filepath.Abs(p.sink.abs(path)); err == nil && p.roots[abs] != nil {
_ = p.roots[abs].Close()
delete(p.roots, abs)
}
}

func (p *artifactPublisher) publish() error {
for _, k := range sortedArtifactKeys(p.sink.dirs) {
if err := p.mkdir(p.sink.abs(k), p.sink.dirs[k]); err != nil {
Expand Down Expand Up @@ -173,6 +180,7 @@ func (p *artifactPublisher) publish() error {
if err != nil {
return err
}
p.releaseCleanupRoot(k) // Windows cannot remove a directory with an open handle
if err := root.Remove(rel); err != nil {
return err
}
Expand Down
5 changes: 5 additions & 0 deletions internal/build/orchestrator_build_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"log/slog"
"os"
"path/filepath"
"runtime"
"strings"
"time"

Expand Down Expand Up @@ -156,7 +157,11 @@ func (st *buildState) loadValidateConfig() (*BuildResult, error) {
// ensureEnvFilePermissions fixes an existing env file to 0600 when it is more
// permissive, through the sink (setup.EnsureEnvFilePermissions semantics: a
// missing file is a no-op). Plan mode records the mode change, not applies it.
// Windows has no owner-only permission bits, so there is nothing to fix there.
func ensureEnvFilePermissions(sink Sink, path string) error {
if runtime.GOOS == "windows" {
return nil
}
info, err := sink.Stat(path)
if os.IsNotExist(err) {
return nil
Expand Down
30 changes: 4 additions & 26 deletions internal/build/proxy_restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"os"
"path/filepath"
"sort"

"github.com/nself-org/cli/internal/dirwritable"
)

// The traefik renderer marks its output with this file and prefix.
Expand Down Expand Up @@ -61,7 +63,7 @@ func cleanupOutputDir(sink Sink, workdir, dir string) error {
return proxyKeep(dir, why)
}
// Plan and apply must agree on removals even for unwritable output.
if why := unwritableIn(filepath.Dir(dir), dir); why != "" {
if why := dirwritable.FirstUnwritable(filepath.Dir(dir), dir); why != "" {
return proxyKeep(dir, "it is owned by root (the traefik-config container wrote it): "+why+" is not writable; run `sudo rm -rf "+dir+"` or chown it to your user, then build again")
}
err = removeTree(sink, dir, info)
Expand All @@ -75,31 +77,6 @@ func cleanupOutputDir(sink Sink, workdir, dir string) error {
return nil
}

// unwritableIn finds the first directory that prevents unlinking, parent first.
func unwritableIn(parent, dir string) string {
if !dirWritable(parent) {
return parent
}
var bad string
_ = filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
if bad != "" {
return nil
}
if err != nil { // an unreadable directory cannot be emptied either
bad = p
return nil
}
if !d.IsDir() {
return nil
}
if !dirWritable(p) {
bad = p
}
return nil
})
return bad
}

// proxyKeep warns and continues without deleting unproven output.
func proxyKeep(dir, why string) error {
slog.Warn("leaving "+dir+" in place: "+why,
Expand Down Expand Up @@ -156,6 +133,7 @@ func removeTree(sink Sink, dir string, before fs.FileInfo) error {
if err := removeTreeIn(sink, root, dir, "."); err != nil {
return err
}
_ = root.Close() // Windows cannot remove a directory while a handle on it is open
return sink.Remove(dir)
}

Expand Down
7 changes: 0 additions & 7 deletions internal/build/proxy_restore_windows.go

This file was deleted.

54 changes: 0 additions & 54 deletions internal/bundle/installer_reconcile_rollback.go

This file was deleted.

50 changes: 50 additions & 0 deletions internal/cmdregistry/flag_argv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cmdregistry

import (
"testing"

"github.com/spf13/pflag"
)

func TestFlagInsertionIndex(t *testing.T) {
local := pflag.NewFlagSet("local", pflag.ContinueOnError)
local.String("name", "", "takes a value")
inherited := pflag.NewFlagSet("inherited", pflag.ContinueOnError)
inherited.String("env", "", "takes a value")
inherited.Bool("verbose", false, "bool")
inherited.Lookup("verbose").NoOptDefVal = "true"

for _, tc := range []struct {
desc string
args []string
wantIndex int
wantFound bool
}{
{"no args", nil, 0, false},
{"positional only", []string{"a", "b"}, 2, false},
{"separator stops the scan", []string{"a", "--", "--json"}, 1, false},
{"flag already forwarded", []string{"a", "--json"}, 1, true},
{"forwarded inline", []string{"--json=false"}, 0, true},
{"local value flag skips its value", []string{"--name", "--json"}, 2, false},
{"inherited value flag skips its value", []string{"--env", "--json", "x"}, 3, false},
{"inline value is not skipped", []string{"--name=x", "--json"}, 1, true},
{"no-value flag is not skipped", []string{"--verbose", "--json"}, 1, true},
{"unknown flag is not skipped", []string{"--other", "--json"}, 1, true},
{"single dash is positional", []string{"-x", "--"}, 1, false},
} {
got, found := FlagInsertionIndex(tc.args, "json", local, inherited)
if got != tc.wantIndex || found != tc.wantFound {
t.Errorf("%s: FlagInsertionIndex(%q) = (%d, %t), want (%d, %t)", tc.desc, tc.args, got, found, tc.wantIndex, tc.wantFound)
}
}
}

func TestNoopBoolValueIgnoresWrites(t *testing.T) {
var v NoopBoolValue
if err := v.Set("true"); err != nil {
t.Fatalf("Set returned %v, want nil", err)
}
if v.String() != "false" || v.Type() != "bool" {
t.Fatalf("NoopBoolValue = (%q, %q), want (false, bool)", v.String(), v.Type())
}
}
2 changes: 1 addition & 1 deletion internal/compose/dry_run_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ func PrintCustomServiceEnvSummary(w io.Writer, workdir string) {
}
for _, cs := range cfg.CustomServices {
names := CustomServiceEnvVarNames(cs)
fmt.Fprintf(w, " [dry-run] Custom service %s will receive: %s\n", cs.Name, strings.Join(names, ", "))
_, _ = fmt.Fprintf(w, " [dry-run] Custom service %s will receive: %s\n", cs.Name, strings.Join(names, ", "))
}
}
4 changes: 3 additions & 1 deletion internal/controlplane/promote/verify/trust_pin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
Expand All @@ -27,7 +28,8 @@ func TestVerifyPinsOriginOnFirstUse(t *testing.T) {
if err != nil || !strings.Contains(string(raw), "\ngit@github.com:o/r.git\n") {
t.Fatalf("pin file = %q, %v", raw, err)
}
if info, _ := os.Stat(pinFile(r.home)); info.Mode().Perm() != 0o600 {
// Windows has no owner-only permission bits (it reports 0666).
if info, _ := os.Stat(pinFile(r.home)); runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 {
t.Errorf("pin file mode %v, want 0600", info.Mode().Perm())
}
if len(notes) != 1 || !strings.Contains(notes[0], "git@github.com:o/r.git") || len(warns) != 0 {
Expand Down
35 changes: 35 additions & 0 deletions internal/dirwritable/dirwritable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Package dirwritable tells, before anything is planned or deleted, whether the
// current user can unlink inside a directory tree (the traefik-config one-shot
// runs as root, so Docker creates its bind directories root-owned on Linux).
// It is read-only (access(2)) and identical in plan and apply mode.
package dirwritable

import (
"io/fs"
"path/filepath"
)

// FirstUnwritable finds the first directory that prevents unlinking, parent first.
func FirstUnwritable(parent, dir string) string {
if !writable(parent) {
return parent
}
var bad string
_ = filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
if bad != "" {
return nil
}
if err != nil { // an unreadable directory cannot be emptied either
bad = p
return nil
}
if !d.IsDir() {
return nil
}
if !writable(p) {
bad = p
}
return nil
})
return bad
}
Loading
Loading