mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 19:20:07 +02:00
* fix(selfhost): read the published host port back from Compose (MUL-5505)
`make selfhost` polled the wrong port when the backend host port was passed
through the environment. Make and Compose disagree on variable precedence: an
assignment in an included makefile (the Makefile `include`s .env) overrides a
real environment variable, while for Compose the environment overrides .env.
Because .env.example ships BACKEND_PORT uncommented, .env always pinned the
recipe's view of the port, so `BACKEND_PORT=9000 make selfhost` published the
stack on 9000 while the health check hammered 8080 for 60s and then reported
"Services are still starting" on a perfectly healthy install. The success
message printed the same wrong backend URL. FRONTEND_PORT had the same
inversion, affecting the printed frontend URL.
Stop re-deriving the host port in the recipe and ask Compose, which is the only
authority on what it published. The wait-and-report block (three port
references per target, duplicated across selfhost and selfhost-build) moves into
scripts/selfhost-wait.sh, which resolves both host ports via `docker compose
port` and falls back to the compose default chain when the stack is not up.
Also fix the input contract: `PORT` was the only port variable documented in
SELF_HOSTING_ADVANCED.md, yet compose read only BACKEND_PORT, so setting the
documented variable was silently ignored. The backend host port mapping now
falls back to `${PORT:-8080}`, matching Makefile and scripts/local-env.sh, and
the docs describe both variables as host ports.
Container-internal ports (8080/3000), the global PORT derivation used by local
dev, and the 127.0.0.1 bindings are unchanged; the default self-host experience
is byte-identical.
Fixes #6145
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): make PORT the real backend port and test the whole chain
Addresses review feedback on #6168.
The first round's root-cause claim was wrong. It said `BACKEND_PORT=9100 make
selfhost` published on 9100 while the health check probed 8080. It does not:
when make drives Compose, Compose inherits make's exported values, so both
sides agree on 8080 and the override is silently dropped instead. Re-measured
through the real recipe, the genuine disagreements were:
- `.env` setting PORT with no BACKEND_PORT: probe followed PORT, Compose
published ${BACKEND_PORT:-8080} — 9100 vs 8080.
- `make selfhost PORT=8080` over a .env with BACKEND_PORT=9100: probe 8080,
Compose published 9100.
Both are the "wrong port variable" defect the GitHub issue names, and reading
the port back from Compose fixes both. The comments and PR text that blamed
make/Compose precedence are corrected.
The PORT fallback added to docker-compose.selfhost.yml was also unreachable:
.env.example shipped BACKEND_PORT=8080 uncommented, so the fallback never
engaged and `SELF_HOSTING_AI.md`'s "edit PORT and FRONTEND_PORT" instruction did
nothing. Pick one contract and apply it everywhere: PORT is the backend port to
edit, BACKEND_PORT/API_PORT/SERVER_PORT are optional aliases that override it.
That is already what Makefile, scripts/local-env.sh, scripts/install.sh and
install.ps1 do; compose and the web dev fallback were the outliers.
- .env.example ships BACKEND_PORT commented out, like its sibling aliases, so
editing PORT works out of the box.
- apps/web/config/runtime-urls.ts walks the same alias chain instead of
reading BACKEND_PORT alone, so `pnpm dev` follows an edited PORT.
- SELF_HOSTING_AI.md and SELF_HOSTING_ADVANCED.md state the contract.
Configuration that cannot take effect is now reported instead of ignored.
scripts/selfhost-preflight.sh warns when an alias in the env file overrides an
edited PORT, and when a port from the shell environment is overridden by the env
file. The Makefile captures the pristine origins before its include, because
afterwards there is no way to tell that `PORT=9000 make selfhost` was asked for.
Tests now cross environment -> make -> compose -> report for real. The docker
stub is no longer told what to publish: on `up` it asks the real
`docker compose config` to interpolate the compose file with the environment the
recipe actually handed it, records that, and answers `port` from the recording.
Verified failing on the old code — with the old recipe and compose file the
suite reports published 8080 against probe 9100, the exact defect.
Co-authored-by: multica-agent <github@multica.ai>
* ci: gate the self-host test on scripts/selfhost-preflight.sh too
The new preflight script was missing from the frontend path filter, so a
change to it alone would skip the test that covers it.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): honour the full backend port alias chain in Compose
Addresses the second review round on #6168.
The contract this PR documents is
BACKEND_PORT -> API_PORT -> SERVER_PORT -> PORT -> 8080
and Makefile, scripts/local-env.sh, scripts/install.sh, scripts/install.ps1 and
apps/web/config/runtime-urls.ts all implement it. docker-compose.selfhost.yml
implemented only BACKEND_PORT -> PORT -> 8080, so `API_PORT=9100` or
`SERVER_PORT=9200` in .env still published 8080.
That is not only a direct-Compose concern. scripts/install.sh:407 derives its
health-check port from the full chain and then runs this compose file, so an
alias it honours but Compose ignores puts the probe on 9100 while the stack
listens on 8080 — the original #6145 defect, reached through the installer.
- docker-compose.selfhost.yml publishes the full nested chain, verified to
keep the same precedence and the same 8080 default.
- scripts/selfhost-wait.sh's fallback matches, so the degraded path agrees
with what Compose would have published.
- The Makefile captures the pristine origins of API_PORT and SERVER_PORT too,
and the preflight reports them, so no alias can be silently overridden by
the env file while the others are reported.
Tests pin the chain on the boundaries a recipe-level test cannot see, because
Makefile normalises all four aliases into PORT before any recipe runs: a matrix
over the direct Compose path asserts each alias moves the published port with
the right precedence, and every case additionally asserts that
scripts/install.sh's resolver returns the same value Compose published. That
invariant is the one that actually protects the installer.
Verified failing without the fix: with the two-level chain restored the suite
reports `expected 9200 / compose published 8080 / installer resolved 9200`, and
with the alias warnings narrowed it reports the missing API_PORT notice.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): resolve one winner before reporting ignored port config
Addresses the third review round on #6168.
The preflight walked each alias independently, so it made a separate "wins"
claim per alias. With
PORT=9000
BACKEND_PORT=8000
API_PORT=7000
SERVER_PORT=6000
in .env it announced that BACKEND_PORT, API_PORT and SERVER_PORT each won, when
only BACKEND_PORT=8000 can. Two of the three notices were simply false.
It also compared only same-named shell and file variables, so shadowing across
aliases stayed silent: .env with PORT=8000 and BACKEND_PORT=8000 plus a shell
API_PORT=7000 produced no output at all, even though API_PORT was discarded.
Resolve the winner along the documented chain first — within a variable the env
file beats the environment, then BACKEND_PORT beats API_PORT beats SERVER_PORT
beats PORT — and report only the inputs that winner actually shadows. Output now
names one winning input and lists the rest, whichever variable or source they
came from. An input carrying the winning value is not reported: it is redundant
but the user still gets the port they asked for, so there is nothing to fix.
Tests cover both cases the old logic got wrong: every alias set at once must
yield exactly one winner claim and list the others as unused, and an env-file
alias beating a lower-priority shell alias must be reported. Also asserted that
a redundant same-value alias and a default configuration both stay silent.
Measured against the previous implementation, phrasing aside: with all four
values set it made 3 winner claims where there is now 1, and on the cross-alias
case it printed nothing where API_PORT is now reported.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): track env-file existence so empty port assignments resolve right
Addresses the fourth review round on #6168.
The Makefile passed only values to the preflight, so the script inferred "the
env file does not set this" from an empty string. An explicit empty assignment is
a different input from an absent one: make lets `BACKEND_PORT=` in the env file
override `BACKEND_PORT=9000` from the environment, and the variable then drops out
of the alias chain instead of setting a port.
With .env holding PORT=8080 and BACKEND_PORT=, invoked as
`BACKEND_PORT=9000 make selfhost`, the stack published 8080 and the health check
probed 8080 — correct — while the preflight announced
the backend host port resolves to 9000 from BACKEND_PORT (environment)
Set but unused: PORT=8080 (.env)
naming the ignored value as the winner and the winning value as unused. A report
that contradicts the startup is worse than no report. FRONTEND_PORT= had the
matching hole: it shadowed the environment, Compose fell back to 3000, and the
preflight said nothing.
The Makefile now captures ENV_FILE_<VAR>_IS_SET from $(origin) alongside the
value, for all five port variables via one $(foreach) instead of hand-written
lines. The preflight treats a variable the file defines as taking the file's
value even when empty, so it shadows the environment, and an empty value never
wins — resolution continues down the chain and falls back to 8080. Inputs the
winner shadows are still listed, including ones shadowed by an empty assignment.
The frontend check mirrors this and now names the port that actually resolved.
Recipe-level tests cover an empty alias over a shell value, every link of the
chain emptied at once, and the frontend equivalent — each asserting the reported
winner, the Compose published port and the probed port all agree. Against the
previous implementation the first case fails with `reported: 9000 / actual: 8080`.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): installers read the published port; drop the second parser
Addresses all four blockers from the consolidated review on #6168.
1. Both installers probed and printed the wrong port. scripts/install.sh:402 and
scripts/install.ps1 start Compose inheriting the current environment, and
Compose lets that environment outrank .env — but the installers then derived
the probe port and the summary URL from .env alone. Measured on this branch
before the fix, every port variable diverged:
ambient PORT=9100 -> compose 9100, installer 8080
ambient BACKEND_PORT=9200 -> compose 9200, installer 8080
ambient API_PORT=9300 -> compose 9300, installer 8080
ambient SERVER_PORT=9400 -> compose 9400, installer 8080
ambient FRONTEND_PORT=3100 -> compose 3100, installer 3000
Both now read the ports back once with `docker compose port` after `up -d`
and reuse that single result for the health check and the summary. If the
query fails they fail loudly instead of claiming success. The .env-derived
helpers are deleted rather than left beside the new path.
2. The Make preflight is removed entirely, as the review recommended. It
modelled `origin=environment` and `origin=file` but not `command line`, so
`make selfhost BACKEND_PORT=9000` over a .env with API_PORT=7000 announced
7000 while Compose published 9000. That was its fourth wrong report in four
rounds, because it was a second parser of precedence rules — the very thing
this PR removes elsewhere. `docker compose port` is the runtime truth, so the
script, the Makefile captures and the macro are gone.
3. Tests now cover the installer paths that were unguarded. scripts/install.test.sh
gains a `--with-server` matrix (defaults, .env PORT, all four backend aliases,
FRONTEND_PORT, ambient overriding .env for all five, and explicit-empty
fallback) plus a case proving an unresolvable port fails loudly. A new
scripts/install.ps1.test.ps1 drives the same matrix through Start-LocalInstall.
Every case asserts Compose's published port == the probed URL == the printed
URL. Ambient variables are explicitly cleared per case so a runner's own PORT
cannot leak. The Makefile matrix gains the command-line origin cases. CI runs
the PowerShell suite on windows-latest in the always-on installer job, and the
frontend filter now includes both installers.
4. Docs state the real contract: the alias order is shared, but which *source*
wins is per entry point — Compose prefers the environment, make prefers the
included env file, and a make command-line assignment outranks both. The
removed preflight's promise is gone from SELF_HOSTING_AI.md, and the compose
header no longer claims the installer derives its own health-check port.
Verified failing without the fixes: the Bash matrix reports
`[ambient PORT beats .env] compose published 9500 / probed 9100 / printed 9100`
against the old installer, and the PowerShell matrix reports
`printed backend=8080` against an injected summary divergence.
Co-authored-by: multica-agent <github@multica.ai>
* fix(selfhost): keep web dev proxy off frontend port
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
519 lines
15 KiB
Bash
519 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
# Build a self-contained sandbox with stub `curl` and a tarball that the
|
|
# release-binary fallback path will download. Each test supplies its own
|
|
# `brew` stub to model a specific Homebrew failure mode.
|
|
_setup_sandbox() {
|
|
local tmp="$1"
|
|
local stub_bin="$tmp/stub-bin"
|
|
local install_bin="$tmp/install-bin"
|
|
local payload_dir="$tmp/payload"
|
|
mkdir -p "$stub_bin" "$install_bin" "$payload_dir"
|
|
|
|
cat >"$payload_dir/multica" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
echo "multica v0.3.2 (commit: test)"
|
|
STUB
|
|
chmod +x "$payload_dir/multica"
|
|
tar -czf "$tmp/multica.tar.gz" -C "$payload_dir" multica
|
|
|
|
cat >"$stub_bin/curl" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
if [[ "$*" == *"-sI"* ]]; then
|
|
printf 'HTTP/2 302\r\nlocation: https://github.com/multica-ai/multica/releases/tag/v0.3.2\r\n'
|
|
exit 0
|
|
fi
|
|
|
|
out=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-o)
|
|
out="$2"
|
|
shift 2
|
|
;;
|
|
*)
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$out" ]]; then
|
|
echo "stub curl expected -o" >&2
|
|
exit 2
|
|
fi
|
|
cp "$MULTICA_TEST_ARCHIVE" "$out"
|
|
STUB
|
|
chmod +x "$stub_bin/curl"
|
|
}
|
|
|
|
_run_installer() {
|
|
local tmp="$1"
|
|
local out="$tmp/install.out"
|
|
local err="$tmp/install.err"
|
|
if ! PATH="$tmp/stub-bin:$tmp/install-bin:/usr/bin:/bin" \
|
|
MULTICA_BIN_DIR="$tmp/install-bin" \
|
|
MULTICA_TEST_ARCHIVE="$tmp/multica.tar.gz" \
|
|
bash "$ROOT_DIR/scripts/install.sh" >"$out" 2>"$err"; then
|
|
echo "install.sh exited non-zero" >&2
|
|
cat "$out" >&2 || true
|
|
cat "$err" >&2 || true
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -x "$tmp/install-bin/multica" ]]; then
|
|
echo "expected fallback binary at $tmp/install-bin/multica" >&2
|
|
cat "$out" >&2 || true
|
|
cat "$err" >&2 || true
|
|
return 1
|
|
fi
|
|
|
|
if ! grep -q "Homebrew output (last 80 lines):" "$err"; then
|
|
echo "expected diagnostic tail in stderr" >&2
|
|
cat "$err" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
test_brew_install_failure_falls_back_to_release_binary() {
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' RETURN
|
|
|
|
_setup_sandbox "$tmp"
|
|
cat >"$tmp/stub-bin/brew" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
case "${1:-}" in
|
|
tap)
|
|
exit 0
|
|
;;
|
|
install)
|
|
echo "simulated brew install failure" >&2
|
|
exit 42
|
|
;;
|
|
list)
|
|
exit 1
|
|
;;
|
|
*)
|
|
exit 0
|
|
;;
|
|
esac
|
|
STUB
|
|
chmod +x "$tmp/stub-bin/brew"
|
|
|
|
_run_installer "$tmp"
|
|
}
|
|
|
|
test_brew_tap_failure_falls_back_to_release_binary() {
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' RETURN
|
|
|
|
_setup_sandbox "$tmp"
|
|
cat >"$tmp/stub-bin/brew" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
case "${1:-}" in
|
|
tap)
|
|
echo "simulated brew tap failure" >&2
|
|
exit 17
|
|
;;
|
|
*)
|
|
echo "brew $* should not be reached after tap failure" >&2
|
|
exit 99
|
|
;;
|
|
esac
|
|
STUB
|
|
chmod +x "$tmp/stub-bin/brew"
|
|
|
|
_run_installer "$tmp"
|
|
}
|
|
|
|
test_remote_ssh_install_prints_token_login_hint() {
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' RETURN
|
|
|
|
_setup_sandbox "$tmp"
|
|
cat >"$tmp/stub-bin/brew" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
case "${1:-}" in
|
|
tap)
|
|
exit 0
|
|
;;
|
|
install)
|
|
echo "simulated brew install failure" >&2
|
|
exit 42
|
|
;;
|
|
list)
|
|
exit 1
|
|
;;
|
|
*)
|
|
exit 0
|
|
;;
|
|
esac
|
|
STUB
|
|
chmod +x "$tmp/stub-bin/brew"
|
|
|
|
(
|
|
export SSH_CONNECTION="192.0.2.10 54321 198.51.100.20 22"
|
|
_run_installer "$tmp"
|
|
)
|
|
|
|
if ! grep -q "Looks like a remote/SSH session" "$tmp/install.out"; then
|
|
echo "expected remote/SSH token-login hint in installer output" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
if ! grep -q "https://multica.ai/settings?tab=tokens" "$tmp/install.out"; then
|
|
echo "expected direct API Tokens settings URL in installer output" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
if ! grep -q "Settings > API Tokens" "$tmp/install.out"; then
|
|
echo "expected API Tokens tab name in installer output" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
if ! grep -q "multica login --token <YOUR_TOKEN>" "$tmp/install.out"; then
|
|
echo "expected token login command in installer output" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
if grep -q "multica config set server_url" "$tmp/install.out"; then
|
|
echo "did not expect default cloud server config command in installer output" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
if grep -q "multica config set app_url" "$tmp/install.out"; then
|
|
echo "did not expect default cloud app config command in installer output" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
test_local_install_does_not_print_token_login_hint() {
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' RETURN
|
|
|
|
_setup_sandbox "$tmp"
|
|
cat >"$tmp/stub-bin/brew" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
case "${1:-}" in
|
|
tap)
|
|
exit 0
|
|
;;
|
|
install)
|
|
echo "simulated brew install failure" >&2
|
|
exit 42
|
|
;;
|
|
list)
|
|
exit 1
|
|
;;
|
|
*)
|
|
exit 0
|
|
;;
|
|
esac
|
|
STUB
|
|
chmod +x "$tmp/stub-bin/brew"
|
|
|
|
(
|
|
unset SSH_CONNECTION SSH_CLIENT SSH_TTY
|
|
_run_installer "$tmp"
|
|
)
|
|
|
|
if grep -q "Looks like a remote/SSH session" "$tmp/install.out"; then
|
|
echo "did not expect remote/SSH token-login hint in local installer output" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
if grep -q "multica login --token <YOUR_TOKEN>" "$tmp/install.out"; then
|
|
echo "did not expect token login command in local installer output" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# --with-server: the probed port and the printed port must both be the port
|
|
# Docker Compose reported (#6145)
|
|
#
|
|
# The installer used to derive the port from .env with its own copy of the alias
|
|
# chain. Compose gives the *calling environment* precedence over .env, so any
|
|
# ambient PORT / BACKEND_PORT / API_PORT / SERVER_PORT / FRONTEND_PORT moved the
|
|
# published port while the installer kept probing and printing the file value.
|
|
#
|
|
# Here the docker stub plays Compose: it answers `port` from the same resolution
|
|
# Compose performs, environment first, then .env. The installer must take that
|
|
# answer as given for both the health check and the summary — which is exactly
|
|
# what a .env-only derivation cannot do, because the two disagree in every case
|
|
# below. That real Compose resolves this way is proven separately, against real
|
|
# `docker compose config`, in scripts/selfhost-config.test.sh; that test needs a
|
|
# Docker CLI, which this job deliberately does not require.
|
|
# ---------------------------------------------------------------------------
|
|
_setup_server_sandbox() {
|
|
local tmp="$1"
|
|
local stub_bin="$tmp/stub-bin"
|
|
local server_dir="$tmp/server"
|
|
mkdir -p "$stub_bin" "$server_dir/.git"
|
|
|
|
# Minimal self-host assets: only the port mapping matters here.
|
|
cat >"$server_dir/.env.example" <<'ENVFILE'
|
|
PORT=8080
|
|
# BACKEND_PORT=8080
|
|
# API_PORT=8080
|
|
# SERVER_PORT=8080
|
|
FRONTEND_PORT=3000
|
|
JWT_SECRET=change-me-in-production
|
|
POSTGRES_PASSWORD=multica
|
|
DATABASE_URL=postgres://multica:multica@localhost:5432/multica?sslmode=disable
|
|
ENVFILE
|
|
touch "$server_dir/docker-compose.selfhost.yml"
|
|
|
|
# Compose stand-in. Resolves the published host port the way Compose does:
|
|
# the process environment wins over .env, then the alias chain decides.
|
|
cat >"$stub_bin/docker" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
|
|
_env_file_value() {
|
|
local key="$1" line
|
|
line="$(grep -E "^${key}=" .env 2>/dev/null | tail -n 1 || true)"
|
|
[ -n "$line" ] || return 1
|
|
printf '%s' "${line#*=}"
|
|
}
|
|
|
|
# Environment first (Compose interpolation), then the env file.
|
|
_resolve() {
|
|
local key="$1" from_env
|
|
eval "from_env=\${$key-__unset__}"
|
|
if [ "$from_env" != "__unset__" ]; then
|
|
printf '%s' "$from_env"
|
|
return 0
|
|
fi
|
|
_env_file_value "$key"
|
|
}
|
|
|
|
_published_backend_port() {
|
|
local value
|
|
for key in BACKEND_PORT API_PORT SERVER_PORT PORT; do
|
|
if value="$(_resolve "$key")" && [ -n "$value" ]; then
|
|
printf '%s' "$value"
|
|
return
|
|
fi
|
|
done
|
|
printf '8080'
|
|
}
|
|
|
|
_published_frontend_port() {
|
|
local value
|
|
if value="$(_resolve FRONTEND_PORT)" && [ -n "$value" ]; then
|
|
printf '%s' "$value"
|
|
return
|
|
fi
|
|
printf '3000'
|
|
}
|
|
|
|
case "${1:-}" in
|
|
info) exit 0 ;;
|
|
compose)
|
|
shift
|
|
subcommand=""
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
pull | up | port | version | ps | logs | down) subcommand="$arg"; break ;;
|
|
esac
|
|
done
|
|
case "$subcommand" in
|
|
port)
|
|
service=""
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
backend | frontend) service="$arg"; break ;;
|
|
esac
|
|
done
|
|
case "$service" in
|
|
backend) printf '127.0.0.1:%s\n' "$(_published_backend_port)" ;;
|
|
frontend) printf '127.0.0.1:%s\n' "$(_published_frontend_port)" ;;
|
|
*) exit 1 ;;
|
|
esac
|
|
;;
|
|
version) echo "2.30.0" ;;
|
|
esac
|
|
exit 0
|
|
;;
|
|
esac
|
|
exit 0
|
|
STUB
|
|
chmod +x "$stub_bin/docker"
|
|
|
|
# git: the installer takes the "existing installation" path, so only the
|
|
# fetch/checkout calls run and they are all tolerant of failure.
|
|
printf '#!/usr/bin/env bash\nexit 0\n' >"$stub_bin/git"
|
|
chmod +x "$stub_bin/git"
|
|
|
|
# brew: pretend the CLI installs cleanly so the run reaches the summary.
|
|
printf '#!/usr/bin/env bash\nexit 0\n' >"$stub_bin/brew"
|
|
chmod +x "$stub_bin/brew"
|
|
|
|
printf '#!/usr/bin/env bash\necho "multica v0.3.2 (commit: test)"\n' >"$stub_bin/multica"
|
|
chmod +x "$stub_bin/multica"
|
|
|
|
# curl records every probed URL so the health-check port can be asserted.
|
|
cat >"$stub_bin/curl" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
http*) printf '%s\n' "$arg" >>"$MULTICA_TEST_CURL_LOG" ;;
|
|
esac
|
|
done
|
|
exit 0
|
|
STUB
|
|
chmod +x "$stub_bin/curl"
|
|
|
|
printf '#!/usr/bin/env bash\nhead -c 32 /dev/zero | od -An -tx1 | tr -d " \\n"\n' >"$stub_bin/openssl"
|
|
chmod +x "$stub_bin/openssl"
|
|
}
|
|
|
|
# Runs `install.sh --with-server` with the sandbox stubs. Remaining arguments are
|
|
# ambient environment assignments, so each case controls the environment
|
|
# explicitly instead of inheriting a CI runner's PORT.
|
|
_run_with_server() {
|
|
local tmp="$1"
|
|
shift
|
|
|
|
: >"$tmp/curl.log"
|
|
if ! env -i \
|
|
PATH="$tmp/stub-bin:/usr/bin:/bin" \
|
|
HOME="$tmp" \
|
|
MULTICA_INSTALL_DIR="$tmp/server" \
|
|
MULTICA_SELFHOST_REF="main" \
|
|
MULTICA_TEST_CURL_LOG="$tmp/curl.log" \
|
|
"$@" \
|
|
bash "$ROOT_DIR/scripts/install.sh" --with-server \
|
|
>"$tmp/install.out" 2>"$tmp/install.err"; then
|
|
echo "install.sh --with-server exited non-zero" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
cat "$tmp/install.err" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Asserts the probed port and the printed ports all match the stub's answer.
|
|
_require_server_ports() {
|
|
local tmp="$1" label="$2" expected_backend="$3" expected_frontend="$4"
|
|
local probed printed_backend printed_frontend
|
|
|
|
probed="$(sed -n '1s#.*localhost:\([0-9]*\)/health#\1#p' "$tmp/curl.log")"
|
|
printed_backend="$(sed -n 's#.*Backend:[^0-9]*http://localhost:\([0-9]*\).*#\1#p' "$tmp/install.out" | head -n 1)"
|
|
printed_frontend="$(sed -n 's#.*Frontend:[^0-9]*http://localhost:\([0-9]*\).*#\1#p' "$tmp/install.out" | head -n 1)"
|
|
|
|
if [ "$probed" != "$expected_backend" ] ||
|
|
[ "$printed_backend" != "$expected_backend" ] ||
|
|
[ "$printed_frontend" != "$expected_frontend" ]; then
|
|
echo "[$label] installer ports disagree with the port Compose published" >&2
|
|
echo " compose published: backend=$expected_backend frontend=$expected_frontend" >&2
|
|
echo " health check probed: ${probed:-<none>}" >&2
|
|
echo " printed: backend=${printed_backend:-<none>} frontend=${printed_frontend:-<none>}" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
test_with_server_uses_compose_published_ports() {
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' RETURN
|
|
|
|
# label | .env mutation (sed) | ambient env | expected backend | expected frontend
|
|
local cases='defaults|||8080|3000
|
|
env-file PORT|s/^PORT=8080/PORT=9100/||9100|3000
|
|
env-file BACKEND_PORT|s/^# BACKEND_PORT=8080/BACKEND_PORT=9200/||9200|3000
|
|
env-file API_PORT|s/^# API_PORT=8080/API_PORT=9300/||9300|3000
|
|
env-file SERVER_PORT|s/^# SERVER_PORT=8080/SERVER_PORT=9400/||9400|3000
|
|
env-file FRONTEND_PORT|s/^FRONTEND_PORT=3000/FRONTEND_PORT=3100/||8080|3100
|
|
ambient PORT beats .env|s/^PORT=8080/PORT=9100/|PORT=9500|9500|3000
|
|
ambient BACKEND_PORT beats .env|s/^PORT=8080/PORT=9100/|BACKEND_PORT=9600|9600|3000
|
|
ambient API_PORT beats .env|s/^PORT=8080/PORT=9100/|API_PORT=9700|9700|3000
|
|
ambient SERVER_PORT beats .env|s/^PORT=8080/PORT=9100/|SERVER_PORT=9800|9800|3000
|
|
ambient FRONTEND_PORT beats .env|s/^FRONTEND_PORT=3000/FRONTEND_PORT=3100/|FRONTEND_PORT=3200|8080|3200
|
|
empty ambient BACKEND_PORT falls through|s/^PORT=8080/PORT=9100/|BACKEND_PORT=|9100|3000
|
|
empty env-file BACKEND_PORT falls through|s/^PORT=8080/PORT=9100/;s/^# BACKEND_PORT=8080/BACKEND_PORT=/||9100|3000'
|
|
|
|
local label mutation ambient expect_backend expect_frontend
|
|
while IFS='|' read -r label mutation ambient expect_backend expect_frontend; do
|
|
[ -n "$label" ] || continue
|
|
|
|
rm -rf "$tmp/server" "$tmp/stub-bin"
|
|
_setup_server_sandbox "$tmp"
|
|
cp "$tmp/server/.env.example" "$tmp/server/.env"
|
|
if [ -n "$mutation" ]; then
|
|
sed "$mutation" "$tmp/server/.env" >"$tmp/server/.env.new"
|
|
mv "$tmp/server/.env.new" "$tmp/server/.env"
|
|
fi
|
|
|
|
if [ -n "$ambient" ]; then
|
|
_run_with_server "$tmp" "$ambient" || return 1
|
|
else
|
|
_run_with_server "$tmp" || return 1
|
|
fi
|
|
_require_server_ports "$tmp" "$label" "$expect_backend" "$expect_frontend" || return 1
|
|
done <<<"$cases"
|
|
}
|
|
|
|
test_with_server_fails_when_compose_port_is_unavailable() {
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' RETURN
|
|
|
|
_setup_server_sandbox "$tmp"
|
|
cp "$tmp/server/.env.example" "$tmp/server/.env"
|
|
|
|
# Compose cannot report a port, e.g. the container never came up.
|
|
cat >"$tmp/stub-bin/docker" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
case "${1:-}" in
|
|
info) exit 0 ;;
|
|
compose)
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
port) exit 1 ;;
|
|
version) echo "2.30.0"; exit 0 ;;
|
|
esac
|
|
done
|
|
exit 0
|
|
;;
|
|
esac
|
|
exit 0
|
|
STUB
|
|
chmod +x "$tmp/stub-bin/docker"
|
|
|
|
if _run_with_server "$tmp" >/dev/null 2>&1; then
|
|
echo "installer must not report success when Compose cannot report the port" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
if ! grep -q "could not read the backend host port" "$tmp/install.err"; then
|
|
echo "expected an explicit failure about the backend host port" >&2
|
|
cat "$tmp/install.err" >&2 || true
|
|
return 1
|
|
fi
|
|
if grep -q "server is running and CLI is ready" "$tmp/install.out"; then
|
|
echo "installer claimed success despite an unresolved port" >&2
|
|
cat "$tmp/install.out" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
test_brew_install_failure_falls_back_to_release_binary
|
|
test_brew_tap_failure_falls_back_to_release_binary
|
|
test_remote_ssh_install_prints_token_login_hint
|
|
test_local_install_does_not_print_token_login_hint
|
|
test_with_server_uses_compose_published_ports
|
|
test_with_server_fails_when_compose_port_is_unavailable
|
|
echo "install.sh tests passed"
|