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: ubuntu-24.04
    env:
      DOCKER_BUILD_SUMMARY: "false"
      DOCKER_BUILD_RECORD_UPLOAD: "false"
    steps:
      - name: Checkout CLI
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.ref || github.ref }}
          persist-credentials: false

      - 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 --kill-after=30s 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: |
          timeout --kill-after=30s 20m 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: |
          timeout --kill-after=30s 20m 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: ubuntu-24.04
    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 }}
          persist-credentials: false

      - 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 --kill-after=30s 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 --kill-after=30s 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: ubuntu-24.04
    steps:
      - name: Checkout CLI
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.ref || github.ref }}
          persist-credentials: false

      - 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: ubuntu-24.04
    steps:
      - name: Checkout CLI
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.ref || github.ref }}
          persist-credentials: false

      - 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 --kill-after=30s 600s docker pull "$IMAGE_REF"

      - name: Run root Dockerfile CLI smoke
        env:
          IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
        run: |
          timeout --kill-after=30s 20m 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: |
          timeout --kill-after=30s 20m 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: ubuntu-24.04
    env:
      DOCKER_BUILD_SUMMARY: "false"
      DOCKER_BUILD_RECORD_UPLOAD: "false"
    steps:
      - name: Checkout CLI
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.ref || github.ref }}
          persist-credentials: false

      - 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 --kill-after=30s 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 --kill-after=30s 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 --kill-after=30s 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: file:///tmp/openclaw-install.sh
          OPENCLAW_INSTALL_CLI_URL: file:///tmp/openclaw-install-cli.sh
          OPENCLAW_NO_ONBOARD: "1"
          OPENCLAW_INSTALL_SMOKE_SKIP_CLI: "0"
          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

      - name: Run Rocky Linux installer smoke
        run: |
          timeout --kill-after=30s 20m docker run --rm \
            -e OPENCLAW_NO_ONBOARD=1 \
            -e OPENCLAW_NO_PROMPT=1 \
            -v "$PWD/scripts/install.sh:/tmp/install.sh:ro" \
            rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
            bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install.sh --install-method npm --version latest --no-onboard --no-prompt --verify && openclaw --version'

      - name: Run Rocky Linux CLI installer smoke
        run: |
          timeout --kill-after=30s 20m docker run --rm \
            -e OPENCLAW_NO_ONBOARD=1 \
            -e OPENCLAW_NO_PROMPT=1 \
            -v "$PWD/scripts/install-cli.sh:/tmp/install-cli.sh:ro" \
            rockylinux:9@sha256:d7be1c094cc5845ee815d4632fe377514ee6ebcf8efaed6892889657e5ddaaa6 \
            bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install-cli.sh --prefix /tmp/openclaw-cli --version latest --no-onboard && /tmp/openclaw-cli/bin/openclaw --version'

  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: ubuntu-24.04
    steps:
      - name: Checkout CLI
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.ref || github.ref }}
          persist-credentials: false

      - 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 --kill-after=30s 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: ubuntu-24.04
    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 }}
          persist-credentials: false

      - 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"
