From 78f0c538713483f2e87582490e6bc066bc6468c6 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 19 Nov 2025 22:46:00 -0500 Subject: [PATCH 1/7] feat: add ability to specify an app path via environment variable This can be used for testing as well as in real usage. --- contrib/dokku_client.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contrib/dokku_client.sh b/contrib/dokku_client.sh index d2c7c4f04..c77ec1130 100755 --- a/contrib/dokku_client.sh +++ b/contrib/dokku_client.sh @@ -3,6 +3,7 @@ set -eo pipefail [[ $DOKKU_TRACE ]] && set -x export DOKKU_PORT=${DOKKU_PORT:=22} export DOKKU_HOST=${DOKKU_HOST:=} +export DOKKU_APP_PATH=${DOKKU_APP_PATH:=} fn-random-number() { [[ -n "$1" ]] && RANGE="$1" @@ -67,6 +68,7 @@ fn-get-remote() { main() { declare CMD="$1" APP_ARG="$2" + local APP="" DOKKU_GIT_REMOTE="$(fn-get-remote)" DOKKU_REMOTE_HOST="" local cmd_set=false next_index=1 skip=false args=("$@") @@ -84,6 +86,10 @@ main() { APP=${args[$next_index]} skip=true shift 2 + elif [[ "$arg" == "--app-path" ]]; then + DOKKU_APP_PATH=${args[$next_index]} + skip=true + shift 2 elif [[ "$arg" == "--remote" ]]; then DOKKU_GIT_REMOTE=${args[$next_index]} skip=true @@ -103,6 +109,11 @@ main() { next_index=$((next_index + 1)) done + if [[ -n "$DOKKU_APP_PATH" ]]; then + pushd "$DOKKU_APP_PATH" &>/dev/null || exit 1 + trap 'popd &>/dev/null || true' RETURN INT TERM + fi + DOKKU_REMOTE_HOST="$(fn-dokku-host "$DOKKU_GIT_REMOTE" "$DOKKU_HOST")" if [[ -z "$DOKKU_REMOTE_HOST" ]] && [[ "$CMD" != remote ]] && [[ "$CMD" != remote:* ]]; then fn-client-help-msg From 7a93b11bdd679b485eb1dbb367de08856e279b5a Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 19 Nov 2025 22:46:29 -0500 Subject: [PATCH 2/7] feat: add tests for remote parsing --- contrib/dokku_client.sh | 10 ++++++ tests/unit/client.bats | 68 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/contrib/dokku_client.sh b/contrib/dokku_client.sh index c77ec1130..0446bc628 100755 --- a/contrib/dokku_client.sh +++ b/contrib/dokku_client.sh @@ -130,6 +130,16 @@ main() { fi case "$CMD" in + remote:show) + echo "dokku-app: $APP" + echo "dokku-git-remote: $DOKKU_GIT_REMOTE" + echo "dokku-host: $DOKKU_HOST" + echo "dokku-port: $DOKKU_PORT" + echo "dokku-remote-host: $DOKKU_REMOTE_HOST" + echo "dokku-ssh-user: dokku" + echo "dokku-constructed-remote: ssh://dokku@$DOKKU_REMOTE_HOST:$DOKKU_PORT/$APP" + exit 0 + ;; apps:create) if [[ -z "$APP" ]] && [[ -z "$APP_ARG" ]]; then APP=$(fn-random-name) diff --git a/tests/unit/client.bats b/tests/unit/client.bats index 0c0610ea9..7cb3fe177 100644 --- a/tests/unit/client.bats +++ b/tests/unit/client.bats @@ -24,6 +24,74 @@ teardown() { assert_exit_status 20 } +@test "(client) remote:show" { + unset DOKKU_HOST + local DOKKU_APP_PATH=$(mktemp -d "/tmp/${DOKKU_DOMAIN}.XXXXX") + trap 'popd &>/dev/null || true; rm -rf "$DOKKU_APP_PATH"' INT TERM + + setup_client_repo "$DOKKU_APP_PATH" + export DOKKU_APP_PATH="$DOKKU_APP_PATH" + + # handle simple remote: dokku@example.com:app-name + run /bin/bash -c "git -C "$DOKKU_APP_PATH" remote add dokku 'dokku@example.com:$TEST_APP'" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "DOKKU_APP_PATH=$DOKKU_APP_PATH ${BATS_TEST_DIRNAME}/../../contrib/dokku_client.sh remote:show" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "dokku-app: $TEST_APP" + assert_output_contains "dokku-git-remote: dokku" + assert_output_not_contains "dokku-host: example.com" + assert_output_not_contains "dokku-host: example.net" + assert_output_contains "dokku-host: " + assert_output_contains "dokku-port: 22" + assert_output_contains "dokku-remote-host: example.com" + assert_output_contains "dokku-ssh-user: dokku" + assert_output_contains "dokku-constructed-remote: ssh://dokku@example.com:22/$TEST_APP" + + # handle custom DOKKU_HOST (no app name detected without --app flag) + export DOKKU_HOST="example.com" + run /bin/bash -c "DOKKU_HOST="example.net" DOKKU_APP_PATH=$DOKKU_APP_PATH ${BATS_TEST_DIRNAME}/../../contrib/dokku_client.sh remote:show" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_not_contains "dokku-app: $TEST_APP" + assert_output_contains "dokku-git-remote: dokku" + assert_output_contains "dokku-host: example.net" + assert_output_contains "dokku-port: 22" + assert_output_contains "dokku-remote-host: example.net" + assert_output_contains "dokku-ssh-user: dokku" + assert_output_contains "dokku-constructed-remote: ssh://dokku@example.net:22/" + assert_output_not_contains "dokku-constructed-remote: ssh://dokku@example.net:22/$TEST_APP" + + # handle custom DOKKU_HOST (app name detected with --app flag) + run /bin/bash -c "DOKKU_HOST="example.net" DOKKU_APP_PATH=$DOKKU_APP_PATH ${BATS_TEST_DIRNAME}/../../contrib/dokku_client.sh remote:show --app $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "dokku-app: $TEST_APP" + assert_output_contains "dokku-git-remote: dokku" + assert_output_contains "dokku-host: example.net" + assert_output_contains "dokku-port: 22" + assert_output_contains "dokku-remote-host: example.net" + assert_output_contains "dokku-ssh-user: dokku" + assert_output_contains "dokku-constructed-remote: ssh://dokku@example.net:22/$TEST_APP" + + # handle custom DOKKU_PORT + run /bin/bash -c "DOKKU_PORT=2222 DOKKU_APP_PATH=$DOKKU_APP_PATH ${BATS_TEST_DIRNAME}/../../contrib/dokku_client.sh remote:show" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "dokku-app: $TEST_APP" + assert_output_contains "dokku-git-remote: dokku" + assert_output_contains "dokku-host: example.com" + assert_output_contains "dokku-port: 2222" + assert_output_contains "dokku-remote-host: example.com" + assert_output_contains "dokku-ssh-user: dokku" + assert_output_contains "dokku-constructed-remote: ssh://dokku@example.com:2222/$TEST_APP" @test "(client) no args should print help" { # dokku container is not run with a TTY on GitHub Actions so we don't get normal output # https://github.com/actions/runner/issues/241 From a21ee6f21684fd888fcb908c472a45b2c6880b8b Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 19 Nov 2025 23:06:47 -0500 Subject: [PATCH 3/7] tests: make it clearer what happened when assert_output_contains fails --- tests/unit/test_helper.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index 06546ce70..ac759139a 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -148,7 +148,7 @@ assert_output_contains() { input="${input/$expected/}" found=$((found + 1)) done - assert_equal "$count" "$found" + [[ "$count" -eq "$found" ]] || flunk "expected $count occurrences of '$expected', found $found" } assert_output_not_contains() { From d94bc733c9b2afa8372bc936f5a088b33f63a4a4 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 19 Nov 2025 23:07:10 -0500 Subject: [PATCH 4/7] feat: allow overriding the TMP directory in setup_client_repo --- tests/unit/test_helper.bash | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index ac759139a..93be07b18 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -375,8 +375,7 @@ deploy_app() { } setup_client_repo() { - local TMP - TMP=$(mktemp -d "/tmp/${DOKKU_DOMAIN}.XXXXX") + declare TMP=${1:=$(mktemp -d "/tmp/${DOKKU_DOMAIN}.XXXXX")} rmdir "$TMP" && cp -r "${BATS_TEST_DIRNAME}/../../tests/apps/nodejs-express" "$TMP" cd "$TMP" || exit 1 git init From 67962ad52742af41c2db3a8a69a7f84e6b52cf17 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 19 Nov 2025 23:07:40 -0500 Subject: [PATCH 5/7] tests: move helper to client.bats --- tests/unit/client.bats | 13 +++++++++++++ tests/unit/test_helper.bash | 13 ------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/unit/client.bats b/tests/unit/client.bats index 7cb3fe177..38edf49c7 100644 --- a/tests/unit/client.bats +++ b/tests/unit/client.bats @@ -400,3 +400,16 @@ teardown() { assert_success assert_output_contains "triggered smoke-test-plugin:args with args: smoke-test-plugin:args, bash, -c, echo Hello" } + +setup_client_repo() { + declare TMP=${1:=$(mktemp -d "/tmp/${DOKKU_DOMAIN}.XXXXX")} + rmdir "$TMP" && cp -r "${BATS_TEST_DIRNAME}/../../tests/apps/nodejs-express" "$TMP" + cd "$TMP" || exit 1 + git init + git config user.email "robot@example.com" + git config user.name "Test Robot" + + [[ -f gitignore ]] && mv gitignore .gitignore + git add . + git commit -m 'initial commit' +} diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index 93be07b18..3af2d7391 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -374,19 +374,6 @@ deploy_app() { git push target "master:${GIT_REMOTE_BRANCH}" || destroy_app $? } -setup_client_repo() { - declare TMP=${1:=$(mktemp -d "/tmp/${DOKKU_DOMAIN}.XXXXX")} - rmdir "$TMP" && cp -r "${BATS_TEST_DIRNAME}/../../tests/apps/nodejs-express" "$TMP" - cd "$TMP" || exit 1 - git init - git config user.email "robot@example.com" - git config user.name "Test Robot" - - [[ -f gitignore ]] && mv gitignore .gitignore - git add . - git commit -m 'initial commit' -} - setup_test_tls() { local TLS_TYPE="$1" local TLS="/home/dokku/$TEST_APP/tls" From 106b2c20f1d6626d2685504ca913340b1aa9b810 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Wed, 19 Nov 2025 23:11:26 -0500 Subject: [PATCH 6/7] feat: add support for git remotes with port specified inline Closes #8093 --- contrib/dokku_client.sh | 37 ++++++++++++++++- docs/deployment/remote-commands.md | 8 +++- tests/unit/client.bats | 67 +++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/contrib/dokku_client.sh b/contrib/dokku_client.sh index 0446bc628..2b43d004d 100755 --- a/contrib/dokku_client.sh +++ b/contrib/dokku_client.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eo pipefail [[ $DOKKU_TRACE ]] && set -x -export DOKKU_PORT=${DOKKU_PORT:=22} +export DOKKU_PORT=${DOKKU_PORT:=} export DOKKU_HOST=${DOKKU_HOST:=} export DOKKU_APP_PATH=${DOKKU_APP_PATH:=} @@ -62,6 +62,35 @@ fn-dokku-host() { echo "$DOKKU_HOST" } +fn-dokku-app() { + declare DOKKU_GIT_REMOTE="$1" DOKKU_REMOTE_HOST="$2" + remote_output="$(git remote -v 2>/dev/null | grep -Ei "^${DOKKU_GIT_REMOTE}\s" | head -n 1 | awk '{print $2}')" + + # check if there is a scheme + if echo "$remote_output" | grep -qEi "ssh://"; then + echo "$remote_output" | grep -Ei "ssh://dokku@$DOKKU_REMOTE_HOST" | cut -f2 -d'@' | cut -f2 -d'/' 2>/dev/null + else + echo "$remote_output" | grep -Ei "dokku@$DOKKU_REMOTE_HOST" | cut -f2 -d'@' | cut -f2 -d':' 2>/dev/null + fi +} + +fn-dokku-port() { + declare DOKKU_GIT_REMOTE="$1" DOKKU_REMOTE_HOST="$2" + remote_output="$(git remote -v 2>/dev/null | grep -Ei "^${DOKKU_GIT_REMOTE}\s" | head -n 1 | awk '{print $2}')" + + if echo "$remote_output" | grep -qEi "ssh://"; then + local output="$(echo "$remote_output" | grep -Ei "ssh://dokku@$DOKKU_REMOTE_HOST" | cut -f2 -d'@' | cut -f1 -d'/' 2>/dev/null)" + # check if output has a : and if so, get the port + if echo "$output" | grep -qEi ":"; then + echo "$output" | cut -f2 -d':' 2>/dev/null + else + echo "22" + fi + else + echo "22" + fi +} + fn-get-remote() { git config dokku.remote 2>/dev/null || echo "dokku" } @@ -122,13 +151,17 @@ main() { if [[ -z "$APP" ]]; then if [[ -d .git ]] || git rev-parse --git-dir &>/dev/null; then set +e - APP=$(git remote -v 2>/dev/null | grep -Ei "^${DOKKU_GIT_REMOTE}\s" | grep -Ei "dokku@$DOKKU_REMOTE_HOST" | head -n 1 | cut -f2 -d'@' | cut -f1 -d' ' | cut -f2 -d':' 2>/dev/null) + APP="$(fn-dokku-app "$DOKKU_GIT_REMOTE" "$DOKKU_REMOTE_HOST")" set -e else echo " ! This is not a git repository" 1>&2 fi fi + if [[ -z "$DOKKU_PORT" ]]; then + DOKKU_PORT="$(fn-dokku-port "$DOKKU_GIT_REMOTE" "$DOKKU_REMOTE_HOST")" + fi + case "$CMD" in remote:show) echo "dokku-app: $APP" diff --git a/docs/deployment/remote-commands.md b/docs/deployment/remote-commands.md index beebfde22..283ea2364 100644 --- a/docs/deployment/remote-commands.md +++ b/docs/deployment/remote-commands.md @@ -71,7 +71,7 @@ All commands have the application name automatically set via the `--app` flag on The client supports several environment variables: -- `DOKKU_HOST` (default: `dokku` git remote): Used to interact with a specific remote server. Can be overridden via the `--remote` flag. +- `DOKKU_HOST` (default: `dokku` git remote): Used to interact with a specific remote server. Can be overridden via the `--remote` flag. Must specify the `--app` flag when specified. - `DOKKU_PORT` (default: `22`): Used to specify a port to connect to the Dokku server on. It also supports several flags (all flags unspecified here are passed as is to the server): @@ -99,6 +99,12 @@ To list all available remotes, run `dokku remote:list`. This is equivalent to ru To add a remote, run `dokku remote:add $REMOTE_NAME dokku@dokku.me:$APP_NAME`, replacing `$REMOTE_NAME` with your desired remote name, the `$APP_NAME` with your desired app name and the host `dokku.me` with your Dokku host. This is equivalent to running `git remote add $REMOTE_NAME dokku@dokku.me:$APP_NAME`. +The following remote formats are supported: + +- `dokku@HOSTNAME.TLD:APP_NAME` +- `ssh://dokku@HOSTNAME.TLD/APP_NAME` +- `ssh://dokku@HOSTNAME.TLD:HOST_PORT/APP_NAME` + To remove a remote, run `dokku remote:remove $REMOTE_NAME`. This is equivalent to running `git remote remove $REMOTE_NAME`. By default, the remote in use is `dokku`. To change the remote the client uses by default, run `dokku remote:set $REMOTE_NAME`, where `$REMOTE_NAME` is the name of your desired remote. This will not check the value specified, so that remote *must* be available or errors will occur when running normal client commands. Note that specifying `--remote` when running dokku commands will override this configuration value. diff --git a/tests/unit/client.bats b/tests/unit/client.bats index 38edf49c7..d9d875ec2 100644 --- a/tests/unit/client.bats +++ b/tests/unit/client.bats @@ -53,7 +53,6 @@ teardown() { assert_output_contains "dokku-constructed-remote: ssh://dokku@example.com:22/$TEST_APP" # handle custom DOKKU_HOST (no app name detected without --app flag) - export DOKKU_HOST="example.com" run /bin/bash -c "DOKKU_HOST="example.net" DOKKU_APP_PATH=$DOKKU_APP_PATH ${BATS_TEST_DIRNAME}/../../contrib/dokku_client.sh remote:show" echo "output: $output" echo "status: $status" @@ -87,11 +86,75 @@ teardown() { assert_success assert_output_contains "dokku-app: $TEST_APP" assert_output_contains "dokku-git-remote: dokku" - assert_output_contains "dokku-host: example.com" + assert_output_not_contains "dokku-host: example.com" + assert_output_not_contains "dokku-host: example.net" + assert_output_contains "dokku-host: " assert_output_contains "dokku-port: 2222" assert_output_contains "dokku-remote-host: example.com" assert_output_contains "dokku-ssh-user: dokku" assert_output_contains "dokku-constructed-remote: ssh://dokku@example.com:2222/$TEST_APP" + + # handle remote with scheme (no port): ssh://dokku@example.com/app-name + run /bin/bash -c "git -C "$DOKKU_APP_PATH" remote set-url dokku 'ssh://dokku@example.com:22/$TEST_APP'" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "DOKKU_APP_PATH=$DOKKU_APP_PATH ${BATS_TEST_DIRNAME}/../../contrib/dokku_client.sh remote:show" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "dokku-app: $TEST_APP" + assert_output_contains "dokku-git-remote: dokku" + assert_output_not_contains "dokku-host: example.com" + assert_output_not_contains "dokku-host: example.net" + assert_output_contains "dokku-host: " + assert_output_contains "dokku-port: 22" + assert_output_contains "dokku-remote-host: example.com" + assert_output_contains "dokku-ssh-user: dokku" + assert_output_contains "dokku-constructed-remote: ssh://dokku@example.com:22/$TEST_APP" + + # handle remote with scheme (with port): ssh://dokku@example.com:2222/app-name + run /bin/bash -c "git -C "$DOKKU_APP_PATH" remote set-url dokku 'ssh://dokku@example.com:2222/$TEST_APP'" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "DOKKU_APP_PATH=$DOKKU_APP_PATH ${BATS_TEST_DIRNAME}/../../contrib/dokku_client.sh remote:show" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "dokku-app: $TEST_APP" + assert_output_contains "dokku-git-remote: dokku" + assert_output_not_contains "dokku-host: example.com" + assert_output_not_contains "dokku-host: example.net" + assert_output_contains "dokku-host: " + assert_output_contains "dokku-port: 2222" + assert_output_contains "dokku-remote-host: example.com" + assert_output_contains "dokku-ssh-user: dokku" + assert_output_contains "dokku-constructed-remote: ssh://dokku@example.com:2222/$TEST_APP" + + # #8093: ssh://dokku@pipescan.shipit.dev:2222/pipescan + run /bin/bash -c "git -C "$DOKKU_APP_PATH" remote set-url dokku 'ssh://dokku@pipescan.shipit.dev:2222/pipescan'" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "DOKKU_APP_PATH=$DOKKU_APP_PATH ${BATS_TEST_DIRNAME}/../../contrib/dokku_client.sh remote:show" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "dokku-app: pipescan" + assert_output_contains "dokku-git-remote: dokku" + assert_output_not_contains "dokku-host: example.com" + assert_output_not_contains "dokku-host: example.net" + assert_output_contains "dokku-host: " + assert_output_contains "dokku-port: 2222" + assert_output_contains "dokku-remote-host: pipescan.shipit.dev" + assert_output_contains "dokku-ssh-user: dokku" + assert_output_contains "dokku-constructed-remote: ssh://dokku@pipescan.shipit.dev:2222/pipescan" +} + @test "(client) no args should print help" { # dokku container is not run with a TTY on GitHub Actions so we don't get normal output # https://github.com/actions/runner/issues/241 From 63fc11a4b9cb970f462affaf200ebc377605cf94 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 20 Nov 2025 02:21:20 -0500 Subject: [PATCH 7/7] fix: properly use $1 as default value --- tests/unit/client.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/client.bats b/tests/unit/client.bats index d9d875ec2..9d81b66a0 100644 --- a/tests/unit/client.bats +++ b/tests/unit/client.bats @@ -465,7 +465,7 @@ teardown() { } setup_client_repo() { - declare TMP=${1:=$(mktemp -d "/tmp/${DOKKU_DOMAIN}.XXXXX")} + declare TMP="${1:-$(mktemp -d "/tmp/${DOKKU_DOMAIN}.XXXXX")}" rmdir "$TMP" && cp -r "${BATS_TEST_DIRNAME}/../../tests/apps/nodejs-express" "$TMP" cd "$TMP" || exit 1 git init