Files
openclaw/.github/workflows/install-smoke-reusable.yml
Peter Steinberger 6ef2c1594d fix(ci): preserve QA package intent boundaries (#104207)
* fix(ci): preserve QA package intent boundaries

* test(ci): track rerun helper temp dirs

---------

Co-authored-by: Peter Steinberger <peter@steipete.me>
2026-07-11 00:51:57 -07:00

901 lines
39 KiB
YAML

name: Install Smoke (Reusable)
on:
workflow_call:
inputs:
allow_unreleased_changelog:
description: Allow Unreleased changelog notes when packaging the current tree
required: false
default: false
type: boolean
ref:
description: Git ref to validate
required: false
type: string
run_bun_global_install_smoke:
description: Run the Bun global install image-provider smoke
required: false
default: true
type: boolean
update_baseline_version:
description: Baseline openclaw version or dist-tag for installer update smoke
required: false
default: latest
type: string
permissions:
actions: read
contents: read
packages: read
concurrency:
group: ${{ github.workflow }}-workflow-call-${{ github.run_id }}
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
preflight:
runs-on: ubuntu-24.04
outputs:
docs_only: ${{ steps.manifest.outputs.docs_only }}
run_install_smoke: ${{ steps.manifest.outputs.run_install_smoke }}
run_fast_install_smoke: ${{ steps.manifest.outputs.run_fast_install_smoke }}
run_full_install_smoke: ${{ steps.manifest.outputs.run_full_install_smoke }}
run_bun_global_install_smoke: ${{ steps.manifest.outputs.run_bun_global_install_smoke }}
target_sha: ${{ steps.manifest.outputs.target_sha }}
dockerfile_image: ${{ steps.manifest.outputs.dockerfile_image }}
workflow_repository: ${{ steps.workflow.outputs.workflow_repository }}
workflow_sha: ${{ steps.workflow.outputs.workflow_sha }}
steps:
# github.workflow_sha identifies the caller during workflow_call. Resolve the called
# workflow SHA from job context so trusted harness checkouts cannot drift to candidate code.
- name: Resolve job workflow identity
id: workflow
env:
JOB_CONTEXT: ${{ toJSON(job) }}
shell: bash
run: |
set -euo pipefail
node --input-type=module <<'NODE'
import fs from "node:fs";
const job = JSON.parse(process.env.JOB_CONTEXT ?? "{}");
if (
typeof job.workflow_repository !== "string" ||
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(job.workflow_repository)
) {
throw new Error("job.workflow_repository must be an owner/repository slug");
}
if (typeof job.workflow_sha !== "string" || !/^[0-9a-f]{40}$/u.test(job.workflow_sha)) {
throw new Error("job.workflow_sha must be a full lowercase commit SHA");
}
const outputPath = process.env.GITHUB_OUTPUT;
if (!outputPath) {
throw new Error("GITHUB_OUTPUT is required");
}
fs.appendFileSync(
outputPath,
`workflow_repository=${job.workflow_repository}\nworkflow_sha=${job.workflow_sha}\n`,
);
NODE
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.ref || github.ref }}
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
- name: Build install-smoke CI manifest
id: manifest
env:
OPENCLAW_CI_WORKFLOW_BUN_GLOBAL_INSTALL_SMOKE: ${{ inputs.run_bun_global_install_smoke || 'false' }}
run: |
set -euo pipefail
workflow_bun_global_install_smoke="${OPENCLAW_CI_WORKFLOW_BUN_GLOBAL_INSTALL_SMOKE:-false}"
docs_only=false
run_fast_install_smoke=true
run_full_install_smoke=true
run_bun_global_install_smoke="$workflow_bun_global_install_smoke"
run_install_smoke=true
target_sha="$(git rev-parse HEAD)"
dockerfile_image="openclaw-dockerfile-smoke-local:${target_sha}"
{
echo "docs_only=$docs_only"
echo "run_install_smoke=$run_install_smoke"
echo "run_fast_install_smoke=$run_fast_install_smoke"
echo "run_full_install_smoke=$run_full_install_smoke"
echo "run_bun_global_install_smoke=$run_bun_global_install_smoke"
echo "target_sha=$target_sha"
echo "dockerfile_image=$dockerfile_image"
} >> "$GITHUB_OUTPUT"
install-smoke-fast:
needs: [preflight]
if: needs.preflight.outputs.run_fast_install_smoke == 'true' && needs.preflight.outputs.run_full_install_smoke != 'true'
runs-on: ubuntu-24.04
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
OPENCLAW_DOCKER_E2E_REQUIRE_LOCAL_IMAGE: "1"
steps:
- name: Checkout CLI
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.target_sha }}
persist-credentials: false
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000
# Keep release smoke builds bounded and log-producing. The Blacksmith
# build action can leave jobs in-progress without step logs when a remote
# builder stalls; an explicit buildx invocation fails closed instead.
- name: Build root Dockerfile smoke image
run: |
timeout --kill-after=30s 45m docker buildx build \
--progress=plain \
--load \
--build-arg OPENCLAW_EXTENSIONS=matrix \
-t openclaw-dockerfile-smoke:local \
-t openclaw-ext-smoke:local \
-f ./Dockerfile \
.
- name: Run root Dockerfile CLI smoke
run: |
timeout --kill-after=30s 20m docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
const fs = require(\"node:fs\");
const path = require(\"node:path\");
const YAML = require(\"yaml\");
const workspace = YAML.parse(fs.readFileSync(\"/app/pnpm-workspace.yaml\", \"utf8\")) ?? {};
for (const [dep, rel] of Object.entries(workspace.patchedDependencies ?? {})) {
const absolute = path.join(\"/app\", rel);
if (!fs.existsSync(absolute)) {
throw new Error(\"missing patch for \" + dep + \": \" + rel);
}
}
"
'
- name: Run agents delete shared workspace Docker CLI smoke
env:
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD: "1"
run: bash scripts/e2e/agents-delete-shared-workspace-docker.sh
- name: Run Docker gateway network e2e
env:
OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE: openclaw-dockerfile-smoke:local
OPENCLAW_GATEWAY_NETWORK_E2E_SKIP_BUILD: "1"
run: bash scripts/e2e/gateway-network-docker.sh
- name: Smoke test Dockerfile with matrix extension build arg
run: |
timeout --kill-after=30s 20m docker run --rm --entrypoint sh openclaw-ext-smoke:local -lc '
which openclaw &&
openclaw --version &&
node -e "
const Module = require(\"node:module\");
const matrixPackage = require(\"/app/extensions/matrix/package.json\");
const requireFromMatrix = Module.createRequire(\"/app/extensions/matrix/package.json\");
const runtimeDeps = Object.keys(matrixPackage.dependencies ?? {});
if (runtimeDeps.length === 0) {
throw new Error(
\"matrix package has no declared runtime dependencies; smoke cannot validate install mirroring\",
);
}
for (const dep of runtimeDeps) {
requireFromMatrix.resolve(dep);
}
const { spawnSync } = require(\"node:child_process\");
const run = spawnSync(\"openclaw\", [\"plugins\", \"list\", \"--json\"], { encoding: \"utf8\" });
if (run.status !== 0) {
process.stderr.write(run.stderr || run.stdout || \"plugins list failed\\n\");
process.exit(run.status ?? 1);
}
const parsed = JSON.parse(run.stdout);
const matrix = (parsed.plugins || []).find((entry) => entry.id === \"matrix\");
if (!matrix) {
throw new Error(\"matrix plugin missing from bundled plugin list\");
}
const matrixDiag = (parsed.diagnostics || []).filter(
(diag) =>
typeof diag.source === \"string\" &&
diag.source.includes(\"/extensions/matrix\") &&
typeof diag.message === \"string\" &&
diag.message.includes(\"extension entry escapes package directory\"),
);
if (matrixDiag.length > 0) {
throw new Error(
\"unexpected matrix diagnostics: \" +
matrixDiag.map((diag) => diag.message).join(\"; \"),
);
}
"
'
root_dockerfile_image:
needs: [preflight]
if: needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: ubuntu-24.04
permissions:
contents: read
packages: read
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 }}
image_ref: ${{ steps.image.outputs.image_ref }}
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout CLI
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.target_sha }}
persist-credentials: false
- name: Checkout trusted image artifact helper
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ needs.preflight.outputs.workflow_repository }}
ref: ${{ needs.preflight.outputs.workflow_sha }}
path: .release-harness
persist-credentials: false
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000
- name: Build local root Dockerfile smoke image
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
run: |
timeout --kill-after=30s 45m docker buildx build \
--progress=plain \
--load \
--build-arg OPENCLAW_EXTENSIONS=matrix \
-t "$IMAGE_REF" \
-f ./Dockerfile \
.
- name: Pack root Dockerfile image artifact
id: image_artifact
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
TARGET_SHA: ${{ needs.preflight.outputs.target_sha }}
WORKFLOW_SHA: ${{ needs.preflight.outputs.workflow_sha }}
run: |
set -euo pipefail
artifact_dir="${RUNNER_TEMP}/install-smoke-root-image"
artifact_name="install-smoke-root-image-${TARGET_SHA:0:12}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
bash .release-harness/scripts/docker/shared-image-artifact.sh \
pack "$artifact_dir" install-smoke-root "$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"
} >> "$GITHUB_OUTPUT"
- name: Upload root Dockerfile 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
- name: Record root image output
id: image
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
run: echo "image_ref=$IMAGE_REF" >> "$GITHUB_OUTPUT"
- name: Summarize root image
env:
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
TARGET_SHA: ${{ needs.preflight.outputs.target_sha }}
run: |
{
echo "## Root Dockerfile smoke image"
echo
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Image: \`${IMAGE_REF}\`"
echo "- Transport: immutable workflow artifact"
echo "- Artifact: \`${{ steps.image_artifact.outputs.artifact_name }}\`"
} >> "$GITHUB_STEP_SUMMARY"
root_dockerfile_image_ready:
needs: [preflight, root_dockerfile_image]
if: always() && needs.preflight.result == 'success' && needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Verify root Dockerfile image preparation
env:
PREPARE_RESULT: ${{ needs.root_dockerfile_image.result }}
run: |
set -euo pipefail
if [[ "$PREPARE_RESULT" != "success" ]]; then
echo "Root Dockerfile image preparation ended with ${PREPARE_RESULT}." >&2
exit 1
fi
qr_package_install_smoke:
needs: [preflight]
if: needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: ubuntu-24.04
steps:
- name: Checkout CLI
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.target_sha }}
persist-credentials: false
- name: Run QR package install smoke
env:
OPENCLAW_QR_SMOKE_FORCE_INSTALL: "1"
run: bash scripts/e2e/qr-import-docker.sh
root_dockerfile_smokes:
needs: [preflight, root_dockerfile_image, root_dockerfile_image_ready]
if: needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: ubuntu-24.04
env:
OPENCLAW_DOCKER_E2E_REQUIRE_LOCAL_IMAGE: "1"
steps:
- name: Checkout CLI
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.target_sha }}
persist-credentials: false
- name: Checkout trusted image artifact helper
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ needs.preflight.outputs.workflow_repository }}
ref: ${{ needs.preflight.outputs.workflow_sha }}
path: .release-harness
persist-credentials: false
- name: Validate root Dockerfile image artifact binding
env:
ARCHIVE_SHA256: ${{ needs.root_dockerfile_image.outputs.archive_sha256 }}
ARTIFACT_DIGEST: ${{ needs.root_dockerfile_image.outputs.artifact_digest }}
ARTIFACT_ID: ${{ needs.root_dockerfile_image.outputs.artifact_id }}
ARTIFACT_NAME: ${{ needs.root_dockerfile_image.outputs.artifact_name }}
ARTIFACT_RUN_ATTEMPT: ${{ needs.root_dockerfile_image.outputs.artifact_run_attempt }}
ARTIFACT_RUN_ID: ${{ needs.root_dockerfile_image.outputs.artifact_run_id }}
GH_TOKEN: ${{ github.token }}
TARGET_SHA: ${{ needs.preflight.outputs.target_sha }}
run: |
set -euo pipefail
[[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]] || {
echo "Root image artifact ID is missing or invalid." >&2
exit 1
}
[[ "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]] || {
echo "Root image artifact digest is missing or invalid." >&2
exit 1
}
[[ "$ARCHIVE_SHA256" =~ ^[a-f0-9]{64}$ ]] || {
echo "Root image archive SHA-256 is missing or invalid." >&2
exit 1
}
[[ "$ARTIFACT_RUN_ID" =~ ^[1-9][0-9]*$ ]] || {
echo "Root image artifact run ID is missing or invalid." >&2
exit 1
}
[[ "$ARTIFACT_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]] || {
echo "Root image artifact run attempt is missing or invalid." >&2
exit 1
}
expected_artifact_name="install-smoke-root-image-${TARGET_SHA:0:12}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}"
[[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || {
echo "Root image artifact name does not match the target and producer run attempt." >&2
exit 1
}
artifact_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")"
jq -e \
--arg digest "sha256:${ARTIFACT_DIGEST}" \
--arg id "$ARTIFACT_ID" \
--arg name "$ARTIFACT_NAME" \
--arg run_id "$ARTIFACT_RUN_ID" \
'
(.id | tostring) == $id and
.name == $name and
.expired == false and
.digest == $digest and
(.workflow_run.id | tostring) == $run_id
' <<< "$artifact_json" >/dev/null || {
echo "Root image artifact identity does not match the requested immutable tuple." >&2
exit 1
}
attempt_json="$(
gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}"
)"
jq -e \
--arg attempt "$ARTIFACT_RUN_ATTEMPT" \
--arg run_id "$ARTIFACT_RUN_ID" \
'(.id | tostring) == $run_id and (.run_attempt | tostring) == $attempt' \
<<< "$attempt_json" >/dev/null || {
echo "Root image artifact producer run attempt does not match the requested tuple." >&2
exit 1
}
- name: Download root Dockerfile image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
artifact-ids: ${{ needs.root_dockerfile_image.outputs.artifact_id }}
path: ${{ runner.temp }}/install-smoke-root-image
run-id: ${{ needs.root_dockerfile_image.outputs.artifact_run_id }}
github-token: ${{ github.token }}
- name: Verify and load root Dockerfile image artifact
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256: ${{ needs.root_dockerfile_image.outputs.archive_sha256 }}
OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT: ${{ needs.root_dockerfile_image.outputs.artifact_run_attempt }}
OPENCLAW_SHARED_IMAGE_RUN_ID: ${{ needs.root_dockerfile_image.outputs.artifact_run_id }}
TARGET_SHA: ${{ needs.preflight.outputs.target_sha }}
WORKFLOW_SHA: ${{ needs.preflight.outputs.workflow_sha }}
run: |
set -euo pipefail
bash .release-harness/scripts/docker/shared-image-artifact.sh \
load "${RUNNER_TEMP}/install-smoke-root-image" install-smoke-root \
"$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF"
- name: Require local root Dockerfile image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: docker image inspect "$IMAGE_REF" >/dev/null
- name: Run root Dockerfile CLI smoke
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: |
timeout --kill-after=30s 20m docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
which openclaw &&
openclaw --version &&
node -e "
const fs = require(\"node:fs\");
const path = require(\"node:path\");
const YAML = require(\"yaml\");
const workspace = YAML.parse(fs.readFileSync(\"/app/pnpm-workspace.yaml\", \"utf8\")) ?? {};
for (const [dep, rel] of Object.entries(workspace.patchedDependencies ?? {})) {
const absolute = path.join(\"/app\", rel);
if (!fs.existsSync(absolute)) {
throw new Error(\"missing patch for \" + dep + \": \" + rel);
}
}
"
'
- name: Run agents delete shared workspace Docker CLI smoke
env:
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD: "1"
run: bash scripts/e2e/agents-delete-shared-workspace-docker.sh
- name: Run Docker gateway network e2e
env:
OPENCLAW_GATEWAY_NETWORK_E2E_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_GATEWAY_NETWORK_E2E_SKIP_BUILD: "1"
run: bash scripts/e2e/gateway-network-docker.sh
- name: Smoke test Dockerfile with matrix extension build arg
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: |
timeout --kill-after=30s 20m docker run --rm --entrypoint sh "$IMAGE_REF" -lc '
which openclaw &&
openclaw --version &&
node -e "
const Module = require(\"node:module\");
const matrixPackage = require(\"/app/extensions/matrix/package.json\");
const requireFromMatrix = Module.createRequire(\"/app/extensions/matrix/package.json\");
const runtimeDeps = Object.keys(matrixPackage.dependencies ?? {});
if (runtimeDeps.length === 0) {
throw new Error(
\"matrix package has no declared runtime dependencies; smoke cannot validate install mirroring\",
);
}
for (const dep of runtimeDeps) {
requireFromMatrix.resolve(dep);
}
const { spawnSync } = require(\"node:child_process\");
const run = spawnSync(\"openclaw\", [\"plugins\", \"list\", \"--json\"], { encoding: \"utf8\" });
if (run.status !== 0) {
process.stderr.write(run.stderr || run.stdout || \"plugins list failed\\n\");
process.exit(run.status ?? 1);
}
const parsed = JSON.parse(run.stdout);
const matrix = (parsed.plugins || []).find((entry) => entry.id === \"matrix\");
if (!matrix) {
throw new Error(\"matrix plugin missing from bundled plugin list\");
}
const matrixDiag = (parsed.diagnostics || []).filter(
(diag) =>
typeof diag.source === \"string\" &&
diag.source.includes(\"/extensions/matrix\") &&
typeof diag.message === \"string\" &&
diag.message.includes(\"extension entry escapes package directory\"),
);
if (matrixDiag.length > 0) {
throw new Error(
\"unexpected matrix diagnostics: \" +
matrixDiag.map((diag) => diag.message).join(\"; \"),
);
}
"
'
installer_smoke:
needs: [preflight, root_dockerfile_image, root_dockerfile_image_ready]
if: needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: ubuntu-24.04
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
OPENCLAW_DOCKER_E2E_REQUIRE_LOCAL_IMAGE: "1"
steps:
- name: Checkout trusted installer harness
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
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
with:
ref: ${{ needs.preflight.outputs.target_sha }}
path: candidate
persist-credentials: false
- name: Checkout trusted image artifact helper
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ needs.preflight.outputs.workflow_repository }}
ref: ${{ needs.preflight.outputs.workflow_sha }}
path: .release-harness
persist-credentials: false
- name: Validate root Dockerfile image artifact binding
env:
ARCHIVE_SHA256: ${{ needs.root_dockerfile_image.outputs.archive_sha256 }}
ARTIFACT_DIGEST: ${{ needs.root_dockerfile_image.outputs.artifact_digest }}
ARTIFACT_ID: ${{ needs.root_dockerfile_image.outputs.artifact_id }}
ARTIFACT_NAME: ${{ needs.root_dockerfile_image.outputs.artifact_name }}
ARTIFACT_RUN_ATTEMPT: ${{ needs.root_dockerfile_image.outputs.artifact_run_attempt }}
ARTIFACT_RUN_ID: ${{ needs.root_dockerfile_image.outputs.artifact_run_id }}
GH_TOKEN: ${{ github.token }}
TARGET_SHA: ${{ needs.preflight.outputs.target_sha }}
run: |
set -euo pipefail
[[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]] || {
echo "Root image artifact ID is missing or invalid." >&2
exit 1
}
[[ "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]] || {
echo "Root image artifact digest is missing or invalid." >&2
exit 1
}
[[ "$ARCHIVE_SHA256" =~ ^[a-f0-9]{64}$ ]] || {
echo "Root image archive SHA-256 is missing or invalid." >&2
exit 1
}
[[ "$ARTIFACT_RUN_ID" =~ ^[1-9][0-9]*$ ]] || {
echo "Root image artifact run ID is missing or invalid." >&2
exit 1
}
[[ "$ARTIFACT_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]] || {
echo "Root image artifact run attempt is missing or invalid." >&2
exit 1
}
expected_artifact_name="install-smoke-root-image-${TARGET_SHA:0:12}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}"
[[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || {
echo "Root image artifact name does not match the target and producer run attempt." >&2
exit 1
}
artifact_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")"
jq -e \
--arg digest "sha256:${ARTIFACT_DIGEST}" \
--arg id "$ARTIFACT_ID" \
--arg name "$ARTIFACT_NAME" \
--arg run_id "$ARTIFACT_RUN_ID" \
'
(.id | tostring) == $id and
.name == $name and
.expired == false and
.digest == $digest and
(.workflow_run.id | tostring) == $run_id
' <<< "$artifact_json" >/dev/null || {
echo "Root image artifact identity does not match the requested immutable tuple." >&2
exit 1
}
attempt_json="$(
gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}"
)"
jq -e \
--arg attempt "$ARTIFACT_RUN_ATTEMPT" \
--arg run_id "$ARTIFACT_RUN_ID" \
'(.id | tostring) == $run_id and (.run_attempt | tostring) == $attempt' \
<<< "$attempt_json" >/dev/null || {
echo "Root image artifact producer run attempt does not match the requested tuple." >&2
exit 1
}
- name: Download root Dockerfile image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
artifact-ids: ${{ needs.root_dockerfile_image.outputs.artifact_id }}
path: ${{ runner.temp }}/install-smoke-root-image
run-id: ${{ needs.root_dockerfile_image.outputs.artifact_run_id }}
github-token: ${{ github.token }}
- name: Verify and load root Dockerfile image artifact
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256: ${{ needs.root_dockerfile_image.outputs.archive_sha256 }}
OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT: ${{ needs.root_dockerfile_image.outputs.artifact_run_attempt }}
OPENCLAW_SHARED_IMAGE_RUN_ID: ${{ needs.root_dockerfile_image.outputs.artifact_run_id }}
TARGET_SHA: ${{ needs.preflight.outputs.target_sha }}
WORKFLOW_SHA: ${{ needs.preflight.outputs.workflow_sha }}
run: |
set -euo pipefail
bash .release-harness/scripts/docker/shared-image-artifact.sh \
load "${RUNNER_TEMP}/install-smoke-root-image" install-smoke-root \
"$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF"
- name: Require local root Dockerfile image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: docker image inspect "$IMAGE_REF" >/dev/null
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000
- name: Build installer smoke image
run: |
timeout --kill-after=30s 20m docker buildx build \
--progress=plain \
--load \
-t openclaw-install-smoke:local \
-f ./scripts/docker/install-sh-smoke/Dockerfile \
./scripts/docker
- name: Build installer non-root image
run: |
timeout --kill-after=30s 20m docker buildx build \
--progress=plain \
--load \
-t openclaw-install-nonroot:local \
-f ./scripts/docker/install-sh-nonroot/Dockerfile \
./scripts/docker
- name: Setup Node environment for installer smoke
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-deps: "true"
- name: Run installer docker tests
env:
OPENCLAW_INSTALL_SMOKE_ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog }}
OPENCLAW_INSTALL_URL: file:///tmp/openclaw-install.sh
OPENCLAW_INSTALL_CLI_URL: file:///tmp/openclaw-install-cli.sh
OPENCLAW_NO_ONBOARD: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_CLI: "0"
OPENCLAW_INSTALL_SMOKE_SKIP_IMAGE_BUILD: "1"
OPENCLAW_INSTALL_NONROOT_SKIP_IMAGE_BUILD: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT: "0"
OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1"
OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS: "1"
OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE: ${{ inputs.update_baseline_version || 'latest' }}
OPENCLAW_INSTALL_SMOKE_UPDATE_DIST_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_INSTALL_SMOKE_UPDATE_SKIP_LOCAL_BUILD: "1"
OPENCLAW_INSTALL_SMOKE_SOURCE_DIR: ${{ github.workspace }}/candidate
run: bash scripts/test-install-sh-docker.sh
- name: Run Rocky Linux installer smoke
run: |
timeout --kill-after=30s 20m docker run --rm \
--platform linux/amd64 \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/candidate/scripts/install.sh:/tmp/install.sh:ro" \
rockylinux:9@sha256:d644d203142cd5b54ad2a83a203e1dee68af2229f8fe32f52a30c6e1d3c3a9e0 \
bash -lc 'dnf install -y -q ca-certificates tar gzip xz findutils which sudo >/dev/null && bash /tmp/install.sh --install-method npm --version latest --no-onboard --no-prompt --verify && openclaw --version'
- name: Run Rocky Linux CLI installer smoke
run: |
timeout --kill-after=30s 20m docker run --rm \
--platform linux/amd64 \
-e OPENCLAW_NO_ONBOARD=1 \
-e OPENCLAW_NO_PROMPT=1 \
-v "$PWD/candidate/scripts/install-cli.sh:/tmp/install-cli.sh:ro" \
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'
bun_global_install_smoke:
needs: [preflight, root_dockerfile_image, root_dockerfile_image_ready]
if: needs.preflight.outputs.run_full_install_smoke == 'true' && needs.preflight.outputs.run_bun_global_install_smoke == 'true'
runs-on: ubuntu-24.04
env:
OPENCLAW_DOCKER_E2E_REQUIRE_LOCAL_IMAGE: "1"
steps:
- name: Checkout CLI
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.target_sha }}
persist-credentials: false
- name: Checkout trusted image artifact helper
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
repository: ${{ needs.preflight.outputs.workflow_repository }}
ref: ${{ needs.preflight.outputs.workflow_sha }}
path: .release-harness
persist-credentials: false
- name: Validate root Dockerfile image artifact binding
env:
ARCHIVE_SHA256: ${{ needs.root_dockerfile_image.outputs.archive_sha256 }}
ARTIFACT_DIGEST: ${{ needs.root_dockerfile_image.outputs.artifact_digest }}
ARTIFACT_ID: ${{ needs.root_dockerfile_image.outputs.artifact_id }}
ARTIFACT_NAME: ${{ needs.root_dockerfile_image.outputs.artifact_name }}
ARTIFACT_RUN_ATTEMPT: ${{ needs.root_dockerfile_image.outputs.artifact_run_attempt }}
ARTIFACT_RUN_ID: ${{ needs.root_dockerfile_image.outputs.artifact_run_id }}
GH_TOKEN: ${{ github.token }}
TARGET_SHA: ${{ needs.preflight.outputs.target_sha }}
run: |
set -euo pipefail
[[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]] || {
echo "Root image artifact ID is missing or invalid." >&2
exit 1
}
[[ "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]] || {
echo "Root image artifact digest is missing or invalid." >&2
exit 1
}
[[ "$ARCHIVE_SHA256" =~ ^[a-f0-9]{64}$ ]] || {
echo "Root image archive SHA-256 is missing or invalid." >&2
exit 1
}
[[ "$ARTIFACT_RUN_ID" =~ ^[1-9][0-9]*$ ]] || {
echo "Root image artifact run ID is missing or invalid." >&2
exit 1
}
[[ "$ARTIFACT_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]] || {
echo "Root image artifact run attempt is missing or invalid." >&2
exit 1
}
expected_artifact_name="install-smoke-root-image-${TARGET_SHA:0:12}-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}"
[[ "$ARTIFACT_NAME" == "$expected_artifact_name" ]] || {
echo "Root image artifact name does not match the target and producer run attempt." >&2
exit 1
}
artifact_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")"
jq -e \
--arg digest "sha256:${ARTIFACT_DIGEST}" \
--arg id "$ARTIFACT_ID" \
--arg name "$ARTIFACT_NAME" \
--arg run_id "$ARTIFACT_RUN_ID" \
'
(.id | tostring) == $id and
.name == $name and
.expired == false and
.digest == $digest and
(.workflow_run.id | tostring) == $run_id
' <<< "$artifact_json" >/dev/null || {
echo "Root image artifact identity does not match the requested immutable tuple." >&2
exit 1
}
attempt_json="$(
gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}"
)"
jq -e \
--arg attempt "$ARTIFACT_RUN_ATTEMPT" \
--arg run_id "$ARTIFACT_RUN_ID" \
'(.id | tostring) == $run_id and (.run_attempt | tostring) == $attempt' \
<<< "$attempt_json" >/dev/null || {
echo "Root image artifact producer run attempt does not match the requested tuple." >&2
exit 1
}
- name: Download root Dockerfile image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
artifact-ids: ${{ needs.root_dockerfile_image.outputs.artifact_id }}
path: ${{ runner.temp }}/install-smoke-root-image
run-id: ${{ needs.root_dockerfile_image.outputs.artifact_run_id }}
github-token: ${{ github.token }}
- name: Verify and load root Dockerfile image artifact
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_SHARED_IMAGE_ARCHIVE_SHA256: ${{ needs.root_dockerfile_image.outputs.archive_sha256 }}
OPENCLAW_SHARED_IMAGE_RUN_ATTEMPT: ${{ needs.root_dockerfile_image.outputs.artifact_run_attempt }}
OPENCLAW_SHARED_IMAGE_RUN_ID: ${{ needs.root_dockerfile_image.outputs.artifact_run_id }}
TARGET_SHA: ${{ needs.preflight.outputs.target_sha }}
WORKFLOW_SHA: ${{ needs.preflight.outputs.workflow_sha }}
run: |
set -euo pipefail
bash .release-harness/scripts/docker/shared-image-artifact.sh \
load "${RUNNER_TEMP}/install-smoke-root-image" install-smoke-root \
"$TARGET_SHA" "$WORKFLOW_SHA" "$IMAGE_REF"
- name: Require local root Dockerfile image
env:
IMAGE_REF: ${{ needs.root_dockerfile_image.outputs.image_ref }}
run: docker image inspect "$IMAGE_REF" >/dev/null
- name: Setup Node environment for Bun smoke
uses: ./.github/actions/setup-node-env
with:
install-bun: "true"
install-deps: "true"
- name: Run Bun global install image-provider smoke
env:
OPENCLAW_BUN_GLOBAL_SMOKE_ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog }}
OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE: ${{ needs.root_dockerfile_image.outputs.image_ref }}
OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD: "0"
run: bash scripts/e2e/bun-global-install-smoke.sh
docker-e2e-fast:
needs: [preflight]
if: needs.preflight.outputs.run_fast_install_smoke == 'true' || needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 12
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout CLI
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.preflight.outputs.target_sha }}
persist-credentials: false
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
with:
max-cache-size-mb: 800000
- name: Setup Node environment for package smoke
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
install-deps: "true"