fix(ci): bind installer smoke artifacts

This commit is contained in:
Vincent Koc
2026-08-21 02:31:18 -07:00
parent 99c13fec19
commit 098dfd4707
4 changed files with 624 additions and 253 deletions
+328 -72
View File
@@ -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:
@@ -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 }}",
});
@@ -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(
+67 -60
View File
@@ -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<string, unknown>;
env?: Record<string, unknown>;
run?: string;
}>;
const consumer = workflow.jobs.installer_smoke_group;
const steps = consumer.steps as Array<{
name?: string;
uses?: string;
with?: Record<string, unknown>;
env?: Record<string, unknown>;
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<Record<string, unknown>> },
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", () => {