diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml index 6429d9ef1c..073be39d38 100644 --- a/.github/workflows/release-smoke.yml +++ b/.github/workflows/release-smoke.yml @@ -70,6 +70,7 @@ jobs: HOST_PORT="${{ inputs.host_port }}" \ DATA_DIR="$RUNNER_TEMP/release-smoke-data" \ PAPERCLIPAI_VERSION="${{ inputs.paperclip_version }}" \ + SMOKE_READY_TIMEOUT_SECONDS=420 \ SMOKE_DETACH=true \ SMOKE_METADATA_FILE="$metadata_file" \ ./scripts/docker-onboard-smoke.sh diff --git a/scripts/__tests__/release-verify-workflow.test.mjs b/scripts/__tests__/release-verify-workflow.test.mjs index b5911ab4e8..841a002a28 100644 --- a/scripts/__tests__/release-verify-workflow.test.mjs +++ b/scripts/__tests__/release-verify-workflow.test.mjs @@ -17,13 +17,30 @@ test("release workflow delegates stable and canary verification to the reusable releaseWorkflow, /verify_canary:\n\s+if: github\.event_name == 'push'\n\s+uses: \.\/\.github\/workflows\/release-verify\.yml\n\s+with:\n\s+ref: \$\{\{ github\.sha \}\}/, ); + // The stable lane is gated on the stable channel since the nightly lane + // was added; a `needs:` line (for example a preflight job) may sit between + // the gate and the delegation. assert.match( releaseWorkflow, - /verify_stable:\n\s+if: github\.event_name == 'workflow_dispatch'\n\s+uses: \.\/\.github\/workflows\/release-verify\.yml\n\s+with:\n\s+ref: \$\{\{ inputs\.source_ref \}\}/, + /verify_stable:\n\s+if: github\.event_name == 'workflow_dispatch' && inputs\.channel == 'stable'\n(?:\s+needs: [^\n]+\n)?\s+uses: \.\/\.github\/workflows\/release-verify\.yml\n\s+with:\n\s+ref: \$\{\{ inputs\.source_ref \}\}/, ); assert.doesNotMatch(releaseWorkflow, /verify_(?:canary|stable):[\s\S]*?pnpm test:run(?:\n|$)/); }); +test("release smoke workflow extends the container readiness budget for CI", () => { + const smokeWorkflow = readWorkflow("release-smoke.yml"); + const harness = readFileSync(path.join(repoRoot, "scripts/docker-onboard-smoke.sh"), "utf8"); + + // CI containers cold-install paperclipai and embedded postgres, so the + // workflow must extend the harness's local-default readiness budget. + assert.match(smokeWorkflow, /SMOKE_READY_TIMEOUT_SECONDS=\d+/); + const ciBudget = Number(smokeWorkflow.match(/SMOKE_READY_TIMEOUT_SECONDS=(\d+)/)[1]); + assert.ok(ciBudget >= 300, `CI readiness budget ${ciBudget}s should be at least 300s`); + + assert.match(harness, /SMOKE_READY_TIMEOUT_SECONDS="\$\{SMOKE_READY_TIMEOUT_SECONDS:-\d+\}"/); + assert.match(harness, /wait_for_http "\$PAPERCLIP_PUBLIC_URL\/api\/health" "\$SMOKE_READY_TIMEOUT_SECONDS" 1/); +}); + test("release verify workflow covers the same split test surface as stable PR verification", () => { const verifyWorkflow = readWorkflow("release-verify.yml"); diff --git a/scripts/docker-onboard-smoke.sh b/scripts/docker-onboard-smoke.sh index c2830ff0f5..548931b557 100755 --- a/scripts/docker-onboard-smoke.sh +++ b/scripts/docker-onboard-smoke.sh @@ -13,6 +13,11 @@ 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}" @@ -63,6 +68,9 @@ wait_for_http() { 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 } @@ -278,7 +286,7 @@ 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" 90 1; then +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