Upgrade Octopus Tentacle Docker image from Debian 11 (Bullseye) to Debian 12 (Bookworm)#1218
Upgrade Octopus Tentacle Docker image from Debian 11 (Bullseye) to Debian 12 (Bookworm)#1218todthomson wants to merge 21 commits into
Conversation
b4e4116 to
7f2e79a
Compare
There was a problem hiding this comment.
Pull request overview
Upgrades the Linux Tentacle Docker image base to Debian 12 (bookworm) and adds a new end-to-end smoke-test script intended to validate image + worker registration + task execution against a local Octopus Server.
Changes:
- Bump
docker/linux/Dockerfilebase image todebian:12-slimand update runtime library dependencies (e.g.,libicu72,libssl3). - Add
scripts/smoke-test-linux-tentacle.shto build the image from a local.deb, start a local server (sibling repo), register a worker, and run an AdHocScript.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
docker/linux/Dockerfile |
Switch base to Debian 12 and adjust installed runtime dependencies / package install commands. |
scripts/smoke-test-linux-tentacle.sh |
New end-to-end smoke test script for local validation of the Debian 12 Tentacle image. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@claude review once |
|
Claude finished @todthomson's task in 2m 24s —— View job Code ReviewRead A few things worth a second look before merge — none blocking.
|
| # Sanity: the printed log should mention Debian 12. | ||
| if ! curl -fsS -H "$H" "$API/tasks/$TASK_ID/raw" | grep -q 'Debian GNU/Linux 12'; then | ||
| warn "Task succeeded but the log does NOT mention 'Debian GNU/Linux 12'. Inspect output above." | ||
| fi |
There was a problem hiding this comment.
🔴 The Debian 12 sanity check on line 252 calls warn rather than die, so when the AdHocScript log fails to mention Debian GNU/Linux 12 the script still falls through to the unconditional PASS log and exits 0. Since this is the only assertion that the script actually ran against the new Debian 12 image — the whole point of the EFT-3311 smoke test — a stale Debian 11 worker (or any other non-Debian-12 target the worker selection happens to pick up) would produce a false-green result. Change warn to die so a missing Debian GNU/Linux 12 string hard-fails.
Extended reasoning...
What the bug is
The smoke test's pass criteria has two parts: (a) the AdHocScript reaches State == Success (enforced at line 247 with die), and (b) the script's stdout mentions Debian GNU/Linux 12 (checked at lines 251-253). Only the first is actually enforced. The OS-release check on line 252 calls warn, which only prints to stderr — control falls through to line 255's unconditional log "PASS — Tentacle (Debian 12) registered and executed hello-world." and the script exits 0.
Why this matters for this PR specifically
This PR's sole purpose is to upgrade the Linux Tentacle base image from debian:11-slim to debian:12-slim. The script's header says it is the "End-to-end smoke test for the Linux Tentacle Docker image (EFT-3311)" and the PR description positions it as the intended starting point for a CI gate on that upgrade. The only artifact in the entire script that proves the AdHocScript ran on a Debian 12 container — as opposed to, say, a Debian 11 Tentacle from a previous build that happens to still be alive locally — is the cat /etc/os-release output checked at line 252. Demoting that check to a warning means the gate the script is supposed to provide doesn't actually exist.
The specific code path that triggers a false PASS
Step-by-step:
- A previous smoke-test run (on Debian 11, before this PR) registered
Workers-Nagainst the local Octopus Server and the container was neverdocker rm'd, so the worker registration is still present in the server's database. - The current run brings up its own Tentacle container, but if registration is slow or the wrapper exits before re-registering, the new worker may not yet exist when Step 5 runs the
/api/workersquery. - Worker selection (lines 195-204) picks the worker with the highest
Workers-Nordinal across the entire server — i.e. the most recently registered worker globally, not the one this run created. If the stale Debian 11 worker has the highest ordinal at query time,WORKER_IDresolves to it. - The AdHocScript (a one-line
echo … ; cat /etc/os-release) is submitted withWorkerIds: [$id]. Bash and/etc/os-releaseexist on Debian 11 Tentacles, so the task reachesState == Success. - Line 251 fetches the task log and greps for
Debian GNU/Linux 12. On a Debian 11 worker, this grep fails. - Because the check uses
warninstead ofdie, the script continues and reaches line 255:PASS — Tentacle (Debian 12) registered and executed hello-world.Exit code 0.
Note that the stale-worker scenario is the cleanest trigger, but it is not the only one — anything that causes the worker selection or the docker build to point at the wrong image (cached smoke-debian12 tag, manual override, registration race) produces the same false-green. The point of an assertion is to catch all such scenarios, not just the ones we can enumerate.
Why existing code doesn't prevent it
set -euo pipefail ensures that unhandled failures abort the script, but warn deliberately swallows the failure (its body is printf … >&2 with no exit). die is the only helper in this script that exits non-zero. The verifier confirmations all note the same thing — the function definitions at lines 39-41 make the distinction explicit: warn returns, die exits 1.
Impact
The smoke test's stated value is gating the Debian 12 upgrade. If the assertion that actually proves Debian 12 was exercised is non-fatal, then a CI invocation of this script can return green while having validated nothing about the new base image. That defeats the gate.
How to fix
One-character change on line 252:
- warn "Task succeeded but the log does NOT mention 'Debian GNU/Linux 12'. Inspect output above."
+ die "Task succeeded but the log does NOT mention 'Debian GNU/Linux 12'. Inspect output above."The error message is already worded as a failure condition, so only the helper call needs to change. This brings the assertion in line with the State != Success check immediately above it (line 246-248), which already uses die.
| for i in {1..120}; do | ||
| STATE="$(curl -fsS -H "$H" "$API/tasks/$TASK_ID" | jq -r '.State')" | ||
| echo " task=$TASK_ID state=$STATE" | ||
| case "$STATE" in | ||
| Success|Failed|Canceled|TimedOut) break ;; | ||
| esac | ||
| sleep 2 | ||
| done |
There was a problem hiding this comment.
🟡 The task polling loop at line 234 (STATE="$(curl -fsS … | jq -r '.State')") runs under set -euo pipefail, so a single transient curl failure inside the 120-iteration loop will exit the script mid-poll without printing the structured task=X state=Y diagnostic for the failing iteration. Consider mirroring the defensive pattern already used in the worker-discovery loop at line 198 (e.g. … || echo Unknown) so polling is resilient to brief hiccups and produces consistent per-iteration output. Nit — smoke-test tooling against a local server, and curl -fsS will still emit its own stderr message, so this isn't a correctness issue.
Extended reasoning...
What
In scripts/smoke-test-linux-tentacle.sh, the task-state polling loop at lines 233–240:
for i in {1..120}; do
STATE="$(curl -fsS -H "$H" "$API/tasks/$TASK_ID" | jq -r '.State')"
echo " task=$TASK_ID state=$STATE"
case "$STATE" in
Success|Failed|Canceled|TimedOut) break ;;
esac
sleep 2
doneruns under set -euo pipefail (line 13). The command substitution wraps a curl-piped-to-jq pipeline. If curl exits non-zero (HTTP 4xx/5xx with -f, transient connection error, container restart, brief 5xx during task execution), pipefail propagates the non-zero status through the pipeline, the command substitution carries it to the assignment, and set -e exits the script immediately — before the echo " task=… state=…" line for that iteration runs.
Step-by-step proof
- Iteration 47 begins; curl is invoked against
$API/tasks/$TASK_ID. - Octopus Server briefly returns HTTP 502 (or the TCP connection is reset, or the container is mid-restart).
curl -fsSexits with non-zero (e.g. 22 for HTTP errors, 7 for connection refused).jqruns on empty stdin and exits 0 (or non-zero), butpipefailensures the pipeline's exit status is curl's non-zero code.- The command substitution propagates that status to the
STATE=…assignment. set -efires; the script exits.- The EXIT trap runs
teardown()— which prints "--- teardown ---" and tears down compose. - The user sees the curl stderr line (e.g.
curl: (22) The requested URL returned error: 502) and the teardown trace, but notask=$TASK_ID state=…line for iteration 47, and no indication that the script was specifically in the task-polling loop when it died. They have to scroll up and reason about where in the script the abort happened.
Why existing code doesn't prevent it
The worker-discovery polling loop a few lines above (line 198) handles exactly this case defensively:
WORKERS_JSON="$(curl -fsS -H "$H" "$API/workers?take=1000" 2>/dev/null || echo '{"Items":[]}')"The || echo '{"Items":[]}' absorbs transient curl failures and feeds valid JSON to jq, so the loop keeps polling. The task-polling loop has no such guard.
The same pattern (curl inside a pipeline whose failure can abort) also appears at line 251 (if ! curl … | grep -q …). Strictly that one won't abort the script — set -e is suppressed inside an if condition — but a curl failure there gets mis-attributed as "log doesn't mention Debian 12" via the warn, which is a separate UX paper-cut.
Addressing the counter-argument
A reasonable counter-argument is that curl -fsS uses the capital -S flag (show errors), so the user does see a curl stderr line like curl: (7) Failed to connect to … — i.e. the failure isn't truly silent. That's correct, and it's the main reason this is a nit, not a normal bug. The remaining gap is diagnostic quality: an operator running the smoke test sees a curl error plus an abrupt teardown trace, but loses the structured task=$TASK_ID state=… line that the loop is designed to emit each iteration, and there's no die message tying the failure back to "task polling" specifically. For a 2-minute polling loop against a server that may legitimately be doing other work, one transient 5xx terminating the whole smoke run is also a slightly steep response.
It's also reasonable to argue the two polling loops have different semantics (worker may legitimately not yet exist; task definitely does), so fail-fast on line 234 is intentional. That's a defensible design choice — but if a hiccup is fatal, it'd be nicer to die "Task polling failed at iteration $i: curl returned $?" than to let set -e exit silently from a command substitution.
Suggested fix
One-line change matching line 198's pattern:
STATE="$(curl -fsS -H "$H" "$API/tasks/$TASK_ID" 2>/dev/null | jq -r '.State' 2>/dev/null || echo Unknown)"This lets the loop continue polling on transient failures, the echo " task=… state=Unknown" line still prints (so the operator can see which iteration hiccuped), and a persistent failure still falls out of the 120-iteration window with a clean "state never reached Success" die.
Severity rationale
Nit. Smoke-test tooling, not production code; the server is local so transient hiccups are uncommon; curl -fsS still emits stderr; and exit code is preserved so test integrity is intact. Purely a defensive-resilience and diagnostic-quality improvement, worth doing because the fix is one line and mirrors a pattern already in the same file.
| ENV_BACKUP="$(mktemp "${TMPDIR:-/tmp}/octopus-server-env-smoke-tentacle-XXXXXX")" | ||
| cp "$ENV_FILE" "$ENV_BACKUP" | ||
| log "Backed up .env to $ENV_BACKUP (will be restored on exit)" |
There was a problem hiding this comment.
🟡 The .env backup pattern at scripts/smoke-test-linux-tentacle.sh:108-109 has a small race window: mktemp creates an empty file and assigns ENV_BACKUP before cp populates it. If the script is interrupted (Ctrl+C, SIGTERM) between those two statements, or if cp itself fails under set -e, the EXIT trap fires with ENV_BACKUP non-empty and the empty file present — its guard [[ -n "$ENV_BACKUP" && -f "$ENV_BACKUP" ]] passes and mv "$ENV_BACKUP" "$ENV_FILE" clobbers the real .env with an empty file. This is the same data-loss scenario commit 439be89 was meant to prevent. Trivial fix: only assign ENV_BACKUP after cp succeeds (copy to a local temp path first, then assign).
Extended reasoning...
The race window
Lines 108-109 of scripts/smoke-test-linux-tentacle.sh:
ENV_BACKUP="$(mktemp "${TMPDIR:-/tmp}/octopus-server-env-smoke-tentacle-XXXXXX")"
cp "$ENV_FILE" "$ENV_BACKUP"mktemp does two things in one shot: it creates an empty file on disk and prints its path. After bash finishes the command substitution, ENV_BACKUP is set and the file exists — but it is still empty. The cp that actually populates it is a separate statement.
The EXIT trap installed at line 60 runs teardown on any exit path, including signals. Its guard at lines 54-55 is:
if [[ -n "$ENV_BACKUP" && -f "$ENV_BACKUP" ]]; then
mv "$ENV_BACKUP" "$ENV_FILE"That guard cannot distinguish "backup we already wrote" from "empty file mktemp just made".
Step-by-step proof
- Bash evaluates line 108. The subshell runs
mktemp, which creates/tmp/octopus-server-env-smoke-tentacle-abc123(empty, 0 bytes) and prints the path. - Bash assigns that path to
ENV_BACKUP. Variable is now non-empty, file exists, file is empty. - Trigger A — signal: User hits Ctrl-C before bash starts line 109.
set -eplus signal propagation invokes the EXIT trap. - Trigger B — cp failure under
set -e: Disk full, source unreadable mid-read, or any othercperror causesset -eto exit. EXIT trap fires. teardownruns. Guard passes (path non-empty, file exists).mv "$ENV_BACKUP" "$ENV_FILE"moves the empty file over the user's real.env. Data destroyed.
Why existing code doesn't prevent this
ENV_BACKUP is initialized empty at line 21 specifically so the trap can tell "backup not yet made" from "backup made". That intent is correct but the protection is defeated by mktemp because the assignment and the population happen in two statements. The guard checks the wrong thing — file existence rather than file validity.
This is exactly the failure mode commit 439be89 (and the resolved Copilot comment 3223737974) set out to eliminate. The mktemp pattern protected against stale backups from previous runs, but it introduced a smaller race against the current run's own interrupt or cp failure.
Impact
The target file .env contains the Ultimate license fetched from 1Password. Loss is recoverable (op read again), so this is not catastrophic, and the timing window for a pure signal race is microseconds. The more realistic trigger is cp failure under set -e — narrower-looking on paper but achievable (e.g. ENOSPC on $TMPDIR).
Suggested fix
Assign ENV_BACKUP only after cp succeeds, so the trap's "is the path set?" check is meaningful:
_tmp="$(mktemp "${TMPDIR:-/tmp}/octopus-server-env-smoke-tentacle-XXXXXX")"
cp "$ENV_FILE" "$_tmp"
ENV_BACKUP="$_tmp"
log "Backed up .env to $ENV_BACKUP (will be restored on exit)"Alternatively, gate the restore in teardown on a BACKUP_READY=1 flag set after cp, or compare sizes ([[ -s "$ENV_BACKUP" ]]) before restoring.
Severity
Nit — developer-only smoke test, window is small, license is recoverable from 1Password. But the bug is real and undermines the safety guarantee the change it lives in was meant to add, so it's worth a line.
🔬 also observed by copilot-pull-request-reviewer
|
Smoke-test feedback #1 — tag worker with per-run name (commit e707cdc) Before — relied on the "highest WORKER_ID="$(echo "$WORKERS_JSON" \
| jq -r '[.Items[] | select(.Id | startswith("Workers-"))] | sort_by(.Id | ltrimstr("Workers-") | tonumber) | last | .Id // empty')"After — generate a unique per-run name and pass it to the Tentacle as WORKER_TARGET_NAME="smoke-tentacle-$(date +%Y%m%d-%H%M%S)-$$"
# ... in the override compose:
# environment:
# TargetName: "$WORKER_TARGET_NAME"
WORKERS_JSON="$(curl -fsS -H "$H" --data-urlencode "name=$WORKER_TARGET_NAME" -G "$API/workers" ...)"
WORKER_ID="$(echo "$WORKERS_JSON" | jq -r --arg name "$WORKER_TARGET_NAME" '.Items[] | select(.Name == $name) | .Id' | head -n1)"Robust against reused DB volumes and any future |
|
Smoke-test feedback #2 — deregister worker in teardown (commit 4730f64) Before — teardown stopped containers but left the worker record in the Server's DB, so the workers list grew monotonically across runs sharing a Server DB volume: teardown() {
local exit_code=$?
log "--- teardown ---"
if [[ -n "$OVERRIDE_COMPOSE" && -f "$OVERRIDE_COMPOSE" ]]; then
(cd "$SERVER_REPO" && docker compose -f docker-compose.yml -f "$OVERRIDE_COMPOSE" --profile tentacle down 2>/dev/null) || true
rm -f "$OVERRIDE_COMPOSE"
fi
(cd "$SERVER_REPO" && docker compose down 2>/dev/null) || true
...
}After — best-effort teardown() {
local exit_code=$?
log "--- teardown ---"
if [[ -n "$WORKER_ID" ]]; then
log "Deregistering worker $WORKER_ID"
curl -fsS -X DELETE -H "$H" "$API/workers/$WORKER_ID" >/dev/null 2>&1 || true
fi
...
}
|
|
Smoke-test feedback #3 — Before — the period in the search string is a regex wildcard under plain if "${COMPOSE[@]}" logs --no-color tentacle 2>/dev/null | grep -q "Configuration successful."; thenAfter — if "${COMPOSE[@]}" logs --no-color tentacle 2>/dev/null | grep -qF "Configuration successful."; then |
|
Smoke-test feedback #4 — document Before — API="http://localhost:8065/api"
API_KEY="API-APIKEY01"
H="X-Octopus-ApiKey: $API_KEY"After — header comment explains that this is the well-known dev sentinel provisioned by the sibling repo's compose stack, not a real secret: # Note on $API_KEY below: "API-APIKEY01" is the well-known dev sentinel API key
# provisioned by the sibling OctopusDeploy repo's docker-compose stack for its
# local-only Server instance. It is not a real secret and is safe to commit. |
|
Smoke-test feedback #5 — Before — hard dependency on require op
...
if ! op account list >/dev/null 2>&1; then
die "1Password CLI is not signed in. Run: eval \$(op signin)"
fi
LICENSE_BASE64="$(op read "$ONEPASSWORD_LICENSE_REF" 2>/dev/null || true)"After — CI can inject the license via [[ -n "${OCTOPUS_LICENSE_BASE64:-}" ]] || require op
...
if [[ -n "${OCTOPUS_LICENSE_BASE64:-}" ]]; then
LICENSE_BASE64="$OCTOPUS_LICENSE_BASE64"
log "Using license from \$OCTOPUS_LICENSE_BASE64 (${#LICENSE_BASE64} bytes)"
else
if ! op account list >/dev/null 2>&1; then
die "1Password CLI is not signed in. Run: eval \$(op signin) — or pre-set \$OCTOPUS_LICENSE_BASE64."
fi
LICENSE_BASE64="$(op read "$ONEPASSWORD_LICENSE_REF" 2>/dev/null || true)"
...
fiThe header comment also documents the new env var, so the CI integration path is discoverable. |
|
Smoke-test feedback #6 — hard-fail the Debian 12 assertion (commit b913c00) Before — the load-bearing assertion of the smoke test was only a warning, so the script would still # Sanity: the printed log should mention Debian 12.
if ! curl -fsS -H "$H" "$API/tasks/$TASK_ID/raw" | grep -q 'Debian GNU/Linux 12'; then
warn "Task succeeded but the log does NOT mention 'Debian GNU/Linux 12'. Inspect output above."
fiAfter — promoted to # Load-bearing assertion: the whole point of this smoke test is to prove the
# Debian 12 base image is what's actually running on the Tentacle, so a missing
# os-release line is a hard failure, not a warning.
if ! curl -fsS -H "$H" "$API/tasks/$TASK_ID/raw" | grep -qF 'Debian GNU/Linux 12'; then
die "Task succeeded but the log does NOT mention 'Debian GNU/Linux 12'. Inspect output above."
fi |
|
Smoke-test feedback #7 — Before — sibling upsert_env_var() {
local key="$1" value="$2"
local tmp="$ENV_FILE.tmp" line found=
: > "$tmp"
...
}After — unique per-call tmp via upsert_env_var() {
local key="$1" value="$2"
local tmp line found=
tmp="$(mktemp "${TMPDIR:-/tmp}/octopus-server-env-smoke-upsert-XXXXXX")"
...
} |
|
Local smoke test: ✅ PASS Re-ran Confirms in one run that the new behaviours are wired up:
|
…age configuration, since apt-utils is not installed
Builds the image from the local .deb, brings up a local Octopus Server, registers the Tentacle as a worker, and runs a hello-world AdHocScript via the REST API to verify the Debian 12 upgrade end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tter Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Replace upsert_env_var's Python heredoc with a pure-bash implementation. The previous version invoked python3 without listing it in the script's required-tools check, so the failure mode on a machine without Python was a confusing "python3: command not found" mid-run instead of an upfront "Missing required tool" message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous version wrote and deleted a fixed-name `docker-compose.smoke.yml` in the sibling OctopusDeploy repo. If a user already had a file by that name (now or in the future), this script would silently clobber it and remove it on teardown. Switch to `mktemp` in $TMPDIR so the path is unique-per-run, and only delete it in teardown if this run actually created one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fixed `.env.smoke-test.bak` path would silently overwrite a stale backup from a previously-crashed run, losing the original .env. Switch to `mktemp` in $TMPDIR so the backup path is unique-per-run, log the path so it's discoverable from script output if anything goes wrong, and guard the teardown restore on the variable being set so an early failure (before the backup is made) doesn't try to mv an empty path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BSD mktemp (macOS) silently leaves Xs unsubstituted when they aren't the
final characters of the template — so the previous templates
("...XXXXXX.bak" and "...XXXXXX.yml") actually produced fixed-name files
with literal Xs, defeating the unique-per-run guarantee. Verified by
inspecting the logged backup path.
Move the Xs to the end and drop the .bak/.yml suffixes (docker compose
parses by content not extension, and the suffix was cosmetic for the
backup). Verified end-to-end against a local Octopus Server.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The period in the search string is a regex wildcard with plain grep -q; -F (--fixed-strings) makes the match literal, which is what's intended. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a sentence to the header comment explaining that API-APIKEY01 is the well-known dev key provisioned by the sibling OctopusDeploy repo's compose stack for its local Server, not a real secret. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is the load-bearing assertion of the smoke test — if the Tentacle isn't actually running on Debian 12, the test should fail, not warn. Also switches the grep to -qF so the period is treated literally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Avoids writing a sibling \$ENV_FILE.tmp that two concurrent runs could race on. Uses the same XXXXXX template convention as the other mktemp calls in this script. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets CI runners (which can't use op) inject the license via env var, while keeping op read as the local-dev default. The op tool is only required when the env var is unset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generates a unique per-run worker name (smoke-tentacle-<timestamp>-<pid>) and passes it to the Tentacle as TargetName via the override compose. The Server-side lookup then filters by Name == $WORKER_TARGET_NAME, which is robust against reused DB volumes growing the workers list and against any future Workers-N renumbering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the Server's DB volume is reused across runs (CI especially), the workers list otherwise grows monotonically. Teardown now issues a best-effort DELETE /api/workers/\$WORKER_ID before bringing the stack down, guarded on \$WORKER_ID being populated (so an early failure before Step 5 is a no-op). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4730f64 to
dca6d6a
Compare
|
Local smoke test (re-run): ✅ PASS Second clean run of Confirms (again) that the AdHocScript ran on |
Background
The Linux Tentacle Docker image was on an older Debian base. This PR upgrades
docker/linux/Dockerfiletodebian:12-slim(bookworm), updates the runtime dependency versions to what Debian 12 ships (libicu72,libssl3,libgssapi-krb5-2), and trims explicit installs of packages already present in the base image.Results
The Linux Tentacle container now runs on Debian 12 (bookworm) with up-to-date runtime libraries.
Testing
Built and exercised the image end-to-end on an Apple Silicon host (Docker Desktop,
--platform linux/amd64)../build.sh PackDebianPackage --RuntimeIds linux-x64.debbuiltdocker build -f docker/linux/DockerfileDebian GNU/Linux 12 (bookworm)confirmed via/etc/os-releasetentacle --versiontentacle helplibssl,libicu,libgssapi-krb5, libstdc++, libc)ldd /opt/octopus/tentacle/Tentaclecurl,jq,sudo,xxd,dos2unix,dockerd,docker,tentacle)PATHtentacledpkg install statusii)/usr/bin/tentacle → /opt/octopus/tentacle/Tentacleconfigure-and-run.shEULA gate (noACCEPT_EULA)configure-and-run.shcredentials validation (no API key/creds)configure-and-run.shServerUrlvalidationscripts/smoke-test-linux-tentacle.shWorkers-1registered against a local Octopus Server (siblingOctopusDeployrepo), AdHocScript task reachedSuccess, task log emittedHello from <container-id>andPRETTY_NAME="Debian GNU/Linux 12 (bookworm)"The smoke-test script (
scripts/smoke-test-linux-tentacle.sh) is committed alongside this change and is intended as the starting point for a CI gate. It builds the image from the local.deb, pulls the Ultimate license from 1Password, brings up Octopus Server via the sibling repo'sdocker compose, registers the Tentacle as a worker (polling mode, DIND disabled), submits a hello-worldAdHocScriptvia the REST API, assertsstate=Success, and restores.envon exit. Recommend wiring it into CI before merge.How to review this PR
Quality ✔️
Pre-requisites