name: Install Smoke on: schedule: - cron: "17 3 * * *" workflow_dispatch: inputs: run_bun_global_install_smoke: description: Run the Bun global install image-provider smoke required: false default: false type: boolean update_baseline_version: description: Baseline openclaw version or dist-tag for installer update smoke required: false default: latest type: string workflow_call: inputs: ref: description: Git ref to validate required: false type: string run_bun_global_install_smoke: description: Run the Bun global install image-provider smoke required: false default: true type: boolean update_baseline_version: description: Baseline openclaw version or dist-tag for installer update smoke required: false default: latest type: string permissions: contents: read packages: write concurrency: group: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && format('{0}-{1}-{2}', github.workflow, github.event_name, github.run_id) || format('{0}-{1}', github.workflow, github.ref) }} cancel-in-progress: ${{ github.event_name != 'workflow_call' }} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" jobs: preflight: runs-on: ubuntu-24.04 outputs: docs_only: ${{ steps.manifest.outputs.docs_only }} run_install_smoke: ${{ steps.manifest.outputs.run_install_smoke }} run_fast_install_smoke: ${{ steps.manifest.outputs.run_fast_install_smoke }} run_full_install_smoke: ${{ steps.manifest.outputs.run_full_install_smoke }} run_bun_global_install_smoke: ${{ steps.manifest.outputs.run_bun_global_install_smoke }} target_sha: ${{ steps.manifest.outputs.target_sha }} dockerfile_image: ${{ steps.manifest.outputs.dockerfile_image }} steps: - name: Checkout uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} fetch-depth: 1 fetch-tags: false persist-credentials: false submodules: false - name: Build install-smoke CI manifest id: manifest env: OPENCLAW_CI_EVENT_NAME: ${{ github.event_name }} OPENCLAW_CI_WORKFLOW_BUN_GLOBAL_INSTALL_SMOKE: ${{ inputs.run_bun_global_install_smoke || 'false' }} run: | event_name="${OPENCLAW_CI_EVENT_NAME:-}" workflow_bun_global_install_smoke="${OPENCLAW_CI_WORKFLOW_BUN_GLOBAL_INSTALL_SMOKE:-false}" docs_only=false run_fast_install_smoke=true run_full_install_smoke=true run_bun_global_install_smoke=false run_install_smoke=true target_sha="$(git rev-parse HEAD)" owner="$(printf '%s' "${GITHUB_REPOSITORY_OWNER:-openclaw}" | tr '[:upper:]' '[:lower:]')" dockerfile_image="ghcr.io/${owner}/openclaw-dockerfile-smoke:${target_sha}" if [ "$event_name" = "schedule" ]; then run_bun_global_install_smoke=true elif [ "$event_name" = "workflow_dispatch" ] || [ "$event_name" = "workflow_call" ]; then if [ "$workflow_bun_global_install_smoke" = "true" ]; then run_bun_global_install_smoke=true fi fi { echo "docs_only=$docs_only" echo "run_install_smoke=$run_install_smoke" echo "run_fast_install_smoke=$run_fast_install_smoke" echo "run_full_install_smoke=$run_full_install_smoke" echo "run_bun_global_install_smoke=$run_bun_global_install_smoke" echo "target_sha=$target_sha" echo "dockerfile_image=$dockerfile_image" } >> "$GITHUB_OUTPUT" install-smoke-fast: needs: [preflight] if: needs.preflight.outputs.run_fast_install_smoke == 'true' && needs.preflight.outputs.run_full_install_smoke != 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 env: DOCKER_BUILD_SUMMARY: "false" DOCKER_BUILD_RECORD_UPLOAD: "false" steps: - name: Checkout CLI uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - name: Set up Blacksmith Docker Builder uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1 with: max-cache-size-mb: 800000 # Keep release smoke builds bounded and log-producing. The Blacksmith # build action can leave jobs in-progress without step logs when a remote # builder stalls; an explicit buildx invocation fails closed instead. - name: Build root Dockerfile smoke image run: | timeout 45m docker buildx build \ --progress=plain \ --load \ --build-arg OPENCLAW_EXTENSIONS=matrix \ -t openclaw-dockerfile-smoke:local \ -t openclaw-ext-smoke:local \ -f ./Dockerfile \ . - name: Run root Dockerfile CLI smoke run: | docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc ' which openclaw && openclaw --version && node -e " const fs = require(\"node:fs\"); const path = require(\"node:path\"); const YAML = require(\"yaml\"); const workspace = YAML.parse(fs.readFileSync(\"/app/pnpm-workspace.yaml\", \"utf8\")) ?? {}; for (const [dep, rel] of Object.entries(workspace.patchedDependencies ?? {})) { const absolute = path.join(\"/app\", rel); if (!fs.existsSync(absolute)) { throw new Error(`missing patch for ${dep}: ${rel}`); } } " ' - name: Run agents delete shared workspace Docker CLI smoke env: OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE: openclaw-dockerfile-smoke:local OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD: "1" run: bash scripts/e2e/agents-delete-shared-workspace-docker.sh - name: Run Docker gateway network e2e env: OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE: openclaw-dockerfile-smoke:local OPENCLAW_GATEWAY_NETWORK_E2E_SKIP_BUILD: "1" run: bash scripts/e2e/gateway-network-docker.sh - name: Smoke test Dockerfile with matrix extension build arg run: | docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc ' which openclaw && openclaw --version && node -e " const Module = require(\"node:module\"); const matrixPackage = require(\"/app/extensions/matrix/package.json\"); const requireFromMatrix = Module.createRequire(\"/app/extensions/matrix/package.json\"); const runtimeDeps = Object.keys(matrixPackage.dependencies ?? {}); if (runtimeDeps.length === 0) { throw new Error( \"matrix package has no declared runtime dependencies; smoke cannot validate install mirroring\", ); } for (const dep of runtimeDeps) { requireFromMatrix.resolve(dep); } const { spawnSync } = require(\"node:child_process\"); const run = spawnSync(\"openclaw\", [\"plugins\", \"list\", \"--json\"], { encoding: \"utf8\" }); if (run.status !== 0) { process.stderr.write(run.stderr || run.stdout || \"plugins list failed\\n\"); process.exit(run.status ?? 1); } const parsed = JSON.parse(run.stdout); const matrix = (parsed.plugins || []).find((entry) => entry.id === \"matrix\"); if (!matrix) { throw new Error(\"matrix plugin missing from bundled plugin list\"); } const matrixDiag = (parsed.diagnostics || []).filter( (diag) => typeof diag.source === \"string\" && diag.source.includes(\"/extensions/matrix\") && typeof diag.message === \"string\" && diag.message.includes(\"extension entry escapes package directory\"), ); if (matrixDiag.length > 0) { throw new Error( \"unexpected matrix diagnostics: \" + matrixDiag.map((diag) => diag.message).join(\"; \"), ); } " ' root_dockerfile_image: needs: [preflight] if: needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 outputs: image_ref: ${{ steps.image.outputs.image_ref }} env: DOCKER_BUILD_SUMMARY: "false" DOCKER_BUILD_RECORD_UPLOAD: "false" steps: - name: Checkout CLI uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - name: Log in to GHCR uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Check for existing root Dockerfile smoke image id: existing env: IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }} run: | set -euo pipefail if timeout 180s docker pull "$IMAGE_REF"; then echo "exists=true" >> "$GITHUB_OUTPUT" echo "Using existing root Dockerfile smoke image: \`$IMAGE_REF\`" >> "$GITHUB_STEP_SUMMARY" else echo "exists=false" >> "$GITHUB_OUTPUT" echo "No existing root Dockerfile smoke image found for \`$IMAGE_REF\`; building it." >> "$GITHUB_STEP_SUMMARY" fi - name: Set up Blacksmith Docker Builder if: steps.existing.outputs.exists != 'true' uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1 with: max-cache-size-mb: 800000 # Build once with the matrix extension and publish by target SHA. Use a # direct buildx command so release jobs emit Docker progress and time out. - name: Build and push root Dockerfile smoke image if: steps.existing.outputs.exists != 'true' env: IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }} run: | timeout 45m docker buildx build \ --progress=plain \ --push \ --build-arg OPENCLAW_EXTENSIONS=matrix \ -t "$IMAGE_REF" \ -f ./Dockerfile \ . - name: Record root image output id: image env: IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }} run: echo "image_ref=$IMAGE_REF" >> "$GITHUB_OUTPUT" - name: Summarize root image env: IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }} TARGET_SHA: ${{ needs.preflight.outputs.target_sha }} run: | { echo "## Root Dockerfile smoke image" echo echo "- Target SHA: \`${TARGET_SHA}\`" echo "- Image: \`${IMAGE_REF}\`" echo "- Reused existing image: \`${{ steps.existing.outputs.exists }}\`" } >> "$GITHUB_STEP_SUMMARY" qr_package_install_smoke: needs: [preflight] if: needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout CLI uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - name: Run QR package install smoke env: OPENCLAW_QR_SMOKE_FORCE_INSTALL: "1" run: bash scripts/e2e/qr-import-docker.sh root_dockerfile_smokes: needs: [preflight, root_dockerfile_image] if: needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout CLI uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - name: Log in to GHCR uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Pull root Dockerfile smoke image env: IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }} run: timeout 600s docker pull "$IMAGE_REF" - name: Run root Dockerfile CLI smoke env: IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }} run: | docker run --rm --entrypoint sh "$IMAGE_REF" -lc ' which openclaw && openclaw --version && node -e " const fs = require(\"node:fs\"); const path = require(\"node:path\"); const YAML = require(\"yaml\"); const workspace = YAML.parse(fs.readFileSync(\"/app/pnpm-workspace.yaml\", \"utf8\")) ?? {}; for (const [dep, rel] of Object.entries(workspace.patchedDependencies ?? {})) { const absolute = path.join(\"/app\", rel); if (!fs.existsSync(absolute)) { throw new Error(`missing patch for ${dep}: ${rel}`); } } " ' - name: Run agents delete shared workspace Docker CLI smoke env: OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }} OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD: "1" run: bash scripts/e2e/agents-delete-shared-workspace-docker.sh - name: Run Docker gateway network e2e env: OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }} OPENCLAW_GATEWAY_NETWORK_E2E_SKIP_BUILD: "1" run: bash scripts/e2e/gateway-network-docker.sh - name: Smoke test Dockerfile with matrix extension build arg env: IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }} run: | docker run --rm --entrypoint sh "$IMAGE_REF" -lc ' which openclaw && openclaw --version && node -e " const Module = require(\"node:module\"); const matrixPackage = require(\"/app/extensions/matrix/package.json\"); const requireFromMatrix = Module.createRequire(\"/app/extensions/matrix/package.json\"); const runtimeDeps = Object.keys(matrixPackage.dependencies ?? {}); if (runtimeDeps.length === 0) { throw new Error( \"matrix package has no declared runtime dependencies; smoke cannot validate install mirroring\", ); } for (const dep of runtimeDeps) { requireFromMatrix.resolve(dep); } const { spawnSync } = require(\"node:child_process\"); const run = spawnSync(\"openclaw\", [\"plugins\", \"list\", \"--json\"], { encoding: \"utf8\" }); if (run.status !== 0) { process.stderr.write(run.stderr || run.stdout || \"plugins list failed\\n\"); process.exit(run.status ?? 1); } const parsed = JSON.parse(run.stdout); const matrix = (parsed.plugins || []).find((entry) => entry.id === \"matrix\"); if (!matrix) { throw new Error(\"matrix plugin missing from bundled plugin list\"); } const matrixDiag = (parsed.diagnostics || []).filter( (diag) => typeof diag.source === \"string\" && diag.source.includes(\"/extensions/matrix\") && typeof diag.message === \"string\" && diag.message.includes(\"extension entry escapes package directory\"), ); if (matrixDiag.length > 0) { throw new Error( \"unexpected matrix diagnostics: \" + matrixDiag.map((diag) => diag.message).join(\"; \"), ); } " ' installer_smoke: needs: [preflight, root_dockerfile_image] if: needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 env: DOCKER_BUILD_SUMMARY: "false" DOCKER_BUILD_RECORD_UPLOAD: "false" steps: - name: Checkout CLI uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - name: Log in to GHCR uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Pull root Dockerfile smoke image env: IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }} run: timeout 600s docker pull "$IMAGE_REF" - name: Set up Blacksmith Docker Builder uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1 with: max-cache-size-mb: 800000 - name: Build installer smoke image run: | timeout 20m docker buildx build \ --progress=plain \ --load \ -t openclaw-install-smoke:local \ -f ./scripts/docker/install-sh-smoke/Dockerfile \ ./scripts/docker - name: Build installer non-root image run: | timeout 20m docker buildx build \ --progress=plain \ --load \ -t openclaw-install-nonroot:local \ -f ./scripts/docker/install-sh-nonroot/Dockerfile \ ./scripts/docker - name: Setup Node environment for installer smoke uses: ./.github/actions/setup-node-env with: install-bun: "false" install-deps: "true" - name: Run installer docker tests env: OPENCLAW_INSTALL_URL: https://openclaw.ai/install.sh OPENCLAW_INSTALL_CLI_URL: https://openclaw.ai/install-cli.sh OPENCLAW_NO_ONBOARD: "1" OPENCLAW_INSTALL_SMOKE_SKIP_CLI: "1" OPENCLAW_INSTALL_SMOKE_SKIP_IMAGE_BUILD: "1" OPENCLAW_INSTALL_NONROOT_SKIP_IMAGE_BUILD: "1" OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT: "0" OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1" OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS: "1" OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: ${{ inputs.update_baseline_version || 'latest' }} OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }} OPENCLAW_INSTALL_SMOKE_UPDATE_SKIP_LOCAL_BUILD: "1" run: bash scripts/test-install-sh-docker.sh bun_global_install_smoke: needs: [preflight, root_dockerfile_image] if: needs.preflight.outputs.run_full_install_smoke == 'true' && needs.preflight.outputs.run_bun_global_install_smoke == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 steps: - name: Checkout CLI uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - name: Log in to GHCR uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Pull root Dockerfile smoke image env: IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }} run: timeout 600s docker pull "$IMAGE_REF" - name: Setup Node environment for Bun smoke uses: ./.github/actions/setup-node-env with: install-bun: "true" install-deps: "true" - name: Run Bun global install image-provider smoke env: OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }} OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD: "0" run: bash scripts/e2e/bun-global-install-smoke.sh docker-e2e-fast: needs: [preflight] if: needs.preflight.outputs.run_fast_install_smoke == 'true' || needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 12 env: DOCKER_BUILD_SUMMARY: "false" DOCKER_BUILD_RECORD_UPLOAD: "false" steps: - name: Checkout CLI uses: actions/checkout@v6 with: ref: ${{ inputs.ref || github.ref }} - name: Set up Blacksmith Docker Builder uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1 with: max-cache-size-mb: 800000 - name: Setup Node environment for package smoke uses: ./.github/actions/setup-node-env with: install-bun: "false" install-deps: "true"