From 098dfd470798bbf828b28ff86eace2f9213d3f15 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 21 Aug 2026 02:31:18 -0700 Subject: [PATCH] fix(ci): bind installer smoke artifacts --- .github/workflows/install-smoke-reusable.yml | 400 ++++++++++++++---- .../install-smoke-no-push-workflow.test.ts | 267 ++++++++---- .../package-acceptance-workflow.test.ts | 83 ++-- test/scripts/test-install-sh-docker.test.ts | 127 +++--- 4 files changed, 624 insertions(+), 253 deletions(-) diff --git a/.github/workflows/install-smoke-reusable.yml b/.github/workflows/install-smoke-reusable.yml index f992b828a20f..c82ad6532017 100644 --- a/.github/workflows/install-smoke-reusable.yml +++ b/.github/workflows/install-smoke-reusable.yml @@ -536,23 +536,20 @@ jobs: " ' - installer_smoke_image: + installer_smoke_update_image: needs: [preflight] if: needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: ubuntu-24.04 timeout-minutes: 45 - strategy: - fail-fast: false - matrix: - include: - - group: update - artifact_key: install-smoke-update - dockerfile: ./scripts/docker/install-sh-smoke/Dockerfile - image_ref: openclaw-install-smoke:local - - group: nonroot - artifact_key: install-smoke-nonroot - dockerfile: ./scripts/docker/install-sh-nonroot/Dockerfile - image_ref: openclaw-install-nonroot:local + outputs: + archive_sha256: ${{ steps.image_artifact.outputs.archive_sha256 }} + artifact_digest: ${{ steps.image_artifact_upload.outputs.artifact-digest }} + artifact_id: ${{ steps.image_artifact_upload.outputs.artifact-id }} + artifact_name: ${{ steps.image_artifact.outputs.artifact_name }} + artifact_run_attempt: ${{ steps.image_artifact.outputs.run_attempt }} + artifact_run_id: ${{ steps.image_artifact.outputs.run_id }} + target_sha: ${{ steps.image_artifact.outputs.target_sha }} + workflow_sha: ${{ steps.image_artifact.outputs.workflow_sha }} env: DOCKER_BUILD_SUMMARY: "false" DOCKER_BUILD_RECORD_UPLOAD: "false" @@ -571,35 +568,43 @@ jobs: - name: Build installer smoke image env: - DOCKERFILE: ${{ matrix.dockerfile }} - IMAGE_REF: ${{ matrix.image_ref }} + IMAGE_REF: openclaw-install-smoke:local run: | timeout --kill-after=30s 20m docker buildx build \ --progress=plain \ --load \ -t "$IMAGE_REF" \ - -f "$DOCKERFILE" \ + -f ./scripts/docker/install-sh-smoke/Dockerfile \ ./scripts/docker - name: Pack installer smoke image artifact id: image_artifact env: - ARTIFACT_KEY: ${{ matrix.artifact_key }} - IMAGE_REF: ${{ matrix.image_ref }} + IMAGE_REF: openclaw-install-smoke:local TARGET_SHA: ${{ needs.preflight.outputs.target_sha }} WORKFLOW_SHA: ${{ needs.preflight.outputs.workflow_sha }} run: | set -euo pipefail - artifact_dir="${RUNNER_TEMP}/${ARTIFACT_KEY}-image" - artifact_name="${ARTIFACT_KEY}-image-${TARGET_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + artifact_dir="${RUNNER_TEMP}/install-smoke-update-image" + artifact_name="install-smoke-update-image-${TARGET_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" bash scripts/docker/shared-image-artifact.sh \ - pack "$artifact_dir" "$ARTIFACT_KEY" "$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF" + pack "$artifact_dir" install-smoke-update "$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF" + archive_sha256="$( + jq -er '.archive.sha256 | select(type == "string" and test("^[a-f0-9]{64}$"))' \ + "$artifact_dir/shared-image-artifact.json" + )" { + echo "archive_sha256=$archive_sha256" echo "artifact_name=$artifact_name" echo "artifact_path=$artifact_dir" + echo "run_attempt=$GITHUB_RUN_ATTEMPT" + echo "run_id=$GITHUB_RUN_ID" + echo "target_sha=$TARGET_SHA" + echo "workflow_sha=$WORKFLOW_SHA" } >> "$GITHUB_OUTPUT" - name: Upload installer smoke image artifact + id: image_artifact_upload uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ steps.image_artifact.outputs.artifact_name }} @@ -608,23 +613,89 @@ jobs: compression-level: 0 retention-days: 7 - installer_smoke_group: - needs: [preflight, root_dockerfile_image, root_dockerfile_image_ready, installer_smoke_image] + installer_smoke_nonroot_image: + needs: [preflight] if: needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: ubuntu-24.04 - timeout-minutes: ${{ matrix.timeout_minutes }} - strategy: - fail-fast: false - matrix: - include: - - group: update - artifact_key: install-smoke-update - image_ref: openclaw-install-smoke:local - timeout_minutes: 120 - - group: nonroot - artifact_key: install-smoke-nonroot - image_ref: openclaw-install-nonroot:local - timeout_minutes: 60 + timeout-minutes: 45 + outputs: + archive_sha256: ${{ steps.image_artifact.outputs.archive_sha256 }} + artifact_digest: ${{ steps.image_artifact_upload.outputs.artifact-digest }} + artifact_id: ${{ steps.image_artifact_upload.outputs.artifact-id }} + artifact_name: ${{ steps.image_artifact.outputs.artifact_name }} + artifact_run_attempt: ${{ steps.image_artifact.outputs.run_attempt }} + artifact_run_id: ${{ steps.image_artifact.outputs.run_id }} + target_sha: ${{ steps.image_artifact.outputs.target_sha }} + workflow_sha: ${{ steps.image_artifact.outputs.workflow_sha }} + env: + DOCKER_BUILD_SUMMARY: "false" + DOCKER_BUILD_RECORD_UPLOAD: "false" + steps: + - name: Checkout trusted installer harness + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + repository: ${{ needs.preflight.outputs.workflow_repository }} + ref: ${{ needs.preflight.outputs.workflow_sha }} + persist-credentials: false + + - name: Set up Blacksmith Docker Builder + uses: useblacksmith/setup-docker-builder@6ff44f8e5255f9d8aa31ef22f7e57a2d926b7da0 # v1 + with: + max-cache-size-mb: 800000 + + - name: Build installer non-root image + env: + IMAGE_REF: openclaw-install-nonroot:local + run: | + timeout --kill-after=30s 20m docker buildx build \ + --progress=plain \ + --load \ + -t "$IMAGE_REF" \ + -f ./scripts/docker/install-sh-nonroot/Dockerfile \ + ./scripts/docker + + - name: Pack installer non-root image artifact + id: image_artifact + env: + IMAGE_REF: openclaw-install-nonroot:local + TARGET_SHA: ${{ needs.preflight.outputs.target_sha }} + WORKFLOW_SHA: ${{ needs.preflight.outputs.workflow_sha }} + run: | + set -euo pipefail + artifact_dir="${RUNNER_TEMP}/install-smoke-nonroot-image" + artifact_name="install-smoke-nonroot-image-${TARGET_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + bash scripts/docker/shared-image-artifact.sh \ + pack "$artifact_dir" install-smoke-nonroot "$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF" + archive_sha256="$( + jq -er '.archive.sha256 | select(type == "string" and test("^[a-f0-9]{64}$"))' \ + "$artifact_dir/shared-image-artifact.json" + )" + { + echo "archive_sha256=$archive_sha256" + echo "artifact_name=$artifact_name" + echo "artifact_path=$artifact_dir" + echo "run_attempt=$GITHUB_RUN_ATTEMPT" + echo "run_id=$GITHUB_RUN_ID" + echo "target_sha=$TARGET_SHA" + echo "workflow_sha=$WORKFLOW_SHA" + } >> "$GITHUB_OUTPUT" + + - name: Upload installer non-root image artifact + id: image_artifact_upload + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ${{ steps.image_artifact.outputs.artifact_name }} + path: ${{ steps.image_artifact.outputs.artifact_path }} + if-no-files-found: error + compression-level: 0 + retention-days: 7 + + installer_smoke_update: + needs: + [preflight, root_dockerfile_image, root_dockerfile_image_ready, installer_smoke_update_image] + if: needs.preflight.outputs.run_full_install_smoke == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 120 env: DOCKER_BUILD_SUMMARY: "false" DOCKER_BUILD_RECORD_UPLOAD: "false" @@ -645,7 +716,6 @@ jobs: persist-credentials: false - name: Checkout trusted image artifact helper - if: matrix.group == 'update' uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: ${{ needs.preflight.outputs.workflow_repository }} @@ -654,7 +724,6 @@ jobs: persist-credentials: false - name: Validate root Dockerfile image artifact binding - if: matrix.group == 'update' env: ARCHIVE_SHA256: ${{ needs.root_dockerfile_image.outputs.archive_sha256 }} ARTIFACT_DIGEST: ${{ needs.root_dockerfile_image.outputs.artifact_digest }} @@ -696,7 +765,6 @@ jobs: "$ARTIFACT_RUN_ID" "$ARTIFACT_RUN_ATTEMPT" - name: Download root Dockerfile image artifact - if: matrix.group == 'update' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: artifact-ids: ${{ needs.root_dockerfile_image.outputs.artifact_id }} @@ -705,7 +773,6 @@ jobs: github-token: ${{ github.token }} - name: Verify and load root Dockerfile image artifact - if: matrix.group == 'update' env: IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }} OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256: ${{ needs.root_dockerfile_image.outputs.archive_sha256 }} @@ -720,45 +787,98 @@ jobs: "$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF" - name: Require local root Dockerfile image - if: matrix.group == 'update' env: IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }} run: docker image inspect "$IMAGE_REF" >/dev/null - - name: Download installer smoke image artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 - with: - name: ${{ format('{0}-image-{1}-{2}-{3}', matrix.artifact_key, needs.preflight.outputs.target_sha, github.run_id, github.run_attempt) }} - path: ${{ runner.temp }}/installer-smoke-image - - - name: Verify and load installer smoke image artifact + - name: Validate installer update image artifact binding env: - ARTIFACT_KEY: ${{ matrix.artifact_key }} - IMAGE_REF: ${{ matrix.image_ref }} + ARCHIVE_SHA256: ${{ needs.installer_smoke_update_image.outputs.archive_sha256 }} + ARTIFACT_DIGEST: ${{ needs.installer_smoke_update_image.outputs.artifact_digest }} + ARTIFACT_ID: ${{ needs.installer_smoke_update_image.outputs.artifact_id }} + ARTIFACT_NAME: ${{ needs.installer_smoke_update_image.outputs.artifact_name }} + ARTIFACT_RUN_ATTEMPT: ${{ needs.installer_smoke_update_image.outputs.artifact_run_attempt }} + ARTIFACT_RUN_ID: ${{ needs.installer_smoke_update_image.outputs.artifact_run_id }} + ARTIFACT_TARGET_SHA: ${{ needs.installer_smoke_update_image.outputs.target_sha }} + ARTIFACT_WORKFLOW_SHA: ${{ needs.installer_smoke_update_image.outputs.workflow_sha }} + GH_TOKEN: ${{ github.token }} TARGET_SHA: ${{ needs.preflight.outputs.target_sha }} WORKFLOW_SHA: ${{ needs.preflight.outputs.workflow_sha }} run: | set -euo pipefail + [[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]] || { + echo "Installer update image artifact ID is missing or invalid." >&2 + exit 1 + } + [[ "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]] || { + echo "Installer update image artifact digest is missing or invalid." >&2 + exit 1 + } + [[ "$ARCHIVE_SHA256" =~ ^[a-f0-9]{64}$ ]] || { + echo "Installer update image archive SHA-256 is missing or invalid." >&2 + exit 1 + } + [[ "$ARTIFACT_RUN_ID" =~ ^[1-9][0-9]*$ ]] || { + echo "Installer update image artifact run ID is missing or invalid." >&2 + exit 1 + } + [[ "$ARTIFACT_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]] || { + echo "Installer update image artifact run attempt is missing or invalid." >&2 + exit 1 + } + [[ "$ARTIFACT_TARGET_SHA" == "$TARGET_SHA" ]] || { + echo "Installer update image target SHA does not match the selected candidate." >&2 + exit 1 + } + [[ "$ARTIFACT_WORKFLOW_SHA" == "$WORKFLOW_SHA" ]] || { + echo "Installer update image workflow SHA does not match the trusted harness." >&2 + exit 1 + } + expected_artifact_name="install-smoke-update-image-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" + [[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || { + echo "Installer update image artifact name does not match the producer tuple." >&2 + exit 1 + } bash scripts/docker/shared-image-artifact.sh \ - load "${RUNNER_TEMP}/installer-smoke-image" "$ARTIFACT_KEY" \ + verify-upload "Installer update image" "$ARTIFACT_ID" "$ARTIFACT_NAME" \ + "$ARTIFACT_DIGEST" "$ARTIFACT_RUN_ID" "$ARTIFACT_RUN_ATTEMPT" + + - name: Download installer update image artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + artifact-ids: ${{ needs.installer_smoke_update_image.outputs.artifact_id }} + path: ${{ runner.temp }}/install-smoke-update-image + run-id: ${{ needs.installer_smoke_update_image.outputs.artifact_run_id }} + github-token: ${{ github.token }} + + - name: Verify and load installer update image artifact + env: + IMAGE_REF: openclaw-install-smoke:local + OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256: ${{ needs.installer_smoke_update_image.outputs.archive_sha256 }} + OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT: ${{ needs.installer_smoke_update_image.outputs.artifact_run_attempt }} + OPENCLAW_SHARED_IMAGE_RUN_ID: ${{ needs.installer_smoke_update_image.outputs.artifact_run_id }} + TARGET_SHA: ${{ needs.installer_smoke_update_image.outputs.target_sha }} + WORKFLOW_SHA: ${{ needs.installer_smoke_update_image.outputs.workflow_sha }} + run: | + set -euo pipefail + bash scripts/docker/shared-image-artifact.sh \ + load "${RUNNER_TEMP}/install-smoke-update-image" install-smoke-update \ "$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF" - - name: Require local installer smoke image - env: - IMAGE_REF: ${{ matrix.image_ref }} - run: docker image inspect "$IMAGE_REF" >/dev/null + - name: Require local installer update image + run: docker image inspect openclaw-install-smoke:local >/dev/null - - name: Setup Node environment for installer smoke + - name: Setup Node environment for installer update smoke uses: ./.github/actions/setup-node-env with: cache-mode: restore install-bun: "false" - install-deps: ${{ matrix.group == 'update' && 'true' || 'false' }} + install-deps: "true" - - name: Run installer docker tests + - name: Run installer update docker tests env: OPENCLAW_INSTALL_SMOKE_ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog }} - OPENCLAW_INSTALL_SMOKE_GROUP: ${{ matrix.group }} + OPENCLAW_INSTALL_SMOKE_GROUP: update OPENCLAW_INSTALL_URL: file:///tmp/openclaw-install.sh OPENCLAW_INSTALL_CLI_URL: file:///tmp/openclaw-install-cli.sh OPENCLAW_NO_ONBOARD: "1" @@ -775,7 +895,6 @@ jobs: run: bash scripts/test-install-sh-docker.sh - name: Run Rocky Linux installer smoke - if: matrix.group == 'update' run: | timeout --kill-after=30s 20m docker run --rm \ --platform linux/amd64 \ @@ -786,7 +905,6 @@ jobs: 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 - if: matrix.group == 'update' run: | timeout --kill-after=30s 20m docker run --rm \ --platform linux/amd64 \ @@ -796,27 +914,165 @@ jobs: rockylinux:9@sha256:d644d203142cd5b54ad2a83a203e1dee68af2229f8fe32f52a30c6e1d3c3a9e0 \ 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' + installer_smoke_nonroot: + needs: [preflight, installer_smoke_nonroot_image] + if: needs.preflight.outputs.run_full_install_smoke == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 60 + env: + OPENCLAW_DOCKER_E2E_REQUIRE_LOCAL_IMAGE: "1" + steps: + - name: Checkout trusted installer harness + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + repository: ${{ needs.preflight.outputs.workflow_repository }} + ref: ${{ needs.preflight.outputs.workflow_sha }} + persist-credentials: false + + - name: Checkout candidate CLI + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ needs.preflight.outputs.target_sha }} + path: candidate + persist-credentials: false + + - name: Validate installer non-root image artifact binding + env: + ARCHIVE_SHA256: ${{ needs.installer_smoke_nonroot_image.outputs.archive_sha256 }} + ARTIFACT_DIGEST: ${{ needs.installer_smoke_nonroot_image.outputs.artifact_digest }} + ARTIFACT_ID: ${{ needs.installer_smoke_nonroot_image.outputs.artifact_id }} + ARTIFACT_NAME: ${{ needs.installer_smoke_nonroot_image.outputs.artifact_name }} + ARTIFACT_RUN_ATTEMPT: ${{ needs.installer_smoke_nonroot_image.outputs.artifact_run_attempt }} + ARTIFACT_RUN_ID: ${{ needs.installer_smoke_nonroot_image.outputs.artifact_run_id }} + ARTIFACT_TARGET_SHA: ${{ needs.installer_smoke_nonroot_image.outputs.target_sha }} + ARTIFACT_WORKFLOW_SHA: ${{ needs.installer_smoke_nonroot_image.outputs.workflow_sha }} + GH_TOKEN: ${{ github.token }} + TARGET_SHA: ${{ needs.preflight.outputs.target_sha }} + WORKFLOW_SHA: ${{ needs.preflight.outputs.workflow_sha }} + run: | + set -euo pipefail + [[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]] || { + echo "Installer non-root image artifact ID is missing or invalid." >&2 + exit 1 + } + [[ "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]] || { + echo "Installer non-root image artifact digest is missing or invalid." >&2 + exit 1 + } + [[ "$ARCHIVE_SHA256" =~ ^[a-f0-9]{64}$ ]] || { + echo "Installer non-root image archive SHA-256 is missing or invalid." >&2 + exit 1 + } + [[ "$ARTIFACT_RUN_ID" =~ ^[1-9][0-9]*$ ]] || { + echo "Installer non-root image artifact run ID is missing or invalid." >&2 + exit 1 + } + [[ "$ARTIFACT_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]] || { + echo "Installer non-root image artifact run attempt is missing or invalid." >&2 + exit 1 + } + [[ "$ARTIFACT_TARGET_SHA" == "$TARGET_SHA" ]] || { + echo "Installer non-root image target SHA does not match the selected candidate." >&2 + exit 1 + } + [[ "$ARTIFACT_WORKFLOW_SHA" == "$WORKFLOW_SHA" ]] || { + echo "Installer non-root image workflow SHA does not match the trusted harness." >&2 + exit 1 + } + expected_artifact_name="install-smoke-nonroot-image-${TARGET_SHA}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" + [[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || { + echo "Installer non-root image artifact name does not match the producer tuple." >&2 + exit 1 + } + bash scripts/docker/shared-image-artifact.sh \ + verify-upload "Installer non-root image" "$ARTIFACT_ID" "$ARTIFACT_NAME" \ + "$ARTIFACT_DIGEST" "$ARTIFACT_RUN_ID" "$ARTIFACT_RUN_ATTEMPT" + + - name: Download installer non-root image artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + artifact-ids: ${{ needs.installer_smoke_nonroot_image.outputs.artifact_id }} + path: ${{ runner.temp }}/install-smoke-nonroot-image + run-id: ${{ needs.installer_smoke_nonroot_image.outputs.artifact_run_id }} + github-token: ${{ github.token }} + + - name: Verify and load installer non-root image artifact + env: + IMAGE_REF: openclaw-install-nonroot:local + OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256: ${{ needs.installer_smoke_nonroot_image.outputs.archive_sha256 }} + OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT: ${{ needs.installer_smoke_nonroot_image.outputs.artifact_run_attempt }} + OPENCLAW_SHARED_IMAGE_RUN_ID: ${{ needs.installer_smoke_nonroot_image.outputs.artifact_run_id }} + TARGET_SHA: ${{ needs.installer_smoke_nonroot_image.outputs.target_sha }} + WORKFLOW_SHA: ${{ needs.installer_smoke_nonroot_image.outputs.workflow_sha }} + run: | + set -euo pipefail + bash scripts/docker/shared-image-artifact.sh \ + load "${RUNNER_TEMP}/install-smoke-nonroot-image" install-smoke-nonroot \ + "$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF" + + - name: Require local installer non-root image + run: docker image inspect openclaw-install-nonroot:local >/dev/null + + - name: Setup Node environment for installer non-root smoke + uses: ./.github/actions/setup-node-env + with: + cache-mode: off + install-bun: "false" + install-deps: "false" + + - name: Run installer non-root docker tests + env: + OPENCLAW_INSTALL_SMOKE_GROUP: nonroot + 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_SOURCE_DIR: ${{ github.workspace }}/candidate + run: bash scripts/test-install-sh-docker.sh + installer_smoke: - needs: [preflight, installer_smoke_image, installer_smoke_group] + needs: + [ + preflight, + root_dockerfile_image, + root_dockerfile_image_ready, + installer_smoke_update_image, + installer_smoke_update, + installer_smoke_nonroot_image, + installer_smoke_nonroot, + ] if: always() && needs.preflight.result == 'success' && needs.preflight.outputs.run_full_install_smoke == 'true' runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - name: Verify installer smoke groups env: - CONSUMER_RESULT: ${{ needs.installer_smoke_group.result }} - PRODUCER_RESULT: ${{ needs.installer_smoke_image.result }} + NONROOT_CONSUMER_RESULT: ${{ needs.installer_smoke_nonroot.result }} + NONROOT_PRODUCER_RESULT: ${{ needs.installer_smoke_nonroot_image.result }} + ROOT_IMAGE_READY_RESULT: ${{ needs.root_dockerfile_image_ready.result }} + ROOT_IMAGE_RESULT: ${{ needs.root_dockerfile_image.result }} + UPDATE_CONSUMER_RESULT: ${{ needs.installer_smoke_update.result }} + UPDATE_PRODUCER_RESULT: ${{ needs.installer_smoke_update_image.result }} run: | set -euo pipefail failed=0 - if [[ "$PRODUCER_RESULT" != "success" ]]; then - echo "::error::Installer smoke image producers ended with ${PRODUCER_RESULT}." - failed=1 - fi - if [[ "$CONSUMER_RESULT" != "success" ]]; then - echo "::error::Installer smoke consumers ended with ${CONSUMER_RESULT}." - failed=1 - fi + check_result() { + local name="$1" + local result="$2" + if [[ "$result" != "success" ]]; then + echo "::error::${name} ended with ${result}." + failed=1 + fi + } + check_result "Root Dockerfile image producer" "$ROOT_IMAGE_RESULT" + check_result "Root Dockerfile image gate" "$ROOT_IMAGE_READY_RESULT" + check_result "Installer update image producer" "$UPDATE_PRODUCER_RESULT" + check_result "Installer update consumer" "$UPDATE_CONSUMER_RESULT" + check_result "Installer non-root image producer" "$NONROOT_PRODUCER_RESULT" + check_result "Installer non-root consumer" "$NONROOT_CONSUMER_RESULT" exit "$failed" bun_global_install_smoke: diff --git a/test/scripts/install-smoke-no-push-workflow.test.ts b/test/scripts/install-smoke-no-push-workflow.test.ts index 270a5657d4a5..3335bc2231ef 100644 --- a/test/scripts/install-smoke-no-push-workflow.test.ts +++ b/test/scripts/install-smoke-no-push-workflow.test.ts @@ -206,15 +206,13 @@ describe("install smoke no-push root image transport", () => { const workflow = readWorkflow(INSTALL_SMOKE_REUSABLE); for (const jobName of [ "root_dockerfile_smokes", - "installer_smoke_group", + "installer_smoke_update", "bun_global_install_smoke", ]) { const consumer = job(workflow, jobName); - const updateOnly = - jobName === "installer_smoke_group" ? "matrix.group == 'update'" : undefined; expect(consumer.needs, jobName).toContain("root_dockerfile_image_ready"); expect(consumer.env?.OPENCLAW_DOCKER_E2E_REQUIRE_LOCAL_IMAGE, jobName).toBe("1"); - expect(step(consumer, "Checkout trusted image artifact helper").if, jobName).toBe(updateOnly); + expect(step(consumer, "Checkout trusted image artifact helper").if, jobName).toBeUndefined(); expect( consumer.steps?.find((candidate) => candidate.name === "Log in to GHCR"), jobName, @@ -225,7 +223,7 @@ describe("install smoke no-push root image transport", () => { ).toBeUndefined(); const binding = step(consumer, "Validate root Dockerfile image artifact binding"); - expect(binding.if, jobName).toBe(updateOnly); + expect(binding.if, jobName).toBeUndefined(); expect(binding.env, jobName).toMatchObject({ ARCHIVE_SHA256: "${{ needs.root_dockerfile_image.outputs.archive_sha256 }}", ARTIFACT_DIGEST: "${{ needs.root_dockerfile_image.outputs.artifact_digest }}", @@ -251,7 +249,7 @@ describe("install smoke no-push root image transport", () => { expect(binding.run, jobName).not.toContain("<<<"); const download = step(consumer, "Download root Dockerfile image artifact"); - expect(download.if, jobName).toBe(updateOnly); + expect(download.if, jobName).toBeUndefined(); expect(download.with, jobName).toMatchObject({ "artifact-ids": "${{ needs.root_dockerfile_image.outputs.artifact_id }}", "github-token": "${{ github.token }}", @@ -260,14 +258,14 @@ describe("install smoke no-push root image transport", () => { }); const load = step(consumer, "Verify and load root Dockerfile image artifact"); - expect(load.if, jobName).toBe(updateOnly); + expect(load.if, jobName).toBeUndefined(); expect(load.run, jobName).toContain( 'load "${RUNNER_TEMP}/install-smoke-root-image" install-smoke-root', ); expect(load.run, jobName).toContain('"$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF"'); const requireLocal = step(consumer, "Require local root Dockerfile image"); - expect(requireLocal.if, jobName).toBe(updateOnly); + expect(requireLocal.if, jobName).toBeUndefined(); expect(requireLocal.run, jobName).toBe('docker image inspect "$IMAGE_REF" >/dev/null'); } @@ -276,101 +274,192 @@ describe("install smoke no-push root image transport", () => { expect(text).not.toContain("gh api"); }); - it("builds and consumes installer images in independent non-fail-fast groups", () => { + it("binds independent installer producer-consumer pairs to immutable artifact tuples", () => { const workflow = readWorkflow(INSTALL_SMOKE_REUSABLE); - const producer = job(workflow, "installer_smoke_image"); - const consumer = job(workflow, "installer_smoke_group"); + const pairs = [ + { + artifactKind: "install-smoke-update", + artifactPrefix: "install-smoke-update-image", + buildName: "Build installer smoke image", + consumerName: "installer_smoke_update", + downloadName: "Download installer update image artifact", + group: "update", + loadName: "Verify and load installer update image artifact", + packName: "Pack installer smoke image artifact", + producerName: "installer_smoke_update_image", + setupName: "Setup Node environment for installer update smoke", + testName: "Run installer update docker tests", + uploadName: "Upload installer smoke image artifact", + validateName: "Validate installer update image artifact binding", + }, + { + artifactKind: "install-smoke-nonroot", + artifactPrefix: "install-smoke-nonroot-image", + buildName: "Build installer non-root image", + consumerName: "installer_smoke_nonroot", + downloadName: "Download installer non-root image artifact", + group: "nonroot", + loadName: "Verify and load installer non-root image artifact", + packName: "Pack installer non-root image artifact", + producerName: "installer_smoke_nonroot_image", + setupName: "Setup Node environment for installer non-root smoke", + testName: "Run installer non-root docker tests", + uploadName: "Upload installer non-root image artifact", + validateName: "Validate installer non-root image artifact binding", + }, + ] as const; + + for (const pair of pairs) { + const producer = job(workflow, pair.producerName); + expect(producer.needs, pair.producerName).toEqual(["preflight"]); + expect(producer["timeout-minutes"], pair.producerName).toBe(45); + expect(producer.outputs, pair.producerName).toEqual({ + archive_sha256: "${{ steps.image_artifact.outputs.archive_sha256 }}", + artifact_digest: "${{ steps.image_artifact_upload.outputs.artifact-digest }}", + artifact_id: "${{ steps.image_artifact_upload.outputs.artifact-id }}", + artifact_name: "${{ steps.image_artifact.outputs.artifact_name }}", + artifact_run_attempt: "${{ steps.image_artifact.outputs.run_attempt }}", + artifact_run_id: "${{ steps.image_artifact.outputs.run_id }}", + target_sha: "${{ steps.image_artifact.outputs.target_sha }}", + workflow_sha: "${{ steps.image_artifact.outputs.workflow_sha }}", + }); + expect(step(producer, pair.buildName).run, pair.producerName).toContain("--load"); + + const pack = step(producer, pair.packName); + expect(pack.run, pair.producerName).toContain( + `artifact_name="${pair.artifactPrefix}-\${TARGET_SHA}-\${GITHUB_RUN_ID}-\${GITHUB_RUN_ATTEMPT}"`, + ); + expect(pack.run, pair.producerName).toContain( + `pack "$artifact_dir" ${pair.artifactKind} "$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF"`, + ); + expect(pack.run, pair.producerName).toContain('echo "archive_sha256=$archive_sha256"'); + expect(pack.run, pair.producerName).toContain('echo "run_attempt=$GITHUB_RUN_ATTEMPT"'); + expect(pack.run, pair.producerName).toContain('echo "run_id=$GITHUB_RUN_ID"'); + expect(pack.run, pair.producerName).toContain('echo "target_sha=$TARGET_SHA"'); + expect(pack.run, pair.producerName).toContain('echo "workflow_sha=$WORKFLOW_SHA"'); + expect(step(producer, pair.uploadName).with, pair.producerName).toMatchObject({ + "compression-level": 0, + "if-no-files-found": "error", + name: "${{ steps.image_artifact.outputs.artifact_name }}", + }); + + const consumer = job(workflow, pair.consumerName); + const expectedNeeds = + pair.group === "update" + ? ["preflight", "root_dockerfile_image", "root_dockerfile_image_ready", pair.producerName] + : ["preflight", pair.producerName]; + expect(consumer.needs, pair.consumerName).toEqual(expectedNeeds); + expect(consumer["timeout-minutes"], pair.consumerName).toBe( + pair.group === "update" ? 120 : 60, + ); + + const binding = step(consumer, pair.validateName); + expect(binding.env, pair.consumerName).toMatchObject({ + ARCHIVE_SHA256: `\${{ needs.${pair.producerName}.outputs.archive_sha256 }}`, + ARTIFACT_DIGEST: `\${{ needs.${pair.producerName}.outputs.artifact_digest }}`, + ARTIFACT_ID: `\${{ needs.${pair.producerName}.outputs.artifact_id }}`, + ARTIFACT_NAME: `\${{ needs.${pair.producerName}.outputs.artifact_name }}`, + ARTIFACT_RUN_ATTEMPT: `\${{ needs.${pair.producerName}.outputs.artifact_run_attempt }}`, + ARTIFACT_RUN_ID: `\${{ needs.${pair.producerName}.outputs.artifact_run_id }}`, + ARTIFACT_TARGET_SHA: `\${{ needs.${pair.producerName}.outputs.target_sha }}`, + ARTIFACT_WORKFLOW_SHA: `\${{ needs.${pair.producerName}.outputs.workflow_sha }}`, + TARGET_SHA: "${{ needs.preflight.outputs.target_sha }}", + WORKFLOW_SHA: "${{ needs.preflight.outputs.workflow_sha }}", + }); + expect(binding.run, pair.consumerName).toContain('[[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]]'); + expect(binding.run, pair.consumerName).toContain( + '[[ "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]]', + ); + expect(binding.run, pair.consumerName).toContain('[[ "$ARCHIVE_SHA256" =~ ^[a-f0-9]{64}$ ]]'); + expect(binding.run, pair.consumerName).toContain( + '[[ "$ARTIFACT_TARGET_SHA" == "$TARGET_SHA" ]]', + ); + expect(binding.run, pair.consumerName).toContain( + '[[ "$ARTIFACT_WORKFLOW_SHA" == "$WORKFLOW_SHA" ]]', + ); + expect(binding.run, pair.consumerName).toContain( + `expected_artifact_name="${pair.artifactPrefix}-\${TARGET_SHA}-\${ARTIFACT_RUN_ID}-\${ARTIFACT_RUN_ATTEMPT}"`, + ); + expect(binding.run, pair.consumerName).toContain("verify-upload"); + + const download = step(consumer, pair.downloadName); + expect(download.with, pair.consumerName).toMatchObject({ + "artifact-ids": `\${{ needs.${pair.producerName}.outputs.artifact_id }}`, + "github-token": "${{ github.token }}", + "run-id": `\${{ needs.${pair.producerName}.outputs.artifact_run_id }}`, + }); + expect(download.with?.name, pair.consumerName).toBeUndefined(); + + const load = step(consumer, pair.loadName); + expect(load.env, pair.consumerName).toMatchObject({ + OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256: `\${{ needs.${pair.producerName}.outputs.archive_sha256 }}`, + OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT: `\${{ needs.${pair.producerName}.outputs.artifact_run_attempt }}`, + OPENCLAW_SHARED_IMAGE_RUN_ID: `\${{ needs.${pair.producerName}.outputs.artifact_run_id }}`, + TARGET_SHA: `\${{ needs.${pair.producerName}.outputs.target_sha }}`, + WORKFLOW_SHA: `\${{ needs.${pair.producerName}.outputs.workflow_sha }}`, + }); + expect(load.run, pair.consumerName).toContain( + `load "\${RUNNER_TEMP}/${pair.artifactPrefix}" ${pair.artifactKind}`, + ); + + const setup = step(consumer, pair.setupName); + expect(setup.with, pair.consumerName).toMatchObject({ + "cache-mode": pair.group === "update" ? "restore" : "off", + "install-bun": "false", + "install-deps": pair.group === "update" ? "true" : "false", + }); + expect(step(consumer, pair.testName).env?.OPENCLAW_INSTALL_SMOKE_GROUP).toBe(pair.group); + } + }); + + it("drains every independent producer and consumer without sibling failure suppression", () => { + const workflow = readWorkflow(INSTALL_SMOKE_REUSABLE); + const update = job(workflow, "installer_smoke_update"); + const nonroot = job(workflow, "installer_smoke_nonroot"); const aggregate = job(workflow, "installer_smoke"); - expect(producer.needs).toEqual(["preflight"]); - expect(producer["timeout-minutes"]).toBe(45); - expect(producer.strategy).toEqual({ - "fail-fast": false, - matrix: { - include: [ - { - artifact_key: "install-smoke-update", - dockerfile: "./scripts/docker/install-sh-smoke/Dockerfile", - group: "update", - image_ref: "openclaw-install-smoke:local", - }, - { - artifact_key: "install-smoke-nonroot", - dockerfile: "./scripts/docker/install-sh-nonroot/Dockerfile", - group: "nonroot", - image_ref: "openclaw-install-nonroot:local", - }, - ], - }, - }); - expect(step(producer, "Build installer smoke image").run).toContain('-f "$DOCKERFILE"'); - expect(step(producer, "Pack installer smoke image artifact").run).toContain( - 'pack "$artifact_dir" "$ARTIFACT_KEY" "$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF"', - ); - expect(step(producer, "Upload installer smoke image artifact").with).toMatchObject({ - "compression-level": 0, - "if-no-files-found": "error", - name: "${{ steps.image_artifact.outputs.artifact_name }}", - }); - - expect(consumer.needs).toEqual([ + expect(update.needs).toEqual([ "preflight", "root_dockerfile_image", "root_dockerfile_image_ready", - "installer_smoke_image", + "installer_smoke_update_image", ]); - expect(consumer["timeout-minutes"]).toBe("${{ matrix.timeout_minutes }}"); - expect(consumer.strategy).toEqual({ - "fail-fast": false, - matrix: { - include: [ - { - artifact_key: "install-smoke-update", - group: "update", - image_ref: "openclaw-install-smoke:local", - timeout_minutes: 120, - }, - { - artifact_key: "install-smoke-nonroot", - group: "nonroot", - image_ref: "openclaw-install-nonroot:local", - timeout_minutes: 60, - }, - ], - }, - }); - expect(step(consumer, "Download installer smoke image artifact").with).toMatchObject({ - name: "${{ format('{0}-image-{1}-{2}-{3}', matrix.artifact_key, needs.preflight.outputs.target_sha, github.run_id, github.run_attempt) }}", - path: "${{ runner.temp }}/installer-smoke-image", - }); - expect(step(consumer, "Verify and load installer smoke image artifact").run).toContain( - 'load "${RUNNER_TEMP}/installer-smoke-image" "$ARTIFACT_KEY"', - ); - expect(step(consumer, "Setup Node environment for installer smoke").with).toMatchObject({ - "install-bun": "false", - "install-deps": "${{ matrix.group == 'update' && 'true' || 'false' }}", - }); - expect(step(consumer, "Run installer docker tests").env).toMatchObject({ - OPENCLAW_INSTALL_SMOKE_GROUP: "${{ matrix.group }}", - }); - expect(step(consumer, "Run Rocky Linux installer smoke").if).toBe("matrix.group == 'update'"); - expect(step(consumer, "Run Rocky Linux CLI installer smoke").if).toBe( - "matrix.group == 'update'", - ); + expect(update.needs).not.toContain("installer_smoke_nonroot_image"); + expect(nonroot.needs).toEqual(["preflight", "installer_smoke_nonroot_image"]); + expect(nonroot.needs).not.toContain("root_dockerfile_image"); + expect(nonroot.needs).not.toContain("root_dockerfile_image_ready"); + expect(nonroot.needs).not.toContain("installer_smoke_update_image"); + expect(aggregate.if).toContain("always()"); expect(aggregate.needs).toEqual([ "preflight", - "installer_smoke_image", - "installer_smoke_group", + "root_dockerfile_image", + "root_dockerfile_image_ready", + "installer_smoke_update_image", + "installer_smoke_update", + "installer_smoke_nonroot_image", + "installer_smoke_nonroot", ]); expect(aggregate["timeout-minutes"]).toBe(5); const verify = step(aggregate, "Verify installer smoke groups"); expect(verify.env).toEqual({ - CONSUMER_RESULT: "${{ needs.installer_smoke_group.result }}", - PRODUCER_RESULT: "${{ needs.installer_smoke_image.result }}", + NONROOT_CONSUMER_RESULT: "${{ needs.installer_smoke_nonroot.result }}", + NONROOT_PRODUCER_RESULT: "${{ needs.installer_smoke_nonroot_image.result }}", + ROOT_IMAGE_READY_RESULT: "${{ needs.root_dockerfile_image_ready.result }}", + ROOT_IMAGE_RESULT: "${{ needs.root_dockerfile_image.result }}", + UPDATE_CONSUMER_RESULT: "${{ needs.installer_smoke_update.result }}", + UPDATE_PRODUCER_RESULT: "${{ needs.installer_smoke_update_image.result }}", }); - expect(verify.run).toContain('if [[ "$PRODUCER_RESULT" != "success" ]]'); - expect(verify.run).toContain('if [[ "$CONSUMER_RESULT" != "success" ]]'); + for (const result of [ + "ROOT_IMAGE_RESULT", + "ROOT_IMAGE_READY_RESULT", + "UPDATE_PRODUCER_RESULT", + "UPDATE_CONSUMER_RESULT", + "NONROOT_PRODUCER_RESULT", + "NONROOT_CONSUMER_RESULT", + ]) { + expect(verify.run).toContain(`"$${result}"`); + } }); it("selects the read-only reusable core from release checks", () => { @@ -393,7 +482,7 @@ describe("install smoke no-push root image transport", () => { it("passes package changelog intent only to current-tree smoke scripts", () => { const workflow = readWorkflow(INSTALL_SMOKE_REUSABLE); expect( - step(job(workflow, "installer_smoke_group"), "Run installer docker tests").env, + step(job(workflow, "installer_smoke_update"), "Run installer update docker tests").env, ).toMatchObject({ OPENCLAW_INSTALL_SMOKE_ALLOW_UNRELEASED_CHANGELOG: "${{ inputs.allow_unreleased_changelog }}", }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 3a4e4a33e869..6ce06b27e419 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -7454,14 +7454,10 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$? expect(installSmoke.jobs?.root_dockerfile_image_ready?.["timeout-minutes"]).toBe(5); expect(installSmoke.jobs?.qr_package_install_smoke?.["timeout-minutes"]).toBe(30); expect(installSmoke.jobs?.root_dockerfile_smokes?.["timeout-minutes"]).toBe(90); - expect(installSmoke.jobs?.installer_smoke_image?.["timeout-minutes"]).toBe(45); - expect( - evaluatedJobTimeouts( - INSTALL_SMOKE_REUSABLE_WORKFLOW, - "installer_smoke_group", - workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_group"), - ), - ).toEqual([120, 60]); + expect(installSmoke.jobs?.installer_smoke_update_image?.["timeout-minutes"]).toBe(45); + expect(installSmoke.jobs?.installer_smoke_nonroot_image?.["timeout-minutes"]).toBe(45); + expect(installSmoke.jobs?.installer_smoke_update?.["timeout-minutes"]).toBe(120); + expect(installSmoke.jobs?.installer_smoke_nonroot?.["timeout-minutes"]).toBe(60); expect(installSmoke.jobs?.installer_smoke?.["timeout-minutes"]).toBe(5); expect(installSmoke.jobs?.bun_global_install_smoke?.["timeout-minutes"]).toBe(60); expect(installSmoke.jobs?.["docker-e2e-fast"]?.["timeout-minutes"]).toBe(12); @@ -7639,42 +7635,64 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$? expect( jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "root_dockerfile_image_ready")), ).toEqual(["preflight", "root_dockerfile_image"]); - expect(jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_image"))).toEqual( - ["preflight"], - ); - expect(jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_group"))).toEqual( - [ - "preflight", - "root_dockerfile_image", - "root_dockerfile_image_ready", - "installer_smoke_image", - ], - ); + expect( + jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_update_image")), + ).toEqual(["preflight"]); + expect( + jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_nonroot_image")), + ).toEqual(["preflight"]); + expect( + jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_update")), + ).toEqual([ + "preflight", + "root_dockerfile_image", + "root_dockerfile_image_ready", + "installer_smoke_update_image", + ]); + expect( + jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_nonroot")), + ).toEqual(["preflight", "installer_smoke_nonroot_image"]); expect(jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke"))).toEqual([ "preflight", - "installer_smoke_image", - "installer_smoke_group", + "root_dockerfile_image", + "root_dockerfile_image_ready", + "installer_smoke_update_image", + "installer_smoke_update", + "installer_smoke_nonroot_image", + "installer_smoke_nonroot", ]); expect(jobNeeds(releaseSummary)).toContain("install_smoke_release_checks"); - const installerGroupTimeout = Math.max( - ...evaluatedJobTimeouts( - INSTALL_SMOKE_REUSABLE_WORKFLOW, - "installer_smoke_group", - workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_group"), - ), - ); const releaseInstallPath = [ timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "stable"), timeoutForProfile(installSmoke.jobs?.preflight?.["timeout-minutes"], "stable"), Math.max( timeoutForProfile(installSmoke.jobs?.root_dockerfile_image?.["timeout-minutes"], "stable"), - timeoutForProfile(installSmoke.jobs?.installer_smoke_image?.["timeout-minutes"], "stable"), + timeoutForProfile( + installSmoke.jobs?.installer_smoke_update_image?.["timeout-minutes"], + "stable", + ), ), - installerGroupTimeout, + timeoutForProfile( + installSmoke.jobs?.root_dockerfile_image_ready?.["timeout-minutes"], + "stable", + ), + timeoutForProfile(installSmoke.jobs?.installer_smoke_update?.["timeout-minutes"], "stable"), timeoutForProfile(installSmoke.jobs?.installer_smoke?.["timeout-minutes"], "stable"), timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "stable"), ]; - expect(releaseInstallPath).toEqual([30, 15, 60, 120, 5, 5]); + expect(releaseInstallPath).toEqual([30, 15, 60, 5, 120, 5, 5]); + const releaseInstallNonrootPath = [ + timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "stable"), + timeoutForProfile(installSmoke.jobs?.preflight?.["timeout-minutes"], "stable"), + timeoutForProfile( + installSmoke.jobs?.installer_smoke_nonroot_image?.["timeout-minutes"], + "stable", + ), + timeoutForProfile(installSmoke.jobs?.installer_smoke_nonroot?.["timeout-minutes"], "stable"), + timeoutForProfile(installSmoke.jobs?.installer_smoke?.["timeout-minutes"], "stable"), + timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "stable"), + ]; + expect(releaseInstallNonrootPath).toEqual([30, 15, 45, 60, 5, 5]); const releaseQaLive = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_release_checks"); expect(jobNeeds(releaseQaLive)).toEqual(["resolve_target"]); @@ -7705,7 +7723,8 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$? expect(420 - childTimeout, `release-checks:${pathName}`).toBeGreaterThanOrEqual(60); } expect(releaseCrossOsPath.reduce((total, timeout) => total + timeout, 0)).toBe(200); - expect(releaseInstallPath.reduce((total, timeout) => total + timeout, 0)).toBe(235); + expect(releaseInstallPath.reduce((total, timeout) => total + timeout, 0)).toBe(240); + expect(releaseInstallNonrootPath.reduce((total, timeout) => total + timeout, 0)).toBe(160); expect(releaseQaLivePath.reduce((total, timeout) => total + timeout, 0)).toBe(165); expect( diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index f27fbd19ad7e..6a27066c4da2 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -2134,72 +2134,79 @@ chmod +x "$BUN_INSTALL/bin/openclaw" it("runs installer packaging from the trusted workflow revision against a nested candidate", () => { const workflow = parse(readFileSync(INSTALL_SMOKE_WORKFLOW_PATH, "utf8")); - const producer = workflow.jobs.installer_smoke_image; - const producerSteps = producer.steps as Array<{ - name?: string; - uses?: string; - with?: Record; - env?: Record; - run?: string; - }>; - const consumer = workflow.jobs.installer_smoke_group; - const steps = consumer.steps as Array<{ - name?: string; - uses?: string; - with?: Record; - env?: Record; - run?: string; - }>; - const step = (name: string) => { - const found = steps.find((entry) => entry.name === name); + const cases = [ + { + buildName: "Build installer smoke image", + consumerName: "installer_smoke_update", + dockerfile: "./scripts/docker/install-sh-smoke/Dockerfile", + group: "update", + producerName: "installer_smoke_update_image", + setupName: "Setup Node environment for installer update smoke", + testName: "Run installer update docker tests", + }, + { + buildName: "Build installer non-root image", + consumerName: "installer_smoke_nonroot", + dockerfile: "./scripts/docker/install-sh-nonroot/Dockerfile", + group: "nonroot", + producerName: "installer_smoke_nonroot_image", + setupName: "Setup Node environment for installer non-root smoke", + testName: "Run installer non-root docker tests", + }, + ] as const; + const workflowStep = ( + workflowJob: { steps?: Array> }, + name: string, + ) => { + const found = workflowJob.steps?.find((entry) => entry.name === name); expect(found, name).toBeDefined(); return found!; }; - expect(step("Checkout trusted installer harness").with).toMatchObject({ - repository: "${{ needs.preflight.outputs.workflow_repository }}", - ref: "${{ needs.preflight.outputs.workflow_sha }}", - "persist-credentials": false, - }); - expect(step("Checkout candidate CLI").with).toMatchObject({ - ref: "${{ needs.preflight.outputs.target_sha }}", - path: "candidate", - "persist-credentials": false, - }); - expect(step("Setup Node environment for installer smoke").uses).toBe( - "./.github/actions/setup-node-env", - ); - expect(step("Setup Node environment for installer smoke").with).toMatchObject({ - "install-deps": "${{ matrix.group == 'update' && 'true' || 'false' }}", - }); - expect(step("Run installer docker tests").env).toMatchObject({ + for (const testCase of cases) { + const producer = workflow.jobs[testCase.producerName]; + const consumer = workflow.jobs[testCase.consumerName]; + expect(workflowStep(producer, "Checkout trusted installer harness").with).toMatchObject({ + repository: "${{ needs.preflight.outputs.workflow_repository }}", + ref: "${{ needs.preflight.outputs.workflow_sha }}", + "persist-credentials": false, + }); + const buildStep = workflowStep(producer, testCase.buildName); + expect(buildStep.run).toContain(`-f ${testCase.dockerfile}`); + expect(buildStep.run).not.toContain("candidate/scripts/docker"); + + expect(workflowStep(consumer, "Checkout trusted installer harness").with).toMatchObject({ + repository: "${{ needs.preflight.outputs.workflow_repository }}", + ref: "${{ needs.preflight.outputs.workflow_sha }}", + "persist-credentials": false, + }); + expect(workflowStep(consumer, "Checkout candidate CLI").with).toMatchObject({ + ref: "${{ needs.preflight.outputs.target_sha }}", + path: "candidate", + "persist-credentials": false, + }); + const setup = workflowStep(consumer, testCase.setupName); + expect(setup.uses).toBe("./.github/actions/setup-node-env"); + expect(setup.with).toMatchObject({ + "cache-mode": testCase.group === "update" ? "restore" : "off", + "install-deps": testCase.group === "update" ? "true" : "false", + }); + const run = workflowStep(consumer, testCase.testName); + expect(run.env).toMatchObject({ + OPENCLAW_INSTALL_SMOKE_GROUP: testCase.group, + OPENCLAW_INSTALL_SMOKE_SOURCE_DIR: "${{ github.workspace }}/candidate", + }); + expect(run.run).toBe("bash scripts/test-install-sh-docker.sh"); + } + + expect( + workflowStep(workflow.jobs.installer_smoke_update, "Run installer update docker tests").env, + ).toMatchObject({ OPENCLAW_INSTALL_SMOKE_ALLOW_UNRELEASED_CHANGELOG: "${{ inputs.allow_unreleased_changelog }}", - OPENCLAW_INSTALL_SMOKE_GROUP: "${{ matrix.group }}", - OPENCLAW_INSTALL_SMOKE_SOURCE_DIR: "${{ github.workspace }}/candidate", }); - expect(step("Run installer docker tests").run).toBe("bash scripts/test-install-sh-docker.sh"); - const buildStep = producerSteps.find((entry) => entry.name === "Build installer smoke image"); - expect(buildStep).toBeDefined(); - expect(buildStep?.run).toContain('-f "$DOCKERFILE"'); - expect(buildStep?.run).not.toContain("candidate/scripts/docker"); - expect(producer.strategy).toMatchObject({ - "fail-fast": false, - matrix: { - include: [ - { dockerfile: "./scripts/docker/install-sh-smoke/Dockerfile", group: "update" }, - { dockerfile: "./scripts/docker/install-sh-nonroot/Dockerfile", group: "nonroot" }, - ], - }, - }); - expect(consumer.strategy).toMatchObject({ - "fail-fast": false, - matrix: { - include: [{ group: "update" }, { group: "nonroot" }], - }, - }); - expect(step("Run Rocky Linux installer smoke").run).toContain( - "$PWD/candidate/scripts/install.sh", - ); + expect( + workflowStep(workflow.jobs.installer_smoke_update, "Run Rocky Linux installer smoke").run, + ).toContain("$PWD/candidate/scripts/install.sh"); }); it("kills Bun global install smoke commands that ignore TERM after timeout", () => {