mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 19:35:04 +02:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - A release is gated by the release smoke: it installs the published
`paperclipai` artifact into a Docker container and drives the sign-in →
onboarding → first-agent path with Playwright
> - That suite runs only from the release pipeline, never on a pull
request, so it sees the UI only after the UI has already changed
> - The onboarding wizard was rebuilt into the agent arc. The "Name your
organization" step, the "Start Onboarding" launcher, and the agent role
picker are all gone
> - The spec still waited for those, so it failed on its first assertion
and blocked every nightly and beta release
> - The failure was also hard to read. The workflow uploaded no
container logs, because it learned the container's name only after the
harness succeeded, and the harness ran the container with `--rm` and
deleted it before anything read it
> - This pull request rewrites the spec to follow the current arc, and
repairs the log capture at both ends
> - The benefit is that nightly and beta releases are unblocked, and the
next failure arrives with the logs attached
## Linked Issues or Issue Description
No existing issue. Describing it inline, following
`.github/ISSUE_TEMPLATE/bug_report.yml`.
Refs #12274 (removed the company-naming step from the wizard).
Refs #12135 (the previous alignment of this spec, before #12274).
Refs #12316 (open; also edits `scripts/docker-onboard-smoke.sh`, in the
bootstrap helpers rather than the container lifecycle, so the two
changes do
not overlap. Whichever lands second should rebase and re-run).
**What happened?**
The release smoke fails.
`tests/release-smoke/docker-auth-onboarding.spec.ts`
never gets past its first wait:
```
✘ tests/release-smoke/docker-auth-onboarding.spec.ts:43:3 › Docker authenticated onboarding smoke › logs in, completes onboarding, and hires the lead agent
Error: expect(locator).toBeVisible() failed — element(s) not found (timeout 20000ms)
> 33 | await expect(wizardHeading.or(startButton)).toBeVisible({ timeout: 20_000 });
```
The spec waits for an `h3` reading "Name your organization" or a
"Start Onboarding" button. Neither exists. #12274 removed the
company-naming
step; the string now survives only in a code comment and in
`ui/src/components/OnboardingWizard.step.test.tsx`, which asserts it is
*absent*. The steps after the first wait are stale too: the CTA on step
1 is
"Continue" and not "Next", the organization input's placeholder changed,
and
the agent step's `#onboarding-agent-role` picker is gone, so every
onboarding
hire is filed under the neutral `general` role.
The suite runs only from the release pipeline, so nothing on a pull
request
saw the drift. Both `smoke_nightly` and `smoke_beta` call the same
reusable
workflow, so every nightly and every beta was blocked.
The failure also arrived without diagnostics. The job's "Capture Docker
logs"
step is `if: always()`, but it is guarded on `SMOKE_CONTAINER_NAME`,
which the
"Launch Docker smoke harness" step writes to `$GITHUB_ENV` only *after*
the
harness returns. On any failure before that the guard is false, the step
does
nothing, and the upload reports "No files were found". Below that,
`scripts/docker-onboard-smoke.sh` starts the container with
`docker run -d --rm`, so the `docker stop` in its EXIT trap deletes the
container and its logs together — and a container that crashes on its
own is
removed the instant its process exits.
**Expected behavior**
The spec walks the onboarding arc the app actually presents, and proves
the
company is created, the lead agent is hired, and the first task is
seeded and
dispatched. When the smoke fails, the run's artifact carries the
container's
logs.
**Steps to reproduce**
1. Run the Release Smoke workflow against a published artifact that
carries
#12274, or run it locally:
`PAPERCLIPAI_VERSION=2026.828.0-canary.3 SMOKE_DETACH=true
./scripts/docker-onboard-smoke.sh`
2. Run `pnpm run test:release-smoke` against that container.
3. The single spec fails at `openOnboarding()` after 20 seconds.
4. In CI, open the run's `release-smoke` artifact. It has no
`docker-onboard-smoke.log`.
**Paperclip version or commit**
`2026.828.0-canary.3` (commit 8316ceb0b).
**Deployment mode**
Docker.
**Installation method**
npm / pnpm global install (the container runs `npx
paperclipai@<version>`).
**Node.js version**
v24.20.0 inside the container.
**Relevant logs or output**
```
Running 1 test using 1 worker
✓ 1 [chromium] › tests/release-smoke/docker-auth-onboarding.spec.ts:76:3 › Docker authenticated onboarding smoke › logs in, completes onboarding, and hires the lead agent (7.0s)
1 passed (8.7s)
```
That is the result after this change. Before it, the same command failed
at
the first wait, as quoted above.
## What Changed
- `tests/release-smoke/docker-auth-onboarding.spec.ts` now follows the
current
arc. It signs in, opens `/onboarding`, names the organization and
presses
"Continue" (which creates the company and routes straight to the agent
step,
because onboarding no longer asks for a mission), names the lead and
presses
"Next", presses "Connect" on the default adapter to hire, then presses
"Get started" to launch.
- The spec addresses controls by role and accessible name, or by id
where one
exists (`#onboarding-agent-name`). Step 1's field has no id and no
associated
label, so it is found as the wizard's only text box rather than by its
placeholder copy.
- The spec asserts the hired agent's role is `general`, which is what
the arc
files every onboarding hire under. Every other API assertion is
unchanged.
- The spec navigates to `/onboarding` explicitly and drops any saved
onboarding
draft first, so it can run twice against one instance. The suite retries
once
in CI. It still asserts that a company-less board routes sign-in into
onboarding, guarded on the board actually being empty.
- `scripts/docker-onboard-smoke.sh` accepts `SMOKE_CONTAINER_NAME`,
drops
`--rm`, removes the container itself, and dumps `docker logs` to
`SMOKE_LOG_FILE` before the teardown.
- `.github/workflows/release-smoke.yml` pins the container name in the
job's
`env`, so every `always()` step has it before anything runs. The capture
step
refreshes the log from a live container when there is one, keeps the
harness's dump when there is not, and writes a one-line explanation when
there is neither. The upload's paths are literals, and
`if-no-files-found: error` makes a broken diagnostics path fail rather
than
warn.
- `scripts/docker-onboard-smoke.test.mjs` pins that wiring. It is added
to
`test:release-registry`, which runs on every pull request.
- `doc/DOCKER.md` documents `SMOKE_CONTAINER_NAME` and `SMOKE_LOG_FILE`.
## Verification
The spec was run against a real container built from the published
`2026.828.0-canary.3` artifact, exactly as the workflow runs it.
```sh
SMOKE_CONTAINER_NAME=release-smoke-onboard \
HOST_PORT=3232 DATA_DIR=<tmp>/smoke-data \
PAPERCLIPAI_VERSION=2026.828.0-canary.3 \
SMOKE_READY_TIMEOUT_SECONDS=420 SMOKE_DETACH=true \
SMOKE_METADATA_FILE=<tmp>/release-smoke.env \
SMOKE_LOG_FILE=<tmp>/docker-onboard-smoke.log \
./scripts/docker-onboard-smoke.sh
PAPERCLIP_RELEASE_SMOKE_BASE_URL=http://localhost:3232 \
PAPERCLIP_RELEASE_SMOKE_EMAIL=smoke-admin@paperclip.local \
PAPERCLIP_RELEASE_SMOKE_PASSWORD=paperclip-smoke-password \
PAPERCLIP_PLAYWRIGHT_CHANNEL=chrome \
pnpm run test:release-smoke
```
```
Running 1 test using 1 worker
✓ 1 [chromium] › tests/release-smoke/docker-auth-onboarding.spec.ts:76:3 › Docker authenticated onboarding smoke › logs in, completes onboarding, and hires the lead agent (7.0s)
1 passed (8.7s)
```
The same command was run a second time against the same, now non-empty,
instance. That covers the retry path, and it also passes.
The log capture was verified by making the container die during startup:
```sh
PAPERCLIPAI_VERSION=0.0.0-no-such-version \
SMOKE_CONTAINER_NAME=release-smoke-onboard SMOKE_LOG_FILE=<tmp>/fail.log \
./scripts/docker-onboard-smoke.sh
```
`<tmp>/fail.log` was written and carried the cause:
```
npm error code ETARGET
npm error notarget No matching version found for paperclipai@0.0.0-no-such-version.
```
The container was removed afterwards. On `master` this file is never
written,
because `--rm` deletes the container the moment its process exits.
The workflow's capture step was run by hand against three states: a live
container (258 lines), a removed container with the harness's dump
already on
disk (258 lines kept), and neither (a one-line explanation).
Unit coverage:
```sh
pnpm run test:release-registry # 93 tests, 93 pass
```
Nothing under `ui/` changed, so `pnpm --filter @paperclipai/ui
typecheck` was
not required. `tests/release-smoke` is outside the TypeScript project
references; Playwright compiles it at run time, which the runs above did
three
times.
## Risks
Low risk. Nothing ships to users. The change touches one Playwright
spec, one
smoke script, and one workflow.
Points worth a reviewer's attention:
- **This suite gates every nightly and beta, and it runs only
post-merge.**
`smoke_nightly` and `smoke_beta` both call `release-smoke.yml`, and no
pull
request runs it. Drift between the wizard and this spec is therefore
invisible until a release is already blocked, which is how this bug
reached
a release train. I think the arc deserves an earlier check. The cheapest
version is the one added here: `scripts/docker-onboard-smoke.test.mjs`
runs
on every pull request and pins the harness wiring. The full container
smoke
is too slow for the pull request path, but a UI-level test of the arc's
step
sequence would catch exactly this class of drift, and
`ui/src/components/OnboardingWizard.step.test.tsx` is already the right
home for it. I did not add it here, to keep this change to the repair.
- **Dropping `--rm`.** The container is now removed by the script's
cleanup
instead of by Docker. The script already ran `docker rm -f` before
starting,
and the workflow's final step removes it too, so a leaked container is
cleaned up on the next run either way. A developer who kills the script
with
`SIGKILL` will leave a stopped container behind, where previously they
would
not.
- **`if-no-files-found: error` on the upload.** The capture step now
always
writes the log file, so the upload always has at least one path to
match. If
that ever stops being true, the job fails instead of warning. That is
deliberate.
- **The spec drops the saved onboarding draft before it walks.** A stale
draft
makes step 1 skip company creation and hire into the previous run's
company.
That state only exists when the spec runs twice against one instance. A
fresh
release-smoke container never has it.
## Model Used
Claude (Anthropic), Claude Opus, 1M context, extended thinking, agentic
tool
use via Claude Code. The container, the Playwright runs, and the failure
injection were driven as real commands on a local Docker host.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
361 lines
12 KiB
Bash
Executable File
361 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
IMAGE_NAME="${IMAGE_NAME:-paperclip-onboard-smoke}"
|
|
HOST_PORT="${HOST_PORT:-3131}"
|
|
PAPERCLIPAI_VERSION="${PAPERCLIPAI_VERSION:-latest}"
|
|
DATA_DIR="${DATA_DIR:-$REPO_ROOT/data/docker-onboard-smoke}"
|
|
HOST_UID="${HOST_UID:-$(id -u)}"
|
|
SMOKE_DETACH="${SMOKE_DETACH:-false}"
|
|
SMOKE_METADATA_FILE="${SMOKE_METADATA_FILE:-}"
|
|
PAPERCLIP_DEPLOYMENT_MODE="${PAPERCLIP_DEPLOYMENT_MODE:-authenticated}"
|
|
PAPERCLIP_DEPLOYMENT_EXPOSURE="${PAPERCLIP_DEPLOYMENT_EXPOSURE:-private}"
|
|
PAPERCLIP_PUBLIC_URL="${PAPERCLIP_PUBLIC_URL:-http://localhost:${HOST_PORT}}"
|
|
SMOKE_AUTO_BOOTSTRAP="${SMOKE_AUTO_BOOTSTRAP:-true}"
|
|
# Seconds to wait for /api/health after the container starts. The container
|
|
# cold-installs paperclipai from npm and initializes embedded postgres before
|
|
# it can serve health, so CI callers with no warm caches need far more than
|
|
# the local default.
|
|
SMOKE_READY_TIMEOUT_SECONDS="${SMOKE_READY_TIMEOUT_SECONDS:-90}"
|
|
SMOKE_ADMIN_NAME="${SMOKE_ADMIN_NAME:-Smoke Admin}"
|
|
SMOKE_ADMIN_EMAIL="${SMOKE_ADMIN_EMAIL:-smoke-admin@paperclip.local}"
|
|
SMOKE_ADMIN_PASSWORD="${SMOKE_ADMIN_PASSWORD:-paperclip-smoke-password}"
|
|
# Overridable so a caller can fix the name before this script runs. CI needs
|
|
# that: a name it only learns from this script's output is a name it does not
|
|
# have when this script fails, which is precisely when its diagnostics steps
|
|
# need one.
|
|
CONTAINER_NAME="${SMOKE_CONTAINER_NAME:-$IMAGE_NAME}"
|
|
CONTAINER_NAME="${CONTAINER_NAME//[^a-zA-Z0-9_.-]/-}"
|
|
# Where the container's logs are written before it is torn down. See
|
|
# `dump_container_logs`.
|
|
SMOKE_LOG_FILE="${SMOKE_LOG_FILE:-${TMPDIR:-/tmp}/${CONTAINER_NAME}.log}"
|
|
LOG_PID=""
|
|
COOKIE_JAR=""
|
|
TMP_DIR=""
|
|
PRESERVE_CONTAINER_ON_EXIT="false"
|
|
|
|
mkdir -p "$DATA_DIR"
|
|
|
|
# Start from an empty dump. `dump_container_logs` only writes when there is a
|
|
# container to read, so a run that fails before one exists — a failed build, a
|
|
# port already bound — would otherwise leave the previous run's file in place,
|
|
# and that file would be read as this run's diagnostics. Truncated rather than
|
|
# removed, so the path is present and writable from here on.
|
|
if [[ -n "$SMOKE_LOG_FILE" ]]; then
|
|
mkdir -p "$(dirname "$SMOKE_LOG_FILE")" >/dev/null 2>&1 || true
|
|
: >"$SMOKE_LOG_FILE" 2>/dev/null || true
|
|
fi
|
|
|
|
# Copy the container's logs out while there is still a container to read them
|
|
# from.
|
|
#
|
|
# This runs on every failure path — the image failing to serve, health never
|
|
# coming up, bootstrap rejecting the admin — which is exactly when the logs are
|
|
# the only account of what went wrong, and exactly when they used to be
|
|
# destroyed unread: `docker run` passed `--rm`, so the container and its logs
|
|
# went away with the stop below (and, for a container that crashed on its own,
|
|
# the moment its process exited). `--rm` is gone for that reason; removal is
|
|
# this script's job now, and it happens after the dump.
|
|
dump_container_logs() {
|
|
if [[ -z "$SMOKE_LOG_FILE" ]]; then
|
|
return 0
|
|
fi
|
|
if ! docker inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
mkdir -p "$(dirname "$SMOKE_LOG_FILE")" >/dev/null 2>&1 || return 0
|
|
docker logs "$CONTAINER_NAME" >"$SMOKE_LOG_FILE" 2>&1 || true
|
|
}
|
|
|
|
cleanup() {
|
|
if [[ -n "$LOG_PID" ]]; then
|
|
kill "$LOG_PID" >/dev/null 2>&1 || true
|
|
fi
|
|
# Before the teardown below, never after it.
|
|
dump_container_logs
|
|
if [[ "$PRESERVE_CONTAINER_ON_EXIT" != "true" ]]; then
|
|
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
fi
|
|
if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then
|
|
rm -rf "$TMP_DIR"
|
|
fi
|
|
}
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
container_is_running() {
|
|
local running
|
|
running="$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null || true)"
|
|
[[ "$running" == "true" ]]
|
|
}
|
|
|
|
wait_for_http() {
|
|
local url="$1"
|
|
local attempts="${2:-60}"
|
|
local sleep_seconds="${3:-1}"
|
|
local i
|
|
for ((i = 1; i <= attempts; i += 1)); do
|
|
if curl -fsS "$url" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
if ! container_is_running; then
|
|
echo "Smoke bootstrap failed: container $CONTAINER_NAME exited before $url became ready" >&2
|
|
docker logs "$CONTAINER_NAME" >&2 || true
|
|
return 1
|
|
fi
|
|
sleep "$sleep_seconds"
|
|
done
|
|
if ! container_is_running; then
|
|
echo "Smoke bootstrap failed: container $CONTAINER_NAME exited before readiness check completed" >&2
|
|
docker logs "$CONTAINER_NAME" >&2 || true
|
|
else
|
|
echo "Smoke bootstrap failed: $url not ready after ${attempts} attempts; container is still running. Last container logs:" >&2
|
|
docker logs --tail 150 "$CONTAINER_NAME" >&2 || true
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
write_metadata_file() {
|
|
if [[ -z "$SMOKE_METADATA_FILE" ]]; then
|
|
return 0
|
|
fi
|
|
mkdir -p "$(dirname "$SMOKE_METADATA_FILE")"
|
|
{
|
|
printf 'SMOKE_BASE_URL=%q\n' "$PAPERCLIP_PUBLIC_URL"
|
|
printf 'SMOKE_ADMIN_EMAIL=%q\n' "$SMOKE_ADMIN_EMAIL"
|
|
printf 'SMOKE_ADMIN_PASSWORD=%q\n' "$SMOKE_ADMIN_PASSWORD"
|
|
printf 'SMOKE_CONTAINER_NAME=%q\n' "$CONTAINER_NAME"
|
|
printf 'SMOKE_LOG_FILE=%q\n' "$SMOKE_LOG_FILE"
|
|
printf 'SMOKE_DATA_DIR=%q\n' "$DATA_DIR"
|
|
printf 'SMOKE_IMAGE_NAME=%q\n' "$IMAGE_NAME"
|
|
printf 'SMOKE_PAPERCLIPAI_VERSION=%q\n' "$PAPERCLIPAI_VERSION"
|
|
} >"$SMOKE_METADATA_FILE"
|
|
}
|
|
|
|
generate_bootstrap_invite_url() {
|
|
local bootstrap_output
|
|
local bootstrap_status
|
|
if bootstrap_output="$(
|
|
docker exec \
|
|
-e PAPERCLIP_DEPLOYMENT_MODE="$PAPERCLIP_DEPLOYMENT_MODE" \
|
|
-e PAPERCLIP_DEPLOYMENT_EXPOSURE="$PAPERCLIP_DEPLOYMENT_EXPOSURE" \
|
|
-e PAPERCLIP_PUBLIC_URL="$PAPERCLIP_PUBLIC_URL" \
|
|
-e PAPERCLIP_HOME="/paperclip" \
|
|
"$CONTAINER_NAME" bash -lc \
|
|
'timeout 20s npx --yes "paperclipai@${PAPERCLIPAI_VERSION}" auth bootstrap-ceo --data-dir "$PAPERCLIP_HOME" --base-url "$PAPERCLIP_PUBLIC_URL"' \
|
|
2>&1
|
|
)"; then
|
|
bootstrap_status=0
|
|
else
|
|
bootstrap_status=$?
|
|
fi
|
|
|
|
if [[ $bootstrap_status -ne 0 && $bootstrap_status -ne 124 ]]; then
|
|
echo "Smoke bootstrap failed: could not run bootstrap-ceo inside container" >&2
|
|
printf '%s\n' "$bootstrap_output" >&2
|
|
return 1
|
|
fi
|
|
|
|
local invite_url
|
|
invite_url="$(
|
|
printf '%s\n' "$bootstrap_output" \
|
|
| grep -o 'https\?://[^[:space:]]*/invite/pcp_bootstrap_[[:alnum:]]*' \
|
|
| tail -n 1
|
|
)"
|
|
|
|
if [[ -z "$invite_url" ]]; then
|
|
echo "Smoke bootstrap failed: bootstrap-ceo did not print an invite URL" >&2
|
|
printf '%s\n' "$bootstrap_output" >&2
|
|
return 1
|
|
fi
|
|
|
|
if [[ $bootstrap_status -eq 124 ]]; then
|
|
echo " Smoke bootstrap: bootstrap-ceo timed out after printing invite URL; continuing" >&2
|
|
fi
|
|
|
|
printf '%s\n' "$invite_url"
|
|
}
|
|
|
|
post_json_with_cookies() {
|
|
local url="$1"
|
|
local body="$2"
|
|
local output_file="$3"
|
|
curl -sS \
|
|
-o "$output_file" \
|
|
-w "%{http_code}" \
|
|
-c "$COOKIE_JAR" \
|
|
-b "$COOKIE_JAR" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Origin: $PAPERCLIP_PUBLIC_URL" \
|
|
-X POST \
|
|
"$url" \
|
|
--data "$body"
|
|
}
|
|
|
|
get_with_cookies() {
|
|
local url="$1"
|
|
curl -fsS \
|
|
-c "$COOKIE_JAR" \
|
|
-b "$COOKIE_JAR" \
|
|
-H "Accept: application/json" \
|
|
"$url"
|
|
}
|
|
|
|
sign_up_or_sign_in() {
|
|
local signup_response="$TMP_DIR/signup.json"
|
|
local signup_status
|
|
signup_status="$(post_json_with_cookies \
|
|
"$PAPERCLIP_PUBLIC_URL/api/auth/sign-up/email" \
|
|
"{\"name\":\"$SMOKE_ADMIN_NAME\",\"email\":\"$SMOKE_ADMIN_EMAIL\",\"password\":\"$SMOKE_ADMIN_PASSWORD\"}" \
|
|
"$signup_response")"
|
|
if [[ "$signup_status" =~ ^2 ]]; then
|
|
echo " Smoke bootstrap: created admin user $SMOKE_ADMIN_EMAIL"
|
|
return 0
|
|
fi
|
|
|
|
local signin_response="$TMP_DIR/signin.json"
|
|
local signin_status
|
|
signin_status="$(post_json_with_cookies \
|
|
"$PAPERCLIP_PUBLIC_URL/api/auth/sign-in/email" \
|
|
"{\"email\":\"$SMOKE_ADMIN_EMAIL\",\"password\":\"$SMOKE_ADMIN_PASSWORD\"}" \
|
|
"$signin_response")"
|
|
if [[ "$signin_status" =~ ^2 ]]; then
|
|
echo " Smoke bootstrap: signed in existing admin user $SMOKE_ADMIN_EMAIL"
|
|
return 0
|
|
fi
|
|
|
|
echo "Smoke bootstrap failed: could not sign up or sign in admin user" >&2
|
|
echo "Sign-up response:" >&2
|
|
cat "$signup_response" >&2 || true
|
|
echo >&2
|
|
echo "Sign-in response:" >&2
|
|
cat "$signin_response" >&2 || true
|
|
echo >&2
|
|
return 1
|
|
}
|
|
|
|
auto_bootstrap_authenticated_smoke() {
|
|
local health_url="$PAPERCLIP_PUBLIC_URL/api/health"
|
|
local health_json
|
|
health_json="$(curl -fsS "$health_url")"
|
|
if [[ "$health_json" != *'"deploymentMode":"authenticated"'* ]]; then
|
|
return 0
|
|
fi
|
|
|
|
sign_up_or_sign_in
|
|
|
|
if [[ "$health_json" == *'"bootstrapStatus":"ready"'* ]]; then
|
|
echo " Smoke bootstrap: instance already ready"
|
|
else
|
|
local invite_url
|
|
invite_url="$(generate_bootstrap_invite_url)"
|
|
echo " Smoke bootstrap: generated bootstrap invite via auth bootstrap-ceo"
|
|
|
|
local invite_token="${invite_url##*/}"
|
|
local accept_response="$TMP_DIR/accept.json"
|
|
local accept_status
|
|
accept_status="$(post_json_with_cookies \
|
|
"$PAPERCLIP_PUBLIC_URL/api/invites/$invite_token/accept" \
|
|
'{"requestType":"human"}' \
|
|
"$accept_response")"
|
|
if [[ ! "$accept_status" =~ ^2 ]]; then
|
|
echo "Smoke bootstrap failed: bootstrap invite acceptance returned HTTP $accept_status" >&2
|
|
cat "$accept_response" >&2 || true
|
|
echo >&2
|
|
return 1
|
|
fi
|
|
echo " Smoke bootstrap: accepted bootstrap invite"
|
|
fi
|
|
|
|
local session_json
|
|
session_json="$(get_with_cookies "$PAPERCLIP_PUBLIC_URL/api/auth/get-session")"
|
|
if [[ "$session_json" != *'"userId"'* ]]; then
|
|
echo "Smoke bootstrap failed: no authenticated session after bootstrap" >&2
|
|
echo "$session_json" >&2
|
|
return 1
|
|
fi
|
|
|
|
local companies_json
|
|
companies_json="$(get_with_cookies "$PAPERCLIP_PUBLIC_URL/api/companies")"
|
|
if [[ "${companies_json:0:1}" != "[" ]]; then
|
|
echo "Smoke bootstrap failed: board companies endpoint did not return JSON array" >&2
|
|
echo "$companies_json" >&2
|
|
return 1
|
|
fi
|
|
|
|
echo " Smoke bootstrap: board session verified"
|
|
echo " Smoke admin credentials: $SMOKE_ADMIN_EMAIL / $SMOKE_ADMIN_PASSWORD"
|
|
}
|
|
|
|
echo "==> Building onboard smoke image"
|
|
docker build \
|
|
--build-arg PAPERCLIPAI_VERSION="$PAPERCLIPAI_VERSION" \
|
|
--build-arg HOST_UID="$HOST_UID" \
|
|
-f "$REPO_ROOT/docker/Dockerfile.onboard-smoke" \
|
|
-t "$IMAGE_NAME" \
|
|
"$REPO_ROOT"
|
|
|
|
echo "==> Running onboard smoke container"
|
|
echo " UI should be reachable at: http://localhost:$HOST_PORT"
|
|
echo " Public URL: $PAPERCLIP_PUBLIC_URL"
|
|
echo " Smoke auto-bootstrap: $SMOKE_AUTO_BOOTSTRAP"
|
|
echo " Detached mode: $SMOKE_DETACH"
|
|
echo " Data dir: $DATA_DIR"
|
|
echo " Container name: $CONTAINER_NAME"
|
|
echo " Container log dump: $SMOKE_LOG_FILE"
|
|
echo " Deployment: $PAPERCLIP_DEPLOYMENT_MODE/$PAPERCLIP_DEPLOYMENT_EXPOSURE"
|
|
if [[ "$SMOKE_DETACH" != "true" ]]; then
|
|
echo " Live output: onboard banner and server logs stream in this terminal (Ctrl+C to stop)"
|
|
fi
|
|
|
|
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
|
|
# No `--rm`. A container that removes itself takes its logs with it the instant
|
|
# it exits, which is the one moment they are worth reading; the cleanup above
|
|
# removes it instead, after dumping them. The `docker rm -f` just above covers
|
|
# a container left behind by a previous run.
|
|
docker run -d \
|
|
--name "$CONTAINER_NAME" \
|
|
-p "$HOST_PORT:3100" \
|
|
-e HOST=0.0.0.0 \
|
|
-e PORT=3100 \
|
|
-e PAPERCLIP_DEPLOYMENT_MODE="$PAPERCLIP_DEPLOYMENT_MODE" \
|
|
-e PAPERCLIP_DEPLOYMENT_EXPOSURE="$PAPERCLIP_DEPLOYMENT_EXPOSURE" \
|
|
-e PAPERCLIP_PUBLIC_URL="$PAPERCLIP_PUBLIC_URL" \
|
|
-v "$DATA_DIR:/paperclip" \
|
|
"$IMAGE_NAME" >/dev/null
|
|
|
|
if [[ "$SMOKE_DETACH" != "true" ]]; then
|
|
docker logs -f "$CONTAINER_NAME" &
|
|
LOG_PID=$!
|
|
fi
|
|
|
|
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/paperclip-onboard-smoke.XXXXXX")"
|
|
COOKIE_JAR="$TMP_DIR/cookies.txt"
|
|
|
|
if ! wait_for_http "$PAPERCLIP_PUBLIC_URL/api/health" "$SMOKE_READY_TIMEOUT_SECONDS" 1; then
|
|
echo "Smoke bootstrap failed: server did not become ready at $PAPERCLIP_PUBLIC_URL/api/health" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$SMOKE_AUTO_BOOTSTRAP" == "true" && "$PAPERCLIP_DEPLOYMENT_MODE" == "authenticated" ]]; then
|
|
auto_bootstrap_authenticated_smoke
|
|
fi
|
|
|
|
write_metadata_file
|
|
|
|
if [[ "$SMOKE_DETACH" == "true" ]]; then
|
|
PRESERVE_CONTAINER_ON_EXIT="true"
|
|
echo "==> Smoke container ready for automation"
|
|
echo " Smoke base URL: $PAPERCLIP_PUBLIC_URL"
|
|
echo " Smoke admin credentials: $SMOKE_ADMIN_EMAIL / $SMOKE_ADMIN_PASSWORD"
|
|
if [[ -n "$SMOKE_METADATA_FILE" ]]; then
|
|
echo " Smoke metadata file: $SMOKE_METADATA_FILE"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
wait "$LOG_PID"
|