From 914eabb226a671770babb11b292edf2ddbe2e5da Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 17 Sep 2026 07:06:11 +0000 Subject: [PATCH] feat: Pin the sandbox service version in get-n8n.sh and update it on --upgrade (#38799) Co-authored-by: Claude Fable 5.1 --- .github/workflows/test-get-n8n.yml | 6 +- docker/get-n8n-compose.yml | 18 +++--- docker/get-n8n.sh | 81 +++++++++++++++++++------ docker/test-get-n8n.sh | 95 +++++++++++++++++++++++++++--- 4 files changed, 165 insertions(+), 35 deletions(-) diff --git a/.github/workflows/test-get-n8n.yml b/.github/workflows/test-get-n8n.yml index 0753832109d..ea8a1ca829d 100644 --- a/.github/workflows/test-get-n8n.yml +++ b/.github/workflows/test-get-n8n.yml @@ -4,9 +4,9 @@ name: 'Test get-n8n.sh installer' # fresh install boots the full stack, re-runs are no-ops, --upgrade only # bumps N8N_VERSION, and all failure modes exit cleanly. # -# Also runs nightly against master: the script pins n8n but pulls the -# sandbox images at :latest, so an image-side change can break installs -# without any change to the script itself. +# Also runs nightly against master: the install resolves the latest n8n +# release and pulls searxng at :latest, so a change outside this repo can +# break installs without any change to the script itself. on: pull_request: diff --git a/docker/get-n8n-compose.yml b/docker/get-n8n-compose.yml index a42695330cd..ade8fe433bb 100644 --- a/docker/get-n8n-compose.yml +++ b/docker/get-n8n-compose.yml @@ -1,16 +1,20 @@ # n8n stack definition for get-n8n.sh (https://get.n8n.io). # Downloaded at install time; the copy in your install directory is yours to -# edit and the script never rewrites it. To review changes between versions: +# edit. On --upgrade the script only repoints the sandbox image tags below at +# N8N_SANDBOX_VERSION. To review changes between versions: # https://github.com/n8n-io/n8n/commits/master/docker/get-n8n-compose.yml -# compose-version: 1 +# compose-version: 2 +# The sandbox service API, runner and sandbox images ship as one release and +# are only supported together, so a single N8N_SANDBOX_VERSION (from .env) +# pins all three. volumes: n8n-data: sandbox-tls: services: sandbox-certs: - image: ghcr.io/n8n-io/n8n-sandbox-service-api:1.2.0 + image: ghcr.io/n8n-io/n8n-sandbox-service-api:${N8N_SANDBOX_VERSION} user: '0:0' entrypoint: ['sh', '-c'] command: @@ -24,7 +28,7 @@ services: - sandbox-tls:/tls sandbox-api: - image: ghcr.io/n8n-io/n8n-sandbox-service-api:1.2.0 + image: ghcr.io/n8n-io/n8n-sandbox-service-api:${N8N_SANDBOX_VERSION} depends_on: sandbox-certs: condition: service_completed_successfully @@ -49,7 +53,7 @@ services: # n8n reaches this container by service name, over the default Compose network. sandbox-runner-1: - image: ghcr.io/n8n-io/n8n-sandbox-service-runner-dind:1.2.0 + image: ghcr.io/n8n-io/n8n-sandbox-service-runner-dind:${N8N_SANDBOX_VERSION} privileged: true depends_on: sandbox-api: @@ -57,11 +61,11 @@ services: env_file: .env environment: SANDBOX_RUNNER_API_GRPC_ADDR: sandbox-api:9090 - SANDBOX_RUNNER_HTTP_BASE_URL: http://sandbox-runner-1:8080 + SANDBOX_RUNNER_HTTP_BASE_URL: https://sandbox-runner-1:8080 SANDBOX_RUNNER_CONTROL_GRPC_LISTEN_ADDR: ':9091' SANDBOX_RUNNER_CONTROL_GRPC_ADVERTISE_ADDR: sandbox-runner-1:9091 SANDBOX_RUNNER_ID: runner-1 - SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE: ghcr.io/n8n-io/n8n-sandbox-service-sandbox:latest + SANDBOX_RUNNER_DOCKER_SANDBOX_IMAGE: ghcr.io/n8n-io/n8n-sandbox-service-sandbox:${N8N_SANDBOX_VERSION} SANDBOX_RUNNER_REGISTRATION_GRPC_CA_FILE: /tls/runner/ca.crt SANDBOX_RUNNER_REGISTRATION_GRPC_CERT_FILE: /tls/runner/grpc-client.crt SANDBOX_RUNNER_REGISTRATION_GRPC_KEY_FILE: /tls/runner/grpc-client.key diff --git a/docker/get-n8n.sh b/docker/get-n8n.sh index c11c52e4b72..dc886061bfb 100755 --- a/docker/get-n8n.sh +++ b/docker/get-n8n.sh @@ -12,10 +12,14 @@ # Source: https://github.com/n8n-io/n8n/blob/master/docker/get-n8n.sh set -eu -SCRIPT_VERSION="1.3.0" +SCRIPT_VERSION="1.4.0" # The version to install is derived from the latest stable GitHub release in # resolve_n8n_version(); this fallback only applies when that lookup fails. FALLBACK_N8N_VERSION="2.32.0" +# Sandbox service release (API, runner and sandbox images ship together). +# Pinned here, not resolved at run time: the stack definition must match it. +# --upgrade moves existing installs to this pin. +SANDBOX_VERSION="1.3.4" N8N_DIR="${N8N_DIR:-./n8n}" N8N_PORT=5678 SOURCE_URL="https://github.com/n8n-io/n8n/blob/master/docker/get-n8n.sh" @@ -110,9 +114,11 @@ Options: --version [x.y.z] Without a value: print script version and the n8n version that would be installed (latest stable). With a value: install (or, with --upgrade, upgrade to) that n8n version. - --upgrade Upgrade an existing install: updates the N8N_VERSION line - in .env, pulls images and restarts. Never touches any - other configuration or secrets. + --upgrade Upgrade an existing install: updates the N8N_VERSION and + N8N_SANDBOX_VERSION lines in .env, points the sandbox image + tags in compose.yml at N8N_SANDBOX_VERSION, pulls images + and restarts. Never touches any other configuration or + secrets. --no-start Write the configuration files but don't start n8n. --help Show this help. @@ -135,7 +141,7 @@ parse_args() { REQUESTED_VERSION="$2" shift else - say "get-n8n.sh v${SCRIPT_VERSION} (installs the latest stable n8n, currently $(resolve_n8n_version))" + say "get-n8n.sh v${SCRIPT_VERSION} (installs the latest stable n8n, currently $(resolve_n8n_version), with sandbox service ${SANDBOX_VERSION})" exit 0 fi ;; @@ -263,8 +269,10 @@ write_env() { cat >"${N8N_DIR}/.env" <"${N8N_DIR}/.env.tmp" + mv "${N8N_DIR}/.env.tmp" "${N8N_DIR}/.env" + else + printf '%s=%s\n' "$1" "$2" >>"${N8N_DIR}/.env" + fi + chmod 600 "${N8N_DIR}/.env" +} + +# Stack definition v1 hard-coded the sandbox image tags. Point them at +# N8N_SANDBOX_VERSION so --upgrade can move them, and switch the runner URL +# to https, which the sandbox service serves since 1.3. Only these lines +# change; the rest of compose.yml stays the user's. +migrate_compose_sandbox_pins() { + # shellcheck disable=SC2016 # the literal ${N8N_SANDBOX_VERSION} is the point + sed -e 's|\(ghcr\.io/n8n-io/n8n-sandbox-service-[a-z-]*:\)[0-9A-Za-z._-]\{1,\}|\1${N8N_SANDBOX_VERSION}|' \ + -e 's|^\( *SANDBOX_RUNNER_HTTP_BASE_URL: \)http://sandbox-runner-1:8080 *$|\1https://sandbox-runner-1:8080|' \ + "${N8N_DIR}/compose.yml" >"${N8N_DIR}/compose.yml.tmp" + if cmp -s "${N8N_DIR}/compose.yml" "${N8N_DIR}/compose.yml.tmp"; then + rm -f "${N8N_DIR}/compose.yml.tmp" + return 0 + fi + had_http=0 + grep -q 'SANDBOX_RUNNER_HTTP_BASE_URL: http://sandbox-runner-1:8080' "${N8N_DIR}/compose.yml" && had_http=1 + mv "${N8N_DIR}/compose.yml.tmp" "${N8N_DIR}/compose.yml" + ok "Updated compose.yml: sandbox images now follow N8N_SANDBOX_VERSION" + say " ${DIM}compose.yml had the sandbox service images pinned to fixed tags. Those image" + say " lines now end in :\${N8N_SANDBOX_VERSION}, so this and future upgrades set the" + say " version through .env." + [ "$had_http" -eq 0 ] || say " The runner URL now uses https, which the sandbox service serves since 1.3." + say " Nothing else in compose.yml changed.${RESET}" +} + # Existing installs keep their compose.yml untouched, so when the published # stack definition has moved on, say so instead of silently rewriting it. # Best-effort: stays quiet if offline or if the user removed the version line. @@ -329,7 +373,8 @@ check_compose_freshness() { [ -n "$latest" ] && [ "$latest" != "$installed" ] || return 0 say "" say "Note: the n8n stack definition is now v${latest}; this install was generated" - say "from v${installed}. get-n8n.sh never rewrites an existing compose.yml." + say "from v${installed}. Beyond the sandbox image tags, get-n8n.sh never rewrites" + say "an existing compose.yml." say "To review what changed: ${COMPOSE_HISTORY_URL}" } @@ -398,15 +443,15 @@ do_upgrade() { target="${REQUESTED_VERSION:-$(resolve_n8n_version)}" current="$(sed -n 's/^N8N_VERSION=//p' "${N8N_DIR}/.env")" - if [ -n "$current" ]; then - # .env is user state: --upgrade edits exactly this one line, nothing else. - sed "s/^N8N_VERSION=.*/N8N_VERSION=${target}/" "${N8N_DIR}/.env" >"${N8N_DIR}/.env.tmp" - mv "${N8N_DIR}/.env.tmp" "${N8N_DIR}/.env" - chmod 600 "${N8N_DIR}/.env" - else - printf 'N8N_VERSION=%s\n' "$target" >>"${N8N_DIR}/.env" - fi + set_env_value N8N_VERSION "$target" ok "n8n version: ${current:-unset} -> ${target}" + # Installs from before N8N_SANDBOX_VERSION carry the tag in compose.yml. + current_sandbox="$(sed -n 's/^N8N_SANDBOX_VERSION=//p' "${N8N_DIR}/.env")" + [ -n "$current_sandbox" ] || + current_sandbox="$(sed -n 's|.*n8n-sandbox-service-api:\([0-9A-Za-z._-]*\).*|\1|p' "${N8N_DIR}/compose.yml" | head -n1)" + set_env_value N8N_SANDBOX_VERSION "$SANDBOX_VERSION" + ok "sandbox service version: ${current_sandbox:-unset} -> ${SANDBOX_VERSION}" + migrate_compose_sandbox_pins check_compose_freshness if [ "$NO_START" -eq 1 ]; then @@ -421,7 +466,7 @@ do_upgrade() { compose_pull STEP="container-start" compose_checked up -d --quiet-pull - ok "Restarted with n8n ${target}" + ok "Restarted with n8n ${target} and sandbox service ${SANDBOX_VERSION}" } wait_for_n8n() { diff --git a/docker/test-get-n8n.sh b/docker/test-get-n8n.sh index b3a7c50ab9c..f83447ae099 100644 --- a/docker/test-get-n8n.sh +++ b/docker/test-get-n8n.sh @@ -50,6 +50,24 @@ check_not() { env_value() { sed -n "s/^$2=//p" "$1/.env"; } +# The sandbox service pin the script writes to .env and expects in compose.yml. +SANDBOX_PIN="$(sed -n 's/^SANDBOX_VERSION="\(.*\)"$/\1/p' "$SCRIPT")" + +# Recreates what stack definition v1 wrote: fixed sandbox image tags and an +# http runner URL. Installs made from it need the --upgrade migration. +# shellcheck disable=SC2016 # matches the literal ${N8N_SANDBOX_VERSION} +legacy_compose() { # legacy_compose (stdout) + sed -e 's|n8n-sandbox-service-api:\${N8N_SANDBOX_VERSION}|n8n-sandbox-service-api:1.2.0|' \ + -e 's|n8n-sandbox-service-runner-dind:\${N8N_SANDBOX_VERSION}|n8n-sandbox-service-runner-dind:1.2.0|' \ + -e 's|n8n-sandbox-service-sandbox:\${N8N_SANDBOX_VERSION}|n8n-sandbox-service-sandbox:latest|' \ + -e 's|SANDBOX_RUNNER_HTTP_BASE_URL: https://|SANDBOX_RUNNER_HTTP_BASE_URL: http://|' \ + -e 's|^# compose-version: .*|# compose-version: 1|' "$1" +} +# A v1 install also has no N8N_SANDBOX_VERSION line in .env. +strip_sandbox_pin() { # strip_sandbox_pin + grep -v '^N8N_SANDBOX_VERSION=' "$1/.env" >"$1/.env.v1" && mv "$1/.env.v1" "$1/.env" +} + teardown() { [ -f "$1/compose.yml" ] && docker compose -f "$1/compose.yml" down -v --remove-orphans >/dev/null 2>&1 return 0 @@ -83,6 +101,16 @@ case "$(env_value "$WORK/a" N8N_VERSION)" in *) fail "default install resolves a sane n8n version" ;; esac +# sandbox images follow one pin in .env; compose.yml carries no tags of its own +[ -n "$SANDBOX_PIN" ] && [ "$(env_value "$WORK/a" N8N_SANDBOX_VERSION)" = "$SANDBOX_PIN" ] && + pass "install pins N8N_SANDBOX_VERSION to the script's pin" || fail "install pins N8N_SANDBOX_VERSION to the script's pin" +check_not "compose.yml hard-codes no sandbox image tag" grep -E 'n8n-sandbox-service-[a-z-]+:[0-9a-z]' "$WORK/a/compose.yml" +resolved="$(docker compose -f "$WORK/a/compose.yml" config 2>/dev/null)" +for img in api runner-dind sandbox; do + echo "$resolved" | grep -q "n8n-sandbox-service-${img}:${SANDBOX_PIN}\$" && + pass "compose resolves the ${img} image to the sandbox pin" || fail "compose resolves the ${img} image to the sandbox pin" +done + api_key="$(env_value "$WORK/a" SANDBOX_API_KEYS)" runner_key="$(env_value "$WORK/a" SANDBOX_RUNNER_API_KEYS)" [ "${#api_key}" -ge 32 ] && pass "sandbox API key generated" || fail "sandbox API key generated" @@ -145,6 +173,46 @@ stage_out="$(env N8N_DIR="$WORK/stage" sh "$SCRIPT" --upgrade --version 2.32.0 - echo "$stage_out" | grep -q "Not restarting" && pass "--upgrade --no-start skips the restart" || fail "--upgrade --no-start skips the restart" +# --upgrade migrates a v1 install: the pin moves into .env, compose.yml loses +# its hard-coded tags and otherwise stays as it was. +legacy_compose "$COMPOSE_SRC" >"$WORK/legacy-compose.yml" +env N8N_DIR="$WORK/legacy" N8N_COMPOSE_URL="$WORK/legacy-compose.yml" sh "$SCRIPT" --version 2.31.4 --no-start >/dev/null 2>&1 +strip_sandbox_pin "$WORK/legacy" +check "v1 install fixture has hard-coded sandbox tags" grep -q 'n8n-sandbox-service-api:1.2.0' "$WORK/legacy/compose.yml" +legacy_out="$(env N8N_DIR="$WORK/legacy" sh "$SCRIPT" --upgrade --version 2.32.0 --no-start 2>&1)" && + pass "--upgrade --no-start on a v1 install succeeds" || fail "--upgrade --no-start on a v1 install succeeds" +echo "$legacy_out" | grep -q 'sandbox service images pinned to fixed tags' && + pass "--upgrade tells the user the fixed sandbox tags were replaced" || + fail "--upgrade tells the user the fixed sandbox tags were replaced" +echo "$legacy_out" | grep -q 'runner URL now uses https' && + pass "--upgrade tells the user the runner URL changed" || fail "--upgrade tells the user the runner URL changed" +[ "$(env_value "$WORK/legacy" N8N_SANDBOX_VERSION)" = "$SANDBOX_PIN" ] && + pass "--upgrade adds N8N_SANDBOX_VERSION to a v1 .env" || fail "--upgrade adds N8N_SANDBOX_VERSION to a v1 .env" +check_not "--upgrade removes hard-coded sandbox tags" grep -E 'n8n-sandbox-service-[a-z-]+:[0-9a-z]' "$WORK/legacy/compose.yml" +grep -q 'SANDBOX_RUNNER_HTTP_BASE_URL: https://sandbox-runner-1:8080' "$WORK/legacy/compose.yml" && + pass "--upgrade switches the runner URL to https" || fail "--upgrade switches the runner URL to https" +grep -v '^#' "$WORK/legacy/compose.yml" >"$WORK/migrated.stripped" +grep -v '^#' "$COMPOSE_SRC" >"$WORK/current.stripped" +cmp -s "$WORK/migrated.stripped" "$WORK/current.stripped" && + pass "migrated v1 compose.yml matches the current stack definition" || + fail "migrated v1 compose.yml matches the current stack definition" +check "migrated compose.yml validates" docker compose -f "$WORK/legacy/compose.yml" config -q +cp "$WORK/legacy/compose.yml" "$WORK/legacy-compose-migrated" +second_out="$(env N8N_DIR="$WORK/legacy" sh "$SCRIPT" --upgrade --version 2.32.0 --no-start 2>&1)" && + pass "second --upgrade succeeds" || fail "second --upgrade succeeds" +cmp -s "$WORK/legacy-compose-migrated" "$WORK/legacy/compose.yml" && + pass "second --upgrade leaves a migrated compose.yml untouched" || fail "second --upgrade leaves a migrated compose.yml untouched" +echo "$second_out" | grep -q 'Updated compose.yml' && fail "second --upgrade prints no compose.yml notice" || + pass "second --upgrade prints no compose.yml notice" + +# images the user pointed elsewhere are not the script's to move +# shellcheck disable=SC2016 +sed 's|ghcr.io/n8n-io/n8n-sandbox-service-api:${N8N_SANDBOX_VERSION}|docker.io/n8nio/n8n-sandbox-service-api:1.3.0|' \ + "$COMPOSE_SRC" >"$WORK/custom-compose.yml" +env N8N_DIR="$WORK/custom" N8N_COMPOSE_URL="$WORK/custom-compose.yml" sh "$SCRIPT" --version 2.31.4 --no-start >/dev/null 2>&1 +check "--upgrade with user-chosen images succeeds" env N8N_DIR="$WORK/custom" sh "$SCRIPT" --upgrade --version 2.32.0 --no-start +check "--upgrade leaves user-chosen sandbox images alone" grep -q 'docker.io/n8nio/n8n-sandbox-service-api:1.3.0' "$WORK/custom/compose.yml" + # refuses non-empty foreign directory mkdir -p "$WORK/dirty" && touch "$WORK/dirty/keep" check_not "refuses non-empty directory" env N8N_DIR="$WORK/dirty" sh "$SCRIPT" @@ -236,8 +304,10 @@ if [ "$E2E" -eq 1 ]; then E2E_DIR="$WORK/e2e" trap 'teardown "$E2E_DIR"; rm -rf "$WORK"' EXIT INT TERM - # install the previous release so --upgrade below is a real version change - if env N8N_DIR="$E2E_DIR" sh "$SCRIPT" --version 2.31.4; then + # install the previous release from stack definition v1, so --upgrade below + # is a real n8n version change and a real sandbox migration (1.2.0 + http) + legacy_compose "$COMPOSE_SRC" >"$WORK/e2e-legacy-compose.yml" + if env N8N_DIR="$E2E_DIR" N8N_COMPOSE_URL="$WORK/e2e-legacy-compose.yml" sh "$SCRIPT" --version 2.31.4; then pass "fresh install boots and reaches /healthz" else fail "fresh install boots and reaches /healthz" @@ -245,6 +315,7 @@ if [ "$E2E" -eq 1 ]; then docker compose -f "$E2E_DIR/compose.yml" logs --tail 30 || true fi + strip_sandbox_pin "$E2E_DIR" ps_out="$(docker compose -f "$E2E_DIR/compose.yml" ps -a --format '{{.Service}} {{.State}} {{.ExitCode}}')" echo "$ps_out" | grep -q '^sandbox-certs exited 0' && pass "sandbox-certs completed" || fail "sandbox-certs completed" echo "$ps_out" | grep -q '^sandbox-api running' && pass "sandbox-api running" || fail "sandbox-api running" @@ -264,23 +335,33 @@ if [ "$E2E" -eq 1 ]; then check_not "fresh install fails while port is taken" env N8N_DIR="$WORK/conflict" sh "$SCRIPT" - # upgrade: only the N8N_VERSION line may change, and the new image must run. + # upgrade: only the version lines may change, and the new images must run. # Pin the target explicitly so the assertion is deterministic (a bare # --upgrade resolves the latest stable release at run time). target="$(sed -n 's/^FALLBACK_N8N_VERSION="\(.*\)"$/\1/p' "$SCRIPT")" cp "$E2E_DIR/.env" "$WORK/env-before" check "--upgrade succeeds" env N8N_DIR="$E2E_DIR" sh "$SCRIPT" --upgrade --version "$target" diff "$WORK/env-before" "$E2E_DIR/.env" >"$WORK/env.diff" 2>&1 || true - if [ "$(grep -c '^[<>]' "$WORK/env.diff")" = "2" ] && + if [ "$(grep -c '^[<>]' "$WORK/env.diff")" = "3" ] && grep -q '^< N8N_VERSION=2.31.4$' "$WORK/env.diff" && - grep -q "^> N8N_VERSION=${target}\$" "$WORK/env.diff"; then - pass "--upgrade changes only N8N_VERSION" + grep -q "^> N8N_VERSION=${target}\$" "$WORK/env.diff" && + grep -q "^> N8N_SANDBOX_VERSION=${SANDBOX_PIN}\$" "$WORK/env.diff"; then + pass "--upgrade changes only the version lines in .env" else - fail "--upgrade changes only N8N_VERSION" + fail "--upgrade changes only the version lines in .env" cat "$WORK/env.diff" >&2 fi docker compose -f "$E2E_DIR/compose.yml" ps n8n --format '{{.Image}}' | grep -q ":${target}\$" && pass "n8n container runs upgraded image" || fail "n8n container runs upgraded image" + for svc in sandbox-api sandbox-runner-1; do + docker compose -f "$E2E_DIR/compose.yml" ps "$svc" --format '{{.Image}}' | grep -q ":${SANDBOX_PIN}\$" && + pass "${svc} runs the pinned sandbox image after upgrade" || fail "${svc} runs the pinned sandbox image after upgrade" + done + docker compose -f "$E2E_DIR/compose.yml" logs sandbox-runner-1 2>/dev/null | + grep -q 'registration stream established' && + pass "runner re-registered after upgrade" || fail "runner re-registered after upgrade" + check "n8n reaches sandbox-api after upgrade" \ + docker compose -f "$E2E_DIR/compose.yml" exec -T n8n wget -qO- http://sandbox-api:8080/healthz else trap 'rm -rf "$WORK"' EXIT INT TERM echo "(e2e skipped — pass --e2e to boot the full stack)"