name: Release Smoke on: workflow_dispatch: inputs: paperclip_version: description: Published Paperclip dist-tag to test required: true default: canary type: choice options: - canary - nightly - beta - latest host_port: description: Host port for the Docker smoke container required: false default: "3232" type: string artifact_name: description: Artifact name for uploaded diagnostics required: false default: release-smoke type: string workflow_call: inputs: paperclip_version: required: true type: string host_port: required: false default: "3232" type: string artifact_name: required: false default: release-smoke type: string jobs: # The Docker smoke below can never exercise the background-service leg of # onboarding: containers have no service manager, so v2026.824.0 shipped a # service install that crash-looped on a missing shim while every # golden-path check stayed green. Run the same published artifact directly # on the runner VM's systemd and require the installed service to end up # serving. smoke_service: runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@v7 - name: Setup Node.js uses: actions/setup-node@v7 with: node-version: 24 - name: Start a user systemd session # The hosted runner has no login session for the runner user, so # `systemctl --user` cannot reach a user manager until lingering # starts one. Export the session address for the steps below. run: | sudo loginctl enable-linger "$(id -un)" uid="$(id -u)" for _ in $(seq 1 30); do [[ -S "/run/user/$uid/bus" ]] && break sleep 1 done [[ -S "/run/user/$uid/bus" ]] { echo "XDG_RUNTIME_DIR=/run/user/$uid" echo "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus" } >> "$GITHUB_ENV" - name: Onboard with the background service env: PAPERCLIPAI_VERSION: ${{ inputs.paperclip_version }} DATA_DIR: ${{ runner.temp }}/service-smoke-data SMOKE_CLEANUP: "false" run: ./scripts/service-onboard-smoke.sh - name: Capture service diagnostics if: always() run: | { systemctl --user --no-pager status paperclipai.service || true journalctl --user -u paperclipai.service --no-pager || true } > "$RUNNER_TEMP/paperclipai-service.log" 2>&1 - name: Upload service diagnostics if: always() uses: actions/upload-artifact@v7 with: name: ${{ inputs.artifact_name }}-service path: ${{ runner.temp }}/paperclipai-service.log retention-days: 14 smoke: runs-on: ubuntu-latest timeout-minutes: 45 # Fixed here rather than read back out of the harness, so the `always()` # diagnostics steps below still know the container's name when the launch # step is the thing that failed. Reading it back was why a failing smoke # uploaded no Docker logs at all. env: SMOKE_CONTAINER_NAME: release-smoke-onboard steps: - name: Checkout repository uses: actions/checkout@v7 - name: Setup pnpm uses: pnpm/action-setup@v6 with: version: 9.15.4 - name: Setup Node.js uses: actions/setup-node@v7 with: node-version: 24 cache: pnpm - name: Install dependencies run: pnpm install --no-frozen-lockfile - name: Verify runner Chrome # Release smoke also runs headless on GitHub's Ubuntu image, so use the # runner's preinstalled Chrome instead of a Playwright browser download. run: google-chrome --version - name: Launch Docker smoke harness run: | 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="${{ runner.temp }}/release-smoke.env" \ SMOKE_LOG_FILE="${{ runner.temp }}/docker-onboard-smoke.log" \ ./scripts/docker-onboard-smoke.sh set -a source "${{ runner.temp }}/release-smoke.env" set +a { echo "SMOKE_BASE_URL=$SMOKE_BASE_URL" echo "SMOKE_ADMIN_EMAIL=$SMOKE_ADMIN_EMAIL" echo "SMOKE_ADMIN_PASSWORD=$SMOKE_ADMIN_PASSWORD" echo "SMOKE_DATA_DIR=$SMOKE_DATA_DIR" echo "SMOKE_IMAGE_NAME=$SMOKE_IMAGE_NAME" echo "SMOKE_PAPERCLIPAI_VERSION=$SMOKE_PAPERCLIPAI_VERSION" } >> "$GITHUB_ENV" - name: Run release smoke Playwright suite env: PAPERCLIP_RELEASE_SMOKE_BASE_URL: ${{ env.SMOKE_BASE_URL }} PAPERCLIP_RELEASE_SMOKE_EMAIL: ${{ env.SMOKE_ADMIN_EMAIL }} PAPERCLIP_RELEASE_SMOKE_PASSWORD: ${{ env.SMOKE_ADMIN_PASSWORD }} PAPERCLIP_PLAYWRIGHT_CHANNEL: "chrome" run: pnpm run test:release-smoke - name: Capture Docker logs if: always() run: | log_file="${{ runner.temp }}/docker-onboard-smoke.log" # A live container has the fuller story, so prefer it. When the # harness already tore the container down it wrote this file on its # way out, and that copy is kept rather than clobbered. if docker inspect "$SMOKE_CONTAINER_NAME" >/dev/null 2>&1; then docker logs "$SMOKE_CONTAINER_NAME" >"$log_file" 2>&1 || true fi # Never leave the upload with nothing to say. An absent log reads as # a missing artifact; a file saying the container was gone reads as # the diagnosis it is. if [[ ! -s "$log_file" ]]; then echo "No Docker logs captured: container '$SMOKE_CONTAINER_NAME' left no log dump and is no longer present." >"$log_file" fi echo "Captured $(wc -l <"$log_file") log lines to $log_file" - name: Upload diagnostics if: always() uses: actions/upload-artifact@v7 with: name: ${{ inputs.artifact_name }} path: | ${{ runner.temp }}/docker-onboard-smoke.log ${{ runner.temp }}/release-smoke.env tests/release-smoke/playwright-report/ tests/release-smoke/test-results/ # The capture step above guarantees the log file, so an empty upload # means the diagnostics wiring itself broke — which is worth failing # over rather than burying in a warning nobody reads. if-no-files-found: error retention-days: 14 - name: Stop Docker smoke container if: always() run: docker rm -f "$SMOKE_CONTAINER_NAME" >/dev/null 2>&1 || true