From d7889e6166e99429ccd4962a785868a709704c94 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Tue, 19 May 2026 22:49:54 +0200 Subject: [PATCH 01/11] Update Go to 1.26.3, add CLAUDE.md, update docs Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build.yml | 4 +- .github/workflows/release.yml | 2 +- CLAUDE.md | 81 +++++++++++++++++++++++++++++++++++ docs/recipes.md | 4 +- docs/usage.md | 2 +- go.mod | 13 ++++-- go.sum | 47 ++++++++++++++++++++ 7 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 CLAUDE.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b61b640..14559cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - go: ['1.20'] + go: ['1.26.3'] steps: - name: Checkout @@ -21,7 +21,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.26.3' cache: false - name: golangci-lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5e38a2..96f3b42 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.26.3' cache: false - name: Create release notes diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e3824d4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,81 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Sake is a task runner for local and remote hosts written in Go. Users define servers and tasks in `sake.yaml` files and execute tasks on servers via SSH or locally. + +## Common Commands + +```bash +# Build +make build # Build binary to dist/sake +make build-all # Cross-platform builds (requires goreleaser) + +# Test +make test # Run all tests (requires docker-compose for mock SSH servers) +make test-unit # Run unit tests only +make test-integration # Run integration tests only (requires mock-ssh running) +make mock-ssh # Start mock SSH servers for integration tests +make update-golden-files # Update golden test files + +# Code quality +make lint # Run golangci-lint and deadcode checker +make gofmt # Format Go code + +# Development +go run ../main.go run ping -a # Quick debug from examples directory +``` + +## Architecture + +### Entry Points +- `main.go` → calls `cmd.Execute()` +- `cmd/root.go` → Cobra CLI setup and command registration + +### Core Packages + +**cmd/** - CLI command handlers using Cobra framework. Each command in its own file (run.go, exec.go, ssh.go, list.go, etc.) + +**core/dao/** - Data Access Objects for config parsing: +- `config.go` - Main Config struct, YAML parsing, imports +- `server.go` - Server definitions with SSH/local connection details +- `task.go` - Task commands and subtasks (TaskCmd, TaskRef) +- `spec.go` - Execution specs (strategy, batch, forks, output format) +- `target.go` - Server filtering (by name, tags, regex, limits) +- `theme.go` - Output formatting themes + +**core/run/** - Task execution engine: +- `exec.go` - Main orchestrator, handles execution strategies +- `ssh.go` - SSH client wrapper (key auth, password auth, agent) +- `localhost.go` - Local execution via exec.Cmd +- `client.go` - Client interface definition + +**core/print/** - Output formatting (table, text, JSON, CSV, HTML, Markdown) + +### Data Flow +``` +CLI Input → cmd/root.go → core/dao/config.go (parse YAML) +→ Create Config (Servers, Tasks, Specs, Targets, Themes) +→ core/run/exec.go (create SSH/Local clients, execute with strategy) +→ core/print/ (format output) +``` + +### Key Patterns + +**YAML Struct Conversion**: Separate `*YAML` structs for unmarshaling that convert to domain structs after validation (e.g., `ServerYAML` → `Server`) + +**Execution Strategies**: linear (sequential), host_pinned (serial per host), free (concurrent) + +**Platform-Specific Code**: `unix.go` and `windows.go` for OS-specific handling + +## Testing + +Integration tests require mock SSH servers running via Docker: +```bash +make mock-ssh # Terminal 1: start mock servers +make test-integration # Terminal 2: run tests +``` + +Golden files in test/integration/ validate output. Update with `make update-golden-files`. diff --git a/docs/recipes.md b/docs/recipes.md index f7295ed..90a8f8c 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -38,7 +38,7 @@ upload-compose: To upload a file: ```bash -$ sake run get-backups --server +$ sake run upload-compose --server ``` You can also override the `SRC` and `DEST` variables at the command line: @@ -108,7 +108,7 @@ ssh-and-cmd: Then run: ```bash -$ sake run get-backups --server +$ sake run ssh-and-cmd --server ``` You can also provide the `--attach` flag to arbitrary commands: diff --git a/docs/usage.md b/docs/usage.md index a437b0c..4394e68 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -29,7 +29,7 @@ tasks: ping: desc: Pong cmd: echo "pong" -"``` +``` ## Run Some Commands diff --git a/go.mod b/go.mod index aa5439d..7dbb326 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/alajmo/sake -go 1.23 +go 1.26.3 require ( github.com/gobwas/glob v0.2.3 @@ -12,21 +12,26 @@ require ( github.com/theckman/yacspin v0.13.12 golang.org/x/crypto v0.31.0 golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 - golang.org/x/sys v0.28.0 - golang.org/x/term v0.27.0 + golang.org/x/sys v0.38.0 + golang.org/x/term v0.37.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gdamore/encoding v1.0.1 // indirect + github.com/gdamore/tcell/v2 v2.13.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/text v0.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/tview v0.42.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/text v0.31.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index d90a60a..df92cf6 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= +github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= +github.com/gdamore/tcell/v2 v2.13.7 h1:yfHdeC7ODIYCc6dgRos8L1VujQtXHmUpU6UZotzD6os= +github.com/gdamore/tcell/v2 v2.13.7/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -20,6 +26,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -29,6 +37,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= +github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -42,18 +52,55 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From cfbb011a2794f970e758c68c5d2999620f20e7bb Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Tue, 19 May 2026 22:56:27 +0200 Subject: [PATCH 02/11] Update workflows --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14559cf..d5298c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,8 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v3 + with: + version: v2.12.2 - name: Get dependencies run: go get -v -t -d ./... From ae7fa55bd3730f843f1d3d73a7935eb6fd876095 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Tue, 19 May 2026 23:07:58 +0200 Subject: [PATCH 03/11] bump golangci lint --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5298c2..5c5dcb2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v9 with: version: v2.12.2 From c90e92573cbca06fb862c08f464e44730671f4de Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Tue, 19 May 2026 23:44:40 +0200 Subject: [PATCH 04/11] Fix golangci-lint errcheck, govet, and staticcheck issues Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/list_servers.go | 6 +----- core/dao/config.go | 2 +- core/dao/theme.go | 4 ++-- core/run/exec.go | 2 +- core/run/ssh.go | 6 +++--- core/ssh_config.go | 4 ++-- core/test/utils.go | 6 +----- core/utils_test.go | 2 +- test/integration/main_test.go | 4 ++-- 9 files changed, 14 insertions(+), 22 deletions(-) diff --git a/cmd/list_servers.go b/cmd/list_servers.go index c21e53c..6639c20 100644 --- a/cmd/list_servers.go +++ b/cmd/list_servers.go @@ -88,11 +88,7 @@ func listServers(config *dao.Config, args []string, listFlags *core.ListFlags, s theme, err := config.GetTheme(listFlags.Theme) core.CheckIfError(err) - allServers := false - if len(serverArgs) == 0 && - len(serverFlags.Tags) == 0 { - allServers = true - } + allServers := len(serverArgs) == 0 && len(serverFlags.Tags) == 0 err = config.ParseInventory(userArgs) core.CheckIfError(err) diff --git a/core/dao/config.go b/core/dao/config.go index 42428fe..9bd3e5e 100644 --- a/core/dao/config.go +++ b/core/dao/config.go @@ -556,7 +556,7 @@ tasks: return []Server{}, err } - f.Close() + _ = f.Close() fmt.Println("\nInitialized sake in", configDir) fmt.Println("- Created sake.yaml") diff --git a/core/dao/theme.go b/core/dao/theme.go index 07ef7d9..2ac0dbd 100644 --- a/core/dao/theme.go +++ b/core/dao/theme.go @@ -116,7 +116,7 @@ var StyleBoxLight = table.BoxStyle{ BottomLeft: "└", BottomRight: "┘", BottomSeparator: "┴", - EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("┼")), + EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("┼")), Left: "│", LeftSeparator: "├", MiddleHorizontal: "─", @@ -137,7 +137,7 @@ var StyleBoxASCII = table.BoxStyle{ BottomLeft: "+", BottomRight: "+", BottomSeparator: "+", - EmptySeparator: text.RepeatAndTrim(" ", text.RuneCount("+")), + EmptySeparator: text.RepeatAndTrim(" ", text.RuneWidthWithoutEscSequences("+")), Left: "|", LeftSeparator: "+", MiddleHorizontal: "-", diff --git a/core/run/exec.go b/core/run/exec.go index 45ef159..38a4579 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -461,7 +461,7 @@ func (run *Run) CleanupClients() { for _, c := range clients { if remote, ok := c.(*SSHClient); ok { for i := range c.(*SSHClient).Sessions { - remote.Close(i) + _ = remote.Close(i) } } } diff --git a/core/run/ssh.go b/core/run/ssh.go index 403e08f..c9e37c4 100644 --- a/core/run/ssh.go +++ b/core/run/ssh.go @@ -184,7 +184,7 @@ func (c *SSHClient) Wait(i int) error { } err := c.Sessions[i].sess.Wait() - c.Sessions[i].sess.Close() + _ = c.Sessions[i].sess.Close() c.Sessions[i].running = false c.Sessions[i].sessOpened = false @@ -194,7 +194,7 @@ func (c *SSHClient) Wait(i int) error { // Close closes the underlying SSH connection and session. func (c *SSHClient) Close(i int) error { if c.Sessions[i].sessOpened { - c.Sessions[i].sess.Close() + _ = c.Sessions[i].sess.Close() c.Sessions[i].sessOpened = false } if !c.connOpened { @@ -339,7 +339,7 @@ func AddKnownHost(host string, key ssh.PublicKey, knownFile string) (err error) return err } - defer f.Close() + defer func() { _ = f.Close() }() line := Line(host, key) _, err = f.WriteString(line + "\n") diff --git a/core/ssh_config.go b/core/ssh_config.go index 9a493ce..93ec13e 100644 --- a/core/ssh_config.go +++ b/core/ssh_config.go @@ -20,7 +20,7 @@ func ParseSSHConfig(path string) (map[string](Endpoint), error) { if err != nil { return nil, fmt.Errorf("failed to open config: %w", err) } - defer f.Close() + defer func() { _ = f.Close() }() endpoints, err := ParseReader(f, path) if err != nil { @@ -336,7 +336,7 @@ func parseFileInternal(path string) (*hostinfoMap, error) { if err != nil { return nil, fmt.Errorf("failed to open config: %w", err) } - defer f.Close() + defer func() { _ = f.Close() }() return parseInternal(f, path) } diff --git a/core/test/utils.go b/core/test/utils.go index 8462f3b..f251968 100644 --- a/core/test/utils.go +++ b/core/test/utils.go @@ -38,11 +38,7 @@ func CheckEqN(t *testing.T, found int, wanted int) { // Equal tells whether a and b contain the same elements. // A nil argument is equivalent to an empty slice. func CheckEqualStringArr(t *testing.T, found []string, wanted []string) { - equal := true - - if len(found) != len(wanted) { - equal = false - } + equal := len(found) == len(wanted) for i, v := range found { if v != wanted[i] { equal = false diff --git a/core/utils_test.go b/core/utils_test.go index 1c15448..b0f26c4 100644 --- a/core/utils_test.go +++ b/core/utils_test.go @@ -17,7 +17,7 @@ func checkEqHost(t *testing.T, hostname string, defaultUser string, defaultPort t.Fatalf(`Wanted: %q, Found: %q`, wantedUser, foundUser) } if foundPort != wantedPort { - t.Fatalf(`Wanted: %q, Found: %q`, wantedPort, foundPort) + t.Fatalf(`Wanted: %d, Found: %d`, wantedPort, foundPort) } } diff --git a/test/integration/main_test.go b/test/integration/main_test.go index 9696904..49f2679 100644 --- a/test/integration/main_test.go +++ b/test/integration/main_test.go @@ -45,7 +45,7 @@ func (tt TemplateTest) GoldenOutput(output []byte) []byte { func clearGolden(file string) { // Guard against accidentally deleting outside directory if strings.Contains(file, "golden") { - os.RemoveAll(file) + _ = os.RemoveAll(file) } } @@ -53,7 +53,7 @@ func clearTmp() { files, _ := os.ReadDir(".") for _, f := range files { filepath := path.Join(tmpDir, f.Name()) - os.Remove(filepath) + _ = os.Remove(filepath) } } From 7e8ab540038ee97d2e2b4b787e73ff5342448a1e Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Tue, 19 May 2026 23:50:04 +0200 Subject: [PATCH 05/11] Fix Makefile docker compose --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 185b2c4..e3d7fb6 100644 --- a/Makefile +++ b/Makefile @@ -32,9 +32,9 @@ test: go test -v ./core/... # Integration tests - cd ./test && docker-compose up -d + cd ./test && docker compose up -d go test -v ./test/integration/... -count=5 -clean - cd ./test && docker-compose down + cd ./test && docker compose down unit-test: go test -v ./core/... @@ -46,10 +46,10 @@ update-golden-files: go test ./test/integration/... -update mock-ssh: - cd ./test && docker-compose up + cd ./test && docker compose up mock-performance-ssh: - cd ./test && docker-compose -f docker-compose-performance.yaml up + cd ./test && docker compose -f docker-compose-performance.yaml up build: CGO_ENABLED=0 go build \ From 24e88eb9c979a527c87181541cce4042aa30a0b8 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Wed, 20 May 2026 00:04:42 +0200 Subject: [PATCH 06/11] Update compose --- test/docker-compose-performance.yaml | 2 +- test/docker-compose.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/docker-compose-performance.yaml b/test/docker-compose-performance.yaml index e891add..09f73c2 100644 --- a/test/docker-compose-performance.yaml +++ b/test/docker-compose-performance.yaml @@ -909,5 +909,5 @@ networks: ipam: driver: default config: - - subnet: 172.24.2.0/16 + - subnet: 172.24.2.0/24 gateway: 172.24.2.1 diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml index 18993e6..4109769 100644 --- a/test/docker-compose.yaml +++ b/test/docker-compose.yaml @@ -8,7 +8,7 @@ networks: ipam: driver: default config: - - subnet: 172.24.2.0/16 + - subnet: 172.24.2.0/24 gateway: 172.24.2.1 - subnet: 2001:3984:3989::/64 From b639eaac456e699c5fcc49c1d72af3a131db915a Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Wed, 20 May 2026 00:14:47 +0200 Subject: [PATCH 07/11] Update docker compose --- test/docker-compose.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml index 4109769..295b255 100644 --- a/test/docker-compose.yaml +++ b/test/docker-compose.yaml @@ -11,8 +11,8 @@ networks: - subnet: 172.24.2.0/24 gateway: 172.24.2.1 - - subnet: 2001:3984:3989::/64 - gateway: 2001:3984:3989::1 + - subnet: fd00:dead:beef::/64 + gateway: fd00:dead:beef::1 services: server-1: @@ -99,7 +99,7 @@ services: - '230:22' networks: sake: - ipv6_address: 2001:3984:3989::10 + ipv6_address: fd00:dead:beef::10 server-10: # Only accesible via bastion-1 container_name: 'server-10' From cfac47fcffb6dcf5b1a13d66790330cb48d57b56 Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Wed, 20 May 2026 00:22:31 +0200 Subject: [PATCH 08/11] Update test servers --- test/servers.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/servers.yaml b/test/servers.yaml index edc993a..e26d74f 100644 --- a/test/servers.yaml +++ b/test/servers.yaml @@ -111,7 +111,7 @@ servers: server-9: desc: server-9 - host: 2001:3984:3989::10 + host: fd00:dead:beef::10 user: test password: test tags: [remote, demo, reachable] From 6742944ecb74d4e616c06624a279a5425a80ab0b Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Wed, 20 May 2026 01:33:18 +0200 Subject: [PATCH 09/11] Stabilize integration tests and fix run-execution bugs - Pin server-9 IPv4 (172.24.2.12) and swap IPv6 to ULA so containers come up cleanly in CI under the /24 subnet - Fix sshd MaxStartups to 100:30:100 (single-integer form left start/rate at default, causing random handshake drops under parallel load) - Drop obsolete compose `version` attribute - core/run: use `&&` instead of `;` between `cd workDir` and the command so a failed `cd` aborts the task - core/run/exec.go: fix PubFile pointer assignment - core/run/unix.go: pass IdentityFile via -i for interactive SSH - Regenerate golden files for IPv6 address change Co-Authored-By: Claude Opus 4.7 (1M context) --- core/run/exec.go | 2 +- core/run/localhost.go | 2 +- core/run/ssh.go | 2 +- core/run/unix.go | 4 ++++ test/Dockerfile | 2 +- test/docker-compose-performance.yaml | 1 - test/docker-compose.yaml | 3 +-- test/integration/golden/golden-10.stdout | 2 +- test/integration/golden/golden-15.stdout | 4 ++-- test/integration/golden/golden-23.stdout | 4 ++-- test/integration/golden/golden-24.stdout | 4 ++-- test/integration/golden/golden-25.stdout | 4 ++-- test/integration/golden/golden-26.stdout | 6 +++--- test/integration/golden/golden-27.stdout | 4 ++-- test/integration/golden/golden-28.stdout | 4 ++-- test/integration/golden/golden-29.stdout | 4 ++-- test/integration/golden/golden-30.stdout | 4 ++-- test/integration/golden/golden-31.stdout | 4 ++-- test/integration/golden/golden-32.stdout | 4 ++-- test/integration/golden/golden-33.stdout | 4 ++-- test/integration/golden/golden-34.stdout | 4 ++-- test/integration/golden/golden-35.stdout | 4 ++-- test/integration/golden/golden-36.stdout | 4 ++-- test/integration/golden/golden-38.stdout | 4 ++-- test/integration/golden/golden-39.stdout | 4 ++-- test/integration/golden/golden-4.stdout | 2 +- test/integration/golden/golden-40.stdout | 4 ++-- test/integration/golden/golden-41.stdout | 4 ++-- test/integration/golden/golden-42.stdout | 4 ++-- 29 files changed, 52 insertions(+), 50 deletions(-) diff --git a/core/run/exec.go b/core/run/exec.go index 38a4579..414e35f 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -648,7 +648,7 @@ func ParseServers( errConnects = append(errConnects, *errConnect) continue } else { - *(*servers)[i].PubFile = pubFile + (*servers)[i].PubFile = &pubFile } } diff --git a/core/run/localhost.go b/core/run/localhost.go index b54c57e..93d0e3a 100644 --- a/core/run/localhost.go +++ b/core/run/localhost.go @@ -47,7 +47,7 @@ func (c *LocalhostClient) Run(i int, env []string, workDir string, shell string, var cmdString string if workDir != "" { - cmdString = fmt.Sprintf("cd %s; %s", workDir, cmdStr) + cmdString = fmt.Sprintf("cd %s && %s", workDir, cmdStr) } else { cmdString = cmdStr } diff --git a/core/run/ssh.go b/core/run/ssh.go index c9e37c4..5d46c2f 100644 --- a/core/run/ssh.go +++ b/core/run/ssh.go @@ -153,7 +153,7 @@ func (c *SSHClient) Run(i int, env []string, workDir string, shell string, cmdSt var cmdString string if workDir != "" { - cmdString = fmt.Sprintf("cd %s; %s", workDir, exportedEnv) + cmdString = fmt.Sprintf("cd %s && %s", workDir, exportedEnv) } else { cmdString = exportedEnv } diff --git a/core/run/unix.go b/core/run/unix.go index 248ecc9..a0462f9 100644 --- a/core/run/unix.go +++ b/core/run/unix.go @@ -30,6 +30,10 @@ func SSHToServer(server dao.Server, disableVerifyHost bool, knownHostFile string args = append(args, fmt.Sprintf("-o UserKnownHostsFile=%s", knownHostFile)) } + if server.IdentityFile != nil && *server.IdentityFile != "" { + args = append(args, fmt.Sprintf("-i %s", *server.IdentityFile)) + } + // TODO: if len(server.Bastions) > 0 { jumphosts := []string{} diff --git a/test/Dockerfile b/test/Dockerfile index 329ec5c..c60f516 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -16,7 +16,7 @@ USER root RUN usermod -aG sudo test RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers -RUN echo 'MaxStartups 100' >> /etc/ssh/sshd_config +RUN echo 'MaxStartups 100:30:100' >> /etc/ssh/sshd_config RUN service ssh start diff --git a/test/docker-compose-performance.yaml b/test/docker-compose-performance.yaml index 09f73c2..146103a 100644 --- a/test/docker-compose-performance.yaml +++ b/test/docker-compose-performance.yaml @@ -1,5 +1,4 @@ --- -version: "3.9" services: server-1: diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml index 295b255..8cf6cda 100644 --- a/test/docker-compose.yaml +++ b/test/docker-compose.yaml @@ -1,6 +1,4 @@ --- -version: '3.9' - networks: sake: name: sake @@ -99,6 +97,7 @@ services: - '230:22' networks: sake: + ipv4_address: 172.24.2.12 ipv6_address: fd00:dead:beef::10 server-10: # Only accesible via bastion-1 diff --git a/test/integration/golden/golden-10.stdout b/test/integration/golden/golden-10.stdout index b308363..9c99baa 100755 --- a/test/integration/golden/golden-10.stdout +++ b/test/integration/golden/golden-10.stdout @@ -176,7 +176,7 @@ tags: remote, demo, reachable name: server-9 desc: server-9 user: test -host: 2001:3984:3989::10 +host: fd00:dead:beef::10 port: 22 tags: remote, demo, reachable diff --git a/test/integration/golden/golden-15.stdout b/test/integration/golden/golden-15.stdout index 4f09cf4..28a613c 100755 --- a/test/integration/golden/golden-15.stdout +++ b/test/integration/golden/golden-15.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | pong 172.24.2.8 | pong 172.24.2.9 | pong - 2001:3984:3989::10 | pong + fd00:dead:beef::10 | pong localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 @@ -41,7 +41,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-23.stdout b/test/integration/golden/golden-23.stdout index 56d7e43..87e7f5b 100755 --- a/test/integration/golden/golden-23.stdout +++ b/test/integration/golden/golden-23.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | pong 172.24.2.8 | pong 172.24.2.9 | pong - 2001:3984:3989::10 | pong + fd00:dead:beef::10 | pong 172.24.2.10 | pong 172.24.2.11 | pong @@ -43,7 +43,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.11 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ diff --git a/test/integration/golden/golden-24.stdout b/test/integration/golden/golden-24.stdout index fd4d9d6..d45438d 100755 --- a/test/integration/golden/golden-24.stdout +++ b/test/integration/golden/golden-24.stdout @@ -84,7 +84,7 @@ TASKS | cookie | release | task local - 2001:3984:3989::10 | foo xyz + fd00:dead:beef::10 | foo xyz | hello | cookie | release @@ -105,7 +105,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-25.stdout b/test/integration/golden/golden-25.stdout index f8ac2fa..d48e309 100755 --- a/test/integration/golden/golden-25.stdout +++ b/test/integration/golden/golden-25.stdout @@ -99,7 +99,7 @@ TASKS | release | release | task local | task remote | xyz xyz | xyz xyz - 2001:3984:3989::10 | foo xyz | foo xyz + fd00:dead:beef::10 | foo xyz | foo xyz | hello | hello | cookie | cookie | release | release @@ -121,7 +121,7 @@ TASKS 172.24.2.7 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=32 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-26.stdout b/test/integration/golden/golden-26.stdout index 2a5b97e..131a8e3 100755 --- a/test/integration/golden/golden-26.stdout +++ b/test/integration/golden/golden-26.stdout @@ -84,9 +84,9 @@ TASKS | S_HOST 172.24.2.9 | S_USER test | S_PORT 22 - 2001:3984:3989::10 | # SERVER + fd00:dead:beef::10 | # SERVER | S_TAGS remote,demo,reachable - | S_HOST 2001:3984:3989::10 + | S_HOST fd00:dead:beef::10 | S_USER test | S_PORT 22 @@ -105,7 +105,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-27.stdout b/test/integration/golden/golden-27.stdout index b77d2ea..b462b71 100755 --- a/test/integration/golden/golden-27.stdout +++ b/test/integration/golden/golden-27.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | pong | pong | pong | pong | pong | pong 172.24.2.8 | pong | pong | pong | pong | pong | pong 172.24.2.9 | pong | pong | pong | pong | pong | pong - 2001:3984:3989::10 | pong | pong | pong | pong | pong | pong + fd00:dead:beef::10 | pong | pong | pong | pong | pong | pong localhost ok=6 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 @@ -41,7 +41,7 @@ TASKS 172.24.2.7 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=6 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=96 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-28.stdout b/test/integration/golden/golden-28.stdout index e6caf46..7047261 100755 --- a/test/integration/golden/golden-28.stdout +++ b/test/integration/golden/golden-28.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | /home | /opt | /home | / 172.24.2.8 | /home | /opt | /home | / 172.24.2.9 | /home | /opt | /home | / - 2001:3984:3989::10 | /home | /opt | /home | / + fd00:dead:beef::10 | /home | /opt | /home | / localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 @@ -41,7 +41,7 @@ TASKS 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-29.stdout b/test/integration/golden/golden-29.stdout index 6b0a1ba..98dff7e 100755 --- a/test/integration/golden/golden-29.stdout +++ b/test/integration/golden/golden-29.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | /usr | /opt | /home/test | / 172.24.2.8 | /usr | /opt | /home/test | / 172.24.2.9 | /usr | /opt | /home/test | / - 2001:3984:3989::10 | /usr | /opt | /home/test | / + fd00:dead:beef::10 | /usr | /opt | /home/test | / localhost ok=4 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 @@ -41,7 +41,7 @@ TASKS 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-30.stdout b/test/integration/golden/golden-30.stdout index 1fb1a22..6996782 100755 --- a/test/integration/golden/golden-30.stdout +++ b/test/integration/golden/golden-30.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | /usr | /etc 172.24.2.8 | /usr | /etc 172.24.2.9 | /usr | /etc - 2001:3984:3989::10 | /usr | /etc + fd00:dead:beef::10 | /usr | /etc localhost ok=2 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 @@ -41,7 +41,7 @@ TASKS 172.24.2.7 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=2 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=32 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-31.stdout b/test/integration/golden/golden-31.stdout index e2634a6..062f08c 100755 --- a/test/integration/golden/golden-31.stdout +++ b/test/integration/golden/golden-31.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | foo 172.24.2.8 | foo 172.24.2.9 | foo - 2001:3984:3989::10 | foo + fd00:dead:beef::10 | foo localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 @@ -41,7 +41,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-32.stdout b/test/integration/golden/golden-32.stdout index 9b2199b..f01e133 100755 --- a/test/integration/golden/golden-32.stdout +++ b/test/integration/golden/golden-32.stdout @@ -174,7 +174,7 @@ TASKS | | | | failed: false | | | | stdout: | | | | stderr: error 2 - 2001:3984:3989::10 | foo | status: ok | error 2 | status: ok + fd00:dead:beef::10 | foo | status: ok | error 2 | status: ok | | rc: 0 | | rc: 0 | | failed: false | | failed: false | | stdout: foo | | stdout: foo @@ -201,7 +201,7 @@ TASKS 172.24.2.7 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=4 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=64 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-33.stdout b/test/integration/golden/golden-33.stdout index 949d651..f1235a8 100755 --- a/test/integration/golden/golden-33.stdout +++ b/test/integration/golden/golden-33.stdout @@ -39,7 +39,7 @@ TASKS | Process exited with status 1 172.24.2.9 | | Process exited with status 1 - 2001:3984:3989::10 | + fd00:dead:beef::10 | | Process exited with status 1 localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 @@ -57,7 +57,7 @@ TASKS 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + fd00:dead:beef::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 ------------------------------------------------------------------------------ Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 diff --git a/test/integration/golden/golden-34.stdout b/test/integration/golden/golden-34.stdout index cc9919b..8903802 100755 --- a/test/integration/golden/golden-34.stdout +++ b/test/integration/golden/golden-34.stdout @@ -39,7 +39,7 @@ TASKS | Process exited with status 1 172.24.2.9 | | Process exited with status 1 - 2001:3984:3989::10 | + fd00:dead:beef::10 | | Process exited with status 1 localhost ok=0 unreachable=0 ignored=0 failed=1 skipped=0 @@ -57,7 +57,7 @@ TASKS 172.24.2.7 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 172.24.2.8 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 172.24.2.9 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 - 2001:3984:3989::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 + fd00:dead:beef::10 ok=0 unreachable=0 ignored=0 failed=1 skipped=0 ------------------------------------------------------------------------------ Total ok=0 unreachable=0 ignored=0 failed=16 skipped=0 diff --git a/test/integration/golden/golden-35.stdout b/test/integration/golden/golden-35.stdout index b29f4bf..9883843 100755 --- a/test/integration/golden/golden-35.stdout +++ b/test/integration/golden/golden-35.stdout @@ -39,7 +39,7 @@ TASKS | | Process exited with status 65 | 172.24.2.9 | 123 | | | | Process exited with status 65 | - 2001:3984:3989::10 | 123 | | + fd00:dead:beef::10 | 123 | | | | Process exited with status 65 | localhost ok=1 unreachable=0 ignored=0 failed=1 skipped=1 @@ -57,7 +57,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=1 skipped=1 -------------------------------------------------------------------------------- Total ok=16 unreachable=0 ignored=0 failed=16 skipped=16 diff --git a/test/integration/golden/golden-36.stdout b/test/integration/golden/golden-36.stdout index 077b2ec..fc70036 100755 --- a/test/integration/golden/golden-36.stdout +++ b/test/integration/golden/golden-36.stdout @@ -39,7 +39,7 @@ TASKS | | Process exited with status 65 | 172.24.2.9 | 123 | | 321 | | Process exited with status 65 | - 2001:3984:3989::10 | 123 | | 321 + fd00:dead:beef::10 | 123 | | 321 | | Process exited with status 65 | localhost ok=2 unreachable=0 ignored=1 failed=0 skipped=0 @@ -57,7 +57,7 @@ TASKS 172.24.2.7 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 172.24.2.8 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 172.24.2.9 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 - 2001:3984:3989::10 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 + fd00:dead:beef::10 ok=2 unreachable=0 ignored=1 failed=0 skipped=0 ------------------------------------------------------------------------------- Total ok=32 unreachable=0 ignored=16 failed=0 skipped=0 diff --git a/test/integration/golden/golden-38.stdout b/test/integration/golden/golden-38.stdout index 547a5f5..f75bb2a 100755 --- a/test/integration/golden/golden-38.stdout +++ b/test/integration/golden/golden-38.stdout @@ -32,7 +32,7 @@ TASKS 172.24.2.7 | 123 172.24.2.8 | 123 172.24.2.9 | 123 - 2001:3984:3989::10 | 123 + fd00:dead:beef::10 | 123 172.24.2.10 | 123 172.24.2.11 | 123 @@ -51,7 +51,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.11 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.50 ok=0 unreachable=1 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-39.stdout b/test/integration/golden/golden-39.stdout index 29914a5..892e816 100755 --- a/test/integration/golden/golden-39.stdout +++ b/test/integration/golden/golden-39.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | Exists 172.24.2.8 | Exists 172.24.2.9 | Exists - 2001:3984:3989::10 | Exists + fd00:dead:beef::10 | Exists localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 @@ -41,7 +41,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-4.stdout b/test/integration/golden/golden-4.stdout index 1bc3ef5..e96c8e6 100755 --- a/test/integration/golden/golden-4.stdout +++ b/test/integration/golden/golden-4.stdout @@ -23,7 +23,7 @@ WantErr: false server-6 | 172.24.2.7 | remote,sandbox,reachable | server-6 server-7 | 172.24.2.8 | remote,demo,reachable | server-7 server-8 | 172.24.2.9 | remote,demo,reachable | server-8 - server-9 | 2001:3984:3989::10 | remote,demo,reachable | server-9 + server-9 | fd00:dead:beef::10 | remote,demo,reachable | server-9 server-10 | 172.24.2.10 | remote,bastion | server-10 desc server-11 | 172.24.2.11 | remote,bastion | server-11 desc diff --git a/test/integration/golden/golden-40.stdout b/test/integration/golden/golden-40.stdout index 4b7c5b0..76d1acf 100755 --- a/test/integration/golden/golden-40.stdout +++ b/test/integration/golden/golden-40.stdout @@ -23,7 +23,7 @@ TASKS 172.24.2.7 | Exists 172.24.2.8 | Exists 172.24.2.9 | Exists - 2001:3984:3989::10 | Exists + fd00:dead:beef::10 | Exists localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 @@ -40,7 +40,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-41.stdout b/test/integration/golden/golden-41.stdout index e0ab3f2..f4461dc 100755 --- a/test/integration/golden/golden-41.stdout +++ b/test/integration/golden/golden-41.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | Hello world | Bye world | Hello again world 172.24.2.8 | Hello world | Bye world | Hello again world 172.24.2.9 | Hello world | Bye world | Hello again world - 2001:3984:3989::10 | Hello world | Bye world | Hello again world + fd00:dead:beef::10 | Hello world | Bye world | Hello again world localhost ok=3 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 @@ -41,7 +41,7 @@ TASKS 172.24.2.7 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=3 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=48 unreachable=0 ignored=0 failed=0 skipped=0 diff --git a/test/integration/golden/golden-42.stdout b/test/integration/golden/golden-42.stdout index 3bfb112..a01e47c 100755 --- a/test/integration/golden/golden-42.stdout +++ b/test/integration/golden/golden-42.stdout @@ -24,7 +24,7 @@ TASKS 172.24.2.7 | 123 172.24.2.8 | 123 172.24.2.9 | 123 - 2001:3984:3989::10 | 123 + fd00:dead:beef::10 | 123 localhost ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.2 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 @@ -41,7 +41,7 @@ TASKS 172.24.2.7 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.8 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 172.24.2.9 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 - 2001:3984:3989::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 + fd00:dead:beef::10 ok=1 unreachable=0 ignored=0 failed=0 skipped=0 ------------------------------------------------------------------------------ Total ok=16 unreachable=0 ignored=0 failed=0 skipped=0 From ded056e9cb98cce3a489b19be5bac01ba53e61da Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Wed, 20 May 2026 23:02:31 +0200 Subject: [PATCH 10/11] Fix env injection, SSH identity arg, and dead signal trap - AsExport: split env on first '=' only and single-quote values to prevent value truncation and remote shell injection - unix SSHToServer: pass -p/-o/-i/-J option values as separate argv elements so OpenSSH does not see an embedded leading space - CleanupClients: drop the dead signal-forwarding goroutine whose wg.Wait returned immediately Co-Authored-By: Claude Opus 4.7 (1M context) --- core/run/exec.go | 35 ----------------------------------- core/run/ssh.go | 15 ++++++++++++--- core/run/unix.go | 11 +++++------ 3 files changed, 17 insertions(+), 44 deletions(-) diff --git a/core/run/exec.go b/core/run/exec.go index 414e35f..2e6d1c1 100644 --- a/core/run/exec.go +++ b/core/run/exec.go @@ -6,7 +6,6 @@ import ( "fmt" "math" "os" - "os/signal" "path/filepath" "strconv" "strings" @@ -422,40 +421,6 @@ func (run *Run) SetClients( func (run *Run) CleanupClients() { clients := run.RemoteClients - var wg sync.WaitGroup - - trap := make(chan os.Signal, 1) - signal.Notify(trap, os.Interrupt) - go func() { - for { - sig, ok := <-trap - if !ok { - return - } - for _, c := range clients { - switch c := c.(type) { - case *SSHClient: - for i := range c.Sessions { - err := c.Signal(i, sig) - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - } - } - case *LocalhostClient: - for i := range c.Sessions { - err := c.Signal(i, sig) - if err != nil { - fmt.Fprintf(os.Stderr, "%v", err) - } - } - } - } - } - }() - wg.Wait() - - signal.Stop(trap) - close(trap) // Close remote connections for _, c := range clients { diff --git a/core/run/ssh.go b/core/run/ssh.go index 5d46c2f..dcc4ad9 100644 --- a/core/run/ssh.go +++ b/core/run/ssh.go @@ -388,18 +388,27 @@ func serialize(k ssh.PublicKey) string { // Process all ENVs into a string of form // Example output: -// export FOO="bar"; export BAR="baz"; +// export FOO='bar'; export BAR='baz'; func AsExport(env []string) string { exports := `` for _, v := range env { - kv := strings.Split(v, "=") - exports += `export ` + kv[0] + `="` + kv[1] + `";` + kv := strings.SplitN(v, "=", 2) + if len(kv) != 2 { + continue + } + exports += `export ` + kv[0] + `=` + shellQuote(kv[1]) + `;` } return exports } +// shellQuote wraps a value in single quotes for safe use in a POSIX shell, +// escaping any embedded single quotes as '\''. +func shellQuote(s string) string { + return `'` + strings.ReplaceAll(s, `'`, `'\''`) + `'` +} + func GetSSHAgentSigners() ([]ssh.Signer, error) { // Load keys from SSH Agent if it's running sockPath, found := os.LookupEnv("SSH_AUTH_SOCK") diff --git a/core/run/unix.go b/core/run/unix.go index a0462f9..ee35c60 100644 --- a/core/run/unix.go +++ b/core/run/unix.go @@ -21,17 +21,16 @@ func SSHToServer(server dao.Server, disableVerifyHost bool, knownHostFile string } sshConnStr := fmt.Sprintf("%s@%s", server.User, server.Host) - portStr := fmt.Sprintf("-p %d", server.Port) - args := []string{"ssh", "-t", sshConnStr, portStr} + args := []string{"ssh", "-t", sshConnStr, "-p", fmt.Sprintf("%d", server.Port)} if disableVerifyHost { - args = append(args, "-o StrictHostKeyChecking=no") + args = append(args, "-o", "StrictHostKeyChecking=no") } else { - args = append(args, fmt.Sprintf("-o UserKnownHostsFile=%s", knownHostFile)) + args = append(args, "-o", fmt.Sprintf("UserKnownHostsFile=%s", knownHostFile)) } if server.IdentityFile != nil && *server.IdentityFile != "" { - args = append(args, fmt.Sprintf("-i %s", *server.IdentityFile)) + args = append(args, "-i", *server.IdentityFile) } // TODO: @@ -41,7 +40,7 @@ func SSHToServer(server dao.Server, disableVerifyHost bool, knownHostFile string jumphosts = append(jumphosts, fmt.Sprintf("%s@%s:%d", bastion.User, bastion.Host, bastion.Port)) } - args = append(args, fmt.Sprintf("-J %s", strings.Join(jumphosts, ","))) + args = append(args, "-J", strings.Join(jumphosts, ",")) } err = unix.Exec(sshBin, args, os.Environ()) From 46c6cc09fa7e44005d6b5d3cc39f6ee03d80eafa Mon Sep 17 00:00:00 2001 From: samiralajmovic Date: Wed, 20 May 2026 23:23:02 +0200 Subject: [PATCH 11/11] Respect target invert flag and apply --invert to config targets GetTaskServers passed runFlags.Invert to FilterServers for config targets, but that value was always false there since any explicit --invert routes to the runtime-selector branch. As a result a target with invert: true never inverted, and --invert combined with a config target was ignored. Use the target's configured invert (overridable by an explicit --invert), and stop treating --invert as a runtime selector since it is a modifier, not a server selector. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/dao/task.go | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/core/dao/task.go b/core/dao/task.go index 36be344..a30c1e4 100644 --- a/core/dao/task.go +++ b/core/dao/task.go @@ -364,24 +364,30 @@ func ParseTaskEnv(cmdEnv []string, userEnv []string, parentEnv []string, configE func (c *Config) GetTaskServers(task *Task, runFlags *core.RunFlags, setRunFlags *core.SetRunFlags) ([]Server, error) { var servers []Server var err error - // If any runtime target flags are used, disregard config specified task targets - if len(runFlags.Servers) > 0 || len(runFlags.Tags) > 0 || runFlags.Regex != "" || setRunFlags.All || setRunFlags.Invert { + // If any runtime selector flags are used, disregard config specified task targets. + // --invert is a modifier, not a selector, so it does not trigger this on its own. + if len(runFlags.Servers) > 0 || len(runFlags.Tags) > 0 || runFlags.Regex != "" || setRunFlags.All { servers, err = c.FilterServers(runFlags.All, runFlags.Servers, runFlags.Tags, runFlags.Regex, runFlags.Invert) if err != nil { return []Server{}, err } - } else if runFlags.Target != "" { - target, err := c.GetTarget(runFlags.Target) - if err != nil { - return []Server{}, err + } else { + // A target named on the CLI overrides the task's config target + if runFlags.Target != "" { + target, err := c.GetTarget(runFlags.Target) + if err != nil { + return []Server{}, err + } + task.Target = *target } - task.Target = *target - servers, err = c.FilterServers(task.Target.All, task.Target.Servers, task.Target.Tags, task.Target.Regex, runFlags.Invert) - if err != nil { - return []Server{}, err + + // Use the target's configured invert, but let an explicit --invert flag override it + invert := task.Target.Invert + if setRunFlags.Invert { + invert = runFlags.Invert } - } else { - servers, err = c.FilterServers(task.Target.All, task.Target.Servers, task.Target.Tags, task.Target.Regex, runFlags.Invert) + + servers, err = c.FilterServers(task.Target.All, task.Target.Servers, task.Target.Tags, task.Target.Regex, invert) if err != nil { return []Server{}, err }