mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(ci): bind installer smoke artifacts
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user