From 26a643d0304c671cf8d0fa88b008f0082490f809 Mon Sep 17 00:00:00 2001 From: rfay Date: Sat, 8 Aug 2026 00:39:00 +0000 Subject: [PATCH 1/2] fix(launch): honor DDEV_DEBUG and support router-disabled templates The Issue The shared `ddev launch` override (image/scripts/.ddev/commands/host/launch) never checked DDEV_DEBUG/DDEV_VERBOSE before printing the interactive Coder URL listing, breaking any tooling that relies on stock ddev's `DDEV_DEBUG=true ddev launch` -> `FULLURL ` contract (e.g. DDEV's own bats docs/tests and TestLaunchCommand). Separately, the script only ever worked for the freeform template: it reads the per-project Traefik `coder-routes` file that `ddev coder-routes` writes when ddev-router is running. drupal-core and drupal-contrib omit ddev-router and bind the web container directly to a fixed host port (see docs/reference/coder-url-patterns.md), so that file never exists there and `ddev launch` always dead-ended on "no coder-routes file found; run 'ddev coder-setup' then 'ddev start'" -- even after a successful start. How This PR Solves The Issue - Add an early DDEV_DEBUG/DDEV_VERBOSE check that prints `FULLURL ${DDEV_PRIMARY_URL}` and exits, matching stock ddev's launch script exactly, before any Coder-specific logic runs. - Detect routing mode via `ddev describe -j`'s `.raw.router_disabled`. When true (drupal-core/drupal-contrib), construct the URL directly from the `ddev-web`/`mailpit` `coder_app` slugs using Coder's named-app URL pattern (no Traefik/coder-routes dependency at all). When false (freeform), behavior is unchanged. - Add an OSC 8 hyperlink helper so the printed URL is clickable in terminals that support it (VS Code's integrated terminal does), gated on `[ -t 1 ]` so piped/non-interactive output (tooling, tests) stays plain text. Manual Testing Instructions Freeform-style (router enabled), unaffected: 1. In a freeform workspace, `ddev config --project-type=php && ddev start`, then `ddev coder-setup && ddev start` again to register routing. 2. `DDEV_DEBUG=true ddev launch` -> `FULLURL `. 3. `ddev launch` / `ddev launch -m` -> same Web/Mailpit/other-addon lines as before (verified byte-identical modulo the added hyperlink escapes, which are invisible when stdout isn't a real TTY). Direct-bind style (drupal-core/drupal-contrib), newly supported: simulate with `ddev config global --omit-containers=ddev-router && ddev restart` on any project. 1. `ddev describe -j | jq -r .raw.router_disabled` -> `true`. 2. `DDEV_DEBUG=true ddev launch` -> `FULLURL ` (unchanged). 3. `ddev launch` -> "Coder URL for project '': Web: https://ddev-web----." instead of the old "no coder-routes file found" dead end. `ddev launch /some/path` appends the path; `ddev launch -m` prints the mailpit named-app URL. 4. `ddev config global --omit-containers=` to revert. Automated Testing Overview No automated test harness exists for these shell scripts in this repo; verified manually as above in a live workspace. `bash -n` confirms no syntax errors. Release/Deployment Notes Requires rebuilding and pushing the base image (`make build-and-push`) since this script lives under `image/scripts/`, then `make push-all-templates` so existing workspaces pick it up on next rebuild. No Terraform changes. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 1 + image/scripts/.ddev/commands/host/launch | 54 +++++++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 84e10f2..b194e64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -354,6 +354,7 @@ Additional logs in workspace: - `drupal-core/template.tf` - Drupal core development template (most actively developed) - `drupal-contrib/template.tf` - Drupal contributed module development template - `freeform/template.tf` - Multi-project freeform workspace template (keeps ddev-router) +- `image/scripts/.ddev/commands/host/launch` - Shared `ddev launch` override for all templates. Honors `DDEV_DEBUG`/`DDEV_VERBOSE` (prints `FULLURL ` and exits, matching stock ddev's contract used by tooling). For interactive use, branches on `ddev describe -j`'s `.raw.router_disabled`: freeform (router enabled) reads the per-project `coder-routes` Traefik config; drupal-core/drupal-contrib (router disabled, direct port bind) construct the named-app URL directly from the `ddev-web`/`mailpit` `coder_app` slugs — no Traefik file needed. - `image/Dockerfile` - Base image build instructions (shared by all templates) - `image/scripts/.ddev/global_config.yaml` - DDEV defaults copied into workspaces - `scripts/coder-delete-workspace-dir.sh` - Sudo wrapper for workspace host dir cleanup (must be installed on server) diff --git a/image/scripts/.ddev/commands/host/launch b/image/scripts/.ddev/commands/host/launch index d045336..6de92e6 100644 --- a/image/scripts/.ddev/commands/host/launch +++ b/image/scripts/.ddev/commands/host/launch @@ -6,9 +6,30 @@ ## Example: "ddev launch" or "ddev launch /admin" or "ddev launch -m" ## Flags: [{"Name":"mailpit","Shorthand":"m","Usage":"ddev launch -m shows the Mailpit URL"}] +# Tooling (bats tests, `ddev launch | awk '/FULLURL/ {print $2}'`, DDEV's own +# TestLaunchCommand) sets DDEV_DEBUG/DDEV_VERBOSE to get a single parseable +# line instead of the interactive URL listing below. Match the stock ddev +# launch script's contract so that tooling works identically here. +if [ "${DDEV_DEBUG:-}" = "true" ] || [ "${DDEV_VERBOSE:-}" = "true" ]; then + echo "FULLURL ${DDEV_PRIMARY_URL}" + exit 0 +fi + +# Emit an OSC 8 hyperlink when stdout is a terminal that supports it (VS Code's +# integrated terminal does), so the URL is clickable even though we can't +# actually launch a browser from inside the workspace container. Terminals +# that don't understand OSC 8 just show the plain URL text. +hyperlink() { + if [ -t 1 ]; then + printf '\033]8;;%s\033\\%s\033]8;;\033\\' "$1" "$1" + else + printf '%s' "$1" + fi +} + # Outside a Coder workspace fall back to a basic URL print (no browser available in DinD) if [ -z "${CODER_WORKSPACE_NAME:-}" ] || ([ -z "${VSCODE_PROXY_URI:-}" ] && [ -z "${CODER_AGENT_URL:-}" ]); then - echo "Primary URL: ${DDEV_PRIMARY_URL:-unknown}" + echo "Primary URL: $(hyperlink "${DDEV_PRIMARY_URL:-unknown}")" echo "(Not running in a Coder workspace; cannot open a browser.)" exit 0 fi @@ -44,6 +65,25 @@ if [ -n "${1:-}" ]; then PATH_SUFFIX="/${1#/}" fi +# drupal-core/drupal-contrib omit ddev-router and bind the web container directly +# to a fixed host port, so there's no Traefik merged config for coder-routes to +# read. They register fixed `coder_app` slugs (ddev-web, mailpit) instead, whose +# subdomain URLs follow Coder's named-app pattern (no agent segment, since these +# templates define a single agent) — see docs/reference/coder-url-patterns.md. +ROUTER_DISABLED=$(ddev describe -j 2>/dev/null | jq -r '.raw.router_disabled // false') + +if [ "$ROUTER_DISABLED" = "true" ]; then + if [ "${MAILPIT}" = "true" ]; then + echo "$(hyperlink "https://mailpit--${WORKSPACE}--${OWNER}.${DOMAIN}")" + exit 0 + fi + echo "" + echo "Coder URL for project '${PROJECT}':" + echo " Web: $(hyperlink "https://ddev-web--${WORKSPACE}--${OWNER}.${DOMAIN}${PATH_SUFFIX}")" + echo "" + exit 0 +fi + # Per-project routes file (written by ddev coder-routes after each ddev start). # Falls back to the legacy single-file name for older setups. CODER_ROUTES="$HOME/.ddev/traefik/custom-global-config/coder-routes-${PROJECT}.yaml" @@ -60,12 +100,12 @@ if [ "${MAILPIT}" = "true" ]; then mailpit_rule=$(yq e ".http.routers.\"${PROJECT}-coder-mailpit-${PROJECT_SLUG}\".rule // \"\"" "$CODER_ROUTES" 2>/dev/null) if [ -n "$mailpit_rule" ] && [ "$mailpit_rule" != "null" ]; then host=$(echo "$mailpit_rule" | sed -E 's/Host\(`(.+)`\)/\1/') - echo "https://${host}" + echo "$(hyperlink "https://${host}")" exit 0 fi fi # fallback - echo "https://mailpit-${PROJECT_SLUG}--${WORKSPACE}--${OWNER}.${DOMAIN}" + echo "$(hyperlink "https://mailpit-${PROJECT_SLUG}--${WORKSPACE}--${OWNER}.${DOMAIN}")" exit 0 fi @@ -94,15 +134,15 @@ while IFS= read -r router; do # Extract hostname from Host(`...`) — these use Coder subdomain proxy host=$(echo "$rule" | sed -E 's/Host\(`(.+)`\)/\1/') if [ "$router" = "$WEB_ROUTER" ]; then - WEB_LINE=" Web: https://${host}${PATH_SUFFIX}" + WEB_LINE=" Web: $(hyperlink "https://${host}${PATH_SUFFIX}")" elif [ "$slug" = "mailpit" ]; then - MAILPIT_LINE=" Mailpit: https://${host}" + MAILPIT_LINE=" Mailpit: $(hyperlink "https://${host}")" else - OTHER_LINES="${OTHER_LINES} ${slug}: https://${host}\n" + OTHER_LINES="${OTHER_LINES} ${slug}: $(hyperlink "https://${host}")\n" fi else # PathPrefix rule — dynamic add-on, accessible via Coder port-forwarding URL - OTHER_LINES="${OTHER_LINES} ${slug}: https://${ext_port}--${AGENT}--${WORKSPACE}--${OWNER}.${DOMAIN}\n" + OTHER_LINES="${OTHER_LINES} ${slug}: $(hyperlink "https://${ext_port}--${AGENT}--${WORKSPACE}--${OWNER}.${DOMAIN}")\n" fi done < <(yq e '.http.routers | keys | .[]' "$CODER_ROUTES" 2>/dev/null) From 7d5fe485b11ad3877f3f0c233756ccbe994e083a Mon Sep 17 00:00:00 2001 From: rfay Date: Sat, 8 Aug 2026 00:54:23 +0000 Subject: [PATCH 2/2] fix(launch): apply path/mailpit adjustment before the DDEV_DEBUG early-exit The Issue The previous commit's DDEV_DEBUG/DDEV_VERBOSE early-exit printed `FULLURL ${DDEV_PRIMARY_URL}` unconditionally, before flags/path were parsed. Verified against ddev's own docs/tests/*.bats suite (copied this script into a live workspace's ~/.ddev/commands/host/launch and ran the tests against ../ddev): `wagtail.bats` failed because `DDEV_DEBUG=true ddev launch /admin` ignored the `/admin` suffix entirely. How This PR Solves The Issue Move flag/path parsing (MAILPIT, PATH_SUFFIX) to the top of the script, run once, and reuse it both in the DDEV_DEBUG branch (building FULLURL from DDEV_PRIMARY_URL with the same path/mailpit-port adjustment the stock ddev launch script applies) and in the existing interactive branches further down (unchanged, just reusing the already-parsed values instead of re-parsing). Manual Testing Instructions Copied this script into a live Coder workspace and ran the full relevant subset of ddev's docs/tests/*.bats suite against it: - generic.bats, nodejs.bats, sveltekit.bats, wagtail.bats (the four files using a generic webserver with deferred web_extra_exposed_ports) - all pass. - Verified DDEV_DEBUG=true ddev launch, `ddev launch /admin/foo`, and `ddev launch -m` all produce the correctly-adjusted FULLURL. - Verified the freeform/router-enabled interactive path (plain, path suffix, and -m) is still byte-identical to before this change. Automated Testing Overview No automated harness for these shell scripts in this repo; verified manually as above. `bash -n` confirms no syntax errors. Release/Deployment Notes Same as the previous commit on this branch - requires an image rebuild/push and template push to take effect in existing workspaces. Co-Authored-By: Claude Sonnet 5 --- image/scripts/.ddev/commands/host/launch | 57 +++++++++++++++--------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/image/scripts/.ddev/commands/host/launch b/image/scripts/.ddev/commands/host/launch index 6de92e6..4c0232a 100644 --- a/image/scripts/.ddev/commands/host/launch +++ b/image/scripts/.ddev/commands/host/launch @@ -6,12 +6,46 @@ ## Example: "ddev launch" or "ddev launch /admin" or "ddev launch -m" ## Flags: [{"Name":"mailpit","Shorthand":"m","Usage":"ddev launch -m shows the Mailpit URL"}] +# Parse flags/path before anything else, so both the DDEV_DEBUG branch below +# and the interactive branches further down agree on the same target. +MAILPIT=false +PATH_SUFFIX="" + +while :; do + case ${1:-} in + -m | --mailpit | --mailhog) + MAILPIT=true + ;; + --) shift; break ;; + -?*) printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2 ;; + *) break ;; + esac + shift +done + +if [ -n "${1:-}" ]; then + PATH_SUFFIX="/${1#/}" +fi + # Tooling (bats tests, `ddev launch | awk '/FULLURL/ {print $2}'`, DDEV's own # TestLaunchCommand) sets DDEV_DEBUG/DDEV_VERBOSE to get a single parseable # line instead of the interactive URL listing below. Match the stock ddev -# launch script's contract so that tooling works identically here. +# launch script's contract: FULLURL is DDEV_PRIMARY_URL with the same +# path/mailpit adjustment the interactive branches apply, since tooling runs +# inside the workspace where *.ddev.site resolves directly (unlike a human's +# browser, which needs the external Coder proxy URL built further down). if [ "${DDEV_DEBUG:-}" = "true" ] || [ "${DDEV_VERBOSE:-}" = "true" ]; then - echo "FULLURL ${DDEV_PRIMARY_URL}" + FULLURL="${DDEV_PRIMARY_URL}" + if [ "${MAILPIT}" = "true" ]; then + if [ "${FULLURL%://*}" = "https" ]; then + FULLURL="${FULLURL%:[0-9]*}:${DDEV_MAILPIT_HTTPS_PORT}" + else + FULLURL="${FULLURL%:[0-9]*}:${DDEV_MAILPIT_PORT}" + fi + else + FULLURL="${FULLURL%/}${PATH_SUFFIX}" + fi + echo "FULLURL ${FULLURL}" exit 0 fi @@ -46,25 +80,6 @@ fi PROJECT="${DDEV_SITENAME}" AGENT="${CODER_AGENT_NAME:-main}" -MAILPIT=false -PATH_SUFFIX="" - -while :; do - case ${1:-} in - -m | --mailpit | --mailhog) - MAILPIT=true - ;; - --) shift; break ;; - -?*) printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2 ;; - *) break ;; - esac - shift -done - -if [ -n "${1:-}" ]; then - PATH_SUFFIX="/${1#/}" -fi - # drupal-core/drupal-contrib omit ddev-router and bind the web container directly # to a fixed host port, so there's no Traefik merged config for coder-routes to # read. They register fixed `coder_app` slugs (ddev-web, mailpit) instead, whose