ci: split install smoke fast path

This commit is contained in:
Peter Steinberger 2026-04-24 00:51:35 +01:00
parent 4ca5ef694f
commit 3a2f0e7a1a
No known key found for this signature in database
7 changed files with 242 additions and 28 deletions

View file

@ -5,6 +5,8 @@ on:
branches: [main]
pull_request:
types: [opened, reopened, synchronize, ready_for_review, converted_to_draft]
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
permissions:
@ -24,6 +26,8 @@ jobs:
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 }}
steps:
- name: Checkout
uses: actions/checkout@v6
@ -34,7 +38,7 @@ jobs:
submodules: false
- name: Ensure preflight base commit
if: github.event_name != 'workflow_dispatch'
if: github.event_name != 'workflow_dispatch' && github.event_name != 'schedule'
uses: ./.github/actions/ensure-base-commit
with:
base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }}
@ -46,7 +50,7 @@ jobs:
- name: Detect changed smoke scope
id: changed_scope
if: github.event_name != 'workflow_dispatch' && steps.docs_scope.outputs.docs_only != 'true'
if: github.event_name != 'workflow_dispatch' && github.event_name != 'schedule' && steps.docs_scope.outputs.docs_only != 'true'
shell: bash
run: |
set -euo pipefail
@ -63,26 +67,125 @@ jobs:
id: manifest
env:
OPENCLAW_CI_DOCS_ONLY: ${{ steps.docs_scope.outputs.docs_only }}
OPENCLAW_CI_FORCE_INSTALL_SMOKE: ${{ github.event_name == 'workflow_dispatch' && 'true' || 'false' }}
OPENCLAW_CI_RUN_CHANGED_SMOKE: ${{ steps.changed_scope.outputs.run_changed_smoke || 'false' }}
OPENCLAW_CI_FORCE_FULL_INSTALL_SMOKE: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event_name == 'push') && 'true' || 'false' }}
OPENCLAW_CI_RUN_FAST_INSTALL_SMOKE: ${{ steps.changed_scope.outputs.run_fast_install_smoke || steps.changed_scope.outputs.run_changed_smoke || 'false' }}
OPENCLAW_CI_RUN_FULL_INSTALL_SMOKE: ${{ steps.changed_scope.outputs.run_full_install_smoke || 'false' }}
run: |
docs_only="${OPENCLAW_CI_DOCS_ONLY:-false}"
force_install_smoke="${OPENCLAW_CI_FORCE_INSTALL_SMOKE:-false}"
run_changed_smoke="${OPENCLAW_CI_RUN_CHANGED_SMOKE:-false}"
force_full_install_smoke="${OPENCLAW_CI_FORCE_FULL_INSTALL_SMOKE:-false}"
run_changed_fast_install_smoke="${OPENCLAW_CI_RUN_FAST_INSTALL_SMOKE:-false}"
run_changed_full_install_smoke="${OPENCLAW_CI_RUN_FULL_INSTALL_SMOKE:-false}"
run_fast_install_smoke=false
run_full_install_smoke=false
run_install_smoke=false
if [ "$force_install_smoke" = "true" ]; then
if [ "$force_full_install_smoke" = "true" ]; then
run_fast_install_smoke=true
run_full_install_smoke=true
run_install_smoke=true
elif [ "$docs_only" != "true" ] && [ "$run_changed_smoke" = "true" ]; then
elif [ "$docs_only" != "true" ] && [ "$run_changed_full_install_smoke" = "true" ]; then
run_fast_install_smoke=true
run_full_install_smoke=true
run_install_smoke=true
elif [ "$docs_only" != "true" ] && [ "$run_changed_fast_install_smoke" = "true" ]; then
run_fast_install_smoke=true
run_install_smoke=true
fi
{
echo "docs_only=$docs_only"
echo "run_install_smoke=$run_install_smoke"
echo "run_fast_install_smoke=$run_fast_install_smoke"
echo "run_full_install_smoke=$run_full_install_smoke"
} >> "$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: blacksmith-16vcpu-ubuntu-2404
env:
DOCKER_BUILD_SUMMARY: "false"
DOCKER_BUILD_RECORD_UPLOAD: "false"
steps:
- name: Checkout CLI
uses: actions/checkout@v6
- name: Set up Blacksmith Docker Builder
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
# Blacksmith's builder owns the Docker layer cache; keep smoke builds off
# explicit gha cache directives so local tags still load cleanly.
- name: Build root Dockerfile smoke image
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
with:
context: .
file: ./Dockerfile
build-args: |
OPENCLAW_DOCKER_APT_UPGRADE=0
OPENCLAW_EXTENSIONS=matrix
tags: |
openclaw-dockerfile-smoke:local
openclaw-ext-smoke:local
load: true
push: false
provenance: false
- name: Run root Dockerfile CLI smoke
run: |
docker run --rm --entrypoint sh openclaw-dockerfile-smoke:local -lc 'which openclaw && openclaw --version'
- 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: |
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(\"; \"),
);
}
"
'
install-smoke:
needs: [preflight]
if: needs.preflight.outputs.run_install_smoke == 'true'
if: needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
env:
DOCKER_BUILD_SUMMARY: "false"
@ -224,7 +327,7 @@ jobs:
docker-e2e-fast:
needs: [preflight]
if: needs.preflight.outputs.run_install_smoke == 'true'
if: needs.preflight.outputs.run_fast_install_smoke == 'true' || needs.preflight.outputs.run_full_install_smoke == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 8
env:

View file

@ -91,7 +91,7 @@ Jobs are ordered so cheap checks fail before expensive ones run:
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`.
CI workflow edits validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes.
Windows Node checks are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes so they do not reserve a 16-vCPU Windows worker for coverage that is already exercised by the normal test shards.
The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It computes `run_install_smoke` from the narrower changed-smoke signal, so Docker/install smoke runs for install, packaging, container-relevant changes, bundled extension production changes, and the core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Test-only and docs-only edits do not reserve Docker workers. Its QR package smoke forces the Docker `pnpm install` layer to rerun while preserving the BuildKit pnpm store cache, so it still exercises installation without redownloading dependencies on every run. Its gateway-network e2e reuses the runtime image built earlier in the job, so it adds real container-to-container WebSocket coverage without adding another Docker build. Local `test:docker:all` prebuilds one shared live-test image and one shared `scripts/e2e/Dockerfile` built-app image, then runs the live/E2E smoke lanes in parallel with `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default concurrency of 4 with `OPENCLAW_DOCKER_ALL_PARALLELISM`. The local aggregate stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`. Startup- or provider-sensitive lanes run exclusively after the parallel pool. The reusable live/E2E workflow mirrors the shared-image pattern by building and pushing one SHA-tagged GHCR Docker E2E image before the Docker matrix, then running the matrix with `OPENCLAW_SKIP_DOCKER_BUILD=1`. The scheduled live/E2E workflow runs the full release-path Docker suite daily. QR and installer Docker tests keep their own install-focused Dockerfiles. A separate `docker-e2e-fast` job runs the bounded bundled-plugin Docker profile under a 120-second command timeout: setup-entry dependency repair plus synthetic bundled-loader failure isolation. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes.
The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 120-second command timeout. The full path keeps QR package install, Bun global install, and installer Docker/update coverage for `main` pushes, nightly scheduled runs, manual dispatches, and true installer/package/Docker changes. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image and one shared `scripts/e2e/Dockerfile` built-app image, then runs the live/E2E smoke lanes in parallel with `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default concurrency of 4 with `OPENCLAW_DOCKER_ALL_PARALLELISM`. The local aggregate stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`. Startup- or provider-sensitive lanes run exclusively after the parallel pool. The reusable live/E2E workflow mirrors the shared-image pattern by building and pushing one SHA-tagged GHCR Docker E2E image before the Docker matrix, then running the matrix with `OPENCLAW_SKIP_DOCKER_BUILD=1`. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The full bundled update/channel matrix remains manual/full-suite because it performs repeated real npm update and doctor repair passes.
Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by `scripts/check-changed.mjs`. That local gate is stricter about architecture boundaries than the broad CI platform scope: core production changes run core prod typecheck plus core tests, core test-only changes run only core test typecheck/tests, extension production changes run extension prod typecheck plus extension tests, and extension test-only changes run only extension test typecheck/tests. Public Plugin SDK or plugin-contract changes expand to extension validation because extensions depend on those core contracts. Release metadata-only version bumps run targeted version/config/root-dependency checks. Unknown root/config changes fail safe to all lanes.

View file

@ -8,6 +8,16 @@ export type ChangedScope = {
runControlUiI18n: boolean;
};
export type InstallSmokeScope = {
runFastInstallSmoke: boolean;
runFullInstallSmoke: boolean;
};
export function detectChangedScope(changedPaths: string[]): ChangedScope;
export function detectInstallSmokeScope(changedPaths: string[]): InstallSmokeScope;
export function listChangedPaths(base: string, head?: string): string[];
export function writeGitHubOutput(scope: ChangedScope, outputPath?: string): void;
export function writeGitHubOutput(
scope: ChangedScope,
outputPath?: string,
installSmokeScope?: InstallSmokeScope,
): void;

View file

@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
/** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */
/** @typedef {{ runFastInstallSmoke: boolean; runFullInstallSmoke: boolean }} InstallSmokeScope */
const FULL_SCOPE = {
runNode: true,
@ -43,10 +44,11 @@ const CONTROL_UI_I18N_SCOPE_RE =
/^(ui\/src\/i18n\/|scripts\/control-ui-i18n\.ts$|\.github\/workflows\/control-ui-locale-refresh\.yml$)/;
const NATIVE_ONLY_RE =
/^(apps\/android\/|apps\/ios\/|apps\/macos\/|apps\/macos-mlx-tts\/|apps\/shared\/|Swabble\/|appcast\.xml$)/;
const CHANGED_SMOKE_SCOPE_RE =
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/install\.sh$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|.*\.sh)$|src\/plugins\/bundled-runtime-deps\.ts$|extensions\/[^/]+\/package\.json$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
const CHANGED_SMOKE_RUNTIME_SCOPE_RE =
/^(src\/(?:channels|gateway|plugin-sdk|plugins)\/|extensions\/)/;
const FAST_INSTALL_SMOKE_SCOPE_RE =
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/postinstall-bundled-plugins\.mjs$|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|gateway-network-docker\.sh|bundled-channel-runtime-deps-docker\.sh)$|src\/plugins\/bundled-runtime-deps\.ts$|extensions\/[^/]+\/(?:package\.json|openclaw\.plugin\.json)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
const FULL_INSTALL_SMOKE_SCOPE_RE =
/^(Dockerfile$|\.npmrc$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|scripts\/ci-changed-scope\.mjs$|scripts\/install\.sh$|scripts\/test-install-sh-docker\.sh$|scripts\/docker\/|scripts\/e2e\/(?:Dockerfile(?:\.qr-import)?|qr-import-docker\.sh|bun-global-install-smoke\.sh)$|\.github\/workflows\/install-smoke\.yml$|\.github\/actions\/setup-node-env\/action\.yml$)/;
const FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE = /^src\/(?:channels|gateway|plugin-sdk|plugins)\//;
/**
* @param {string[]} changedPaths
@ -114,10 +116,7 @@ export function detectChangedScope(changedPaths) {
runWindows = true;
}
if (
CHANGED_SMOKE_SCOPE_RE.test(path) ||
(CHANGED_SMOKE_RUNTIME_SCOPE_RE.test(path) && !TEST_ONLY_PATH_RE.test(path))
) {
if (detectInstallSmokeScopeForPath(path).runFastInstallSmoke) {
runChangedSmoke = true;
}
@ -145,6 +144,42 @@ export function detectChangedScope(changedPaths) {
};
}
/**
* @param {string} path
* @returns {InstallSmokeScope}
*/
function detectInstallSmokeScopeForPath(path) {
const runFullInstallSmoke = FULL_INSTALL_SMOKE_SCOPE_RE.test(path);
const runFastInstallSmoke =
runFullInstallSmoke ||
FAST_INSTALL_SMOKE_SCOPE_RE.test(path) ||
(FAST_INSTALL_SMOKE_RUNTIME_SCOPE_RE.test(path) && !TEST_ONLY_PATH_RE.test(path));
return { runFastInstallSmoke, runFullInstallSmoke };
}
/**
* @param {string[]} changedPaths
* @returns {InstallSmokeScope}
*/
export function detectInstallSmokeScope(changedPaths) {
if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
return { runFastInstallSmoke: true, runFullInstallSmoke: true };
}
let runFastInstallSmoke = false;
let runFullInstallSmoke = false;
for (const rawPath of changedPaths) {
const path = rawPath.trim();
if (!path || DOCS_PATH_RE.test(path)) {
continue;
}
const pathScope = detectInstallSmokeScopeForPath(path);
runFastInstallSmoke ||= pathScope.runFastInstallSmoke;
runFullInstallSmoke ||= pathScope.runFullInstallSmoke;
}
return { runFastInstallSmoke, runFullInstallSmoke };
}
/**
* @param {string} base
* @param {string} [head]
@ -167,8 +202,16 @@ export function listChangedPaths(base, head = "HEAD") {
/**
* @param {ChangedScope} scope
* @param {string} [outputPath]
* @param {InstallSmokeScope} [installSmokeScope]
*/
export function writeGitHubOutput(scope, outputPath = process.env.GITHUB_OUTPUT) {
export function writeGitHubOutput(
scope,
outputPath = process.env.GITHUB_OUTPUT,
installSmokeScope = {
runFastInstallSmoke: scope.runChangedSmoke,
runFullInstallSmoke: scope.runChangedSmoke,
},
) {
if (!outputPath) {
throw new Error("GITHUB_OUTPUT is required");
}
@ -178,6 +221,16 @@ export function writeGitHubOutput(scope, outputPath = process.env.GITHUB_OUTPUT)
appendFileSync(outputPath, `run_windows=${scope.runWindows}\n`, "utf8");
appendFileSync(outputPath, `run_skills_python=${scope.runSkillsPython}\n`, "utf8");
appendFileSync(outputPath, `run_changed_smoke=${scope.runChangedSmoke}\n`, "utf8");
appendFileSync(
outputPath,
`run_fast_install_smoke=${installSmokeScope.runFastInstallSmoke}\n`,
"utf8",
);
appendFileSync(
outputPath,
`run_full_install_smoke=${installSmokeScope.runFullInstallSmoke}\n`,
"utf8",
);
appendFileSync(outputPath, `run_control_ui_i18n=${scope.runControlUiI18n}\n`, "utf8");
}
@ -211,7 +264,11 @@ if (isDirectRun()) {
writeGitHubOutput(EMPTY_SCOPE);
process.exit(0);
}
writeGitHubOutput(detectChangedScope(changedPaths));
writeGitHubOutput(
detectChangedScope(changedPaths),
process.env.GITHUB_OUTPUT,
detectInstallSmokeScope(changedPaths),
);
} catch {
writeGitHubOutput(FULL_SCOPE);
}

View file

@ -27,4 +27,8 @@ declare module "../../scripts/ci-changed-scope.mjs" {
runChangedSmoke: boolean;
runControlUiI18n: boolean;
};
export function detectInstallSmokeScope(paths: string[]): {
runFastInstallSmoke: boolean;
runFullInstallSmoke: boolean;
};
}

View file

@ -5,7 +5,7 @@ import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { bundledPluginFile } from "../../test/helpers/bundled-plugin-paths.js";
const { detectChangedScope, listChangedPaths } =
const { detectChangedScope, detectInstallSmokeScope, listChangedPaths } =
(await import("../../scripts/ci-changed-scope.mjs")) as unknown as {
detectChangedScope: (paths: string[]) => {
runNode: boolean;
@ -16,6 +16,10 @@ const { detectChangedScope, listChangedPaths } =
runChangedSmoke: boolean;
runControlUiI18n: boolean;
};
detectInstallSmokeScope: (paths: string[]) => {
runFastInstallSmoke: boolean;
runFullInstallSmoke: boolean;
};
listChangedPaths: (base: string, head?: string) => string[];
};
@ -319,7 +323,7 @@ describe("detectChangedScope", () => {
runAndroid: false,
runWindows: false,
runSkillsPython: false,
runChangedSmoke: true,
runChangedSmoke: false,
runControlUiI18n: false,
});
expect(detectChangedScope(["scripts/postinstall-bundled-plugins.mjs"])).toEqual({
@ -351,7 +355,7 @@ describe("detectChangedScope", () => {
});
});
it("runs changed-smoke for Docker-covered core and extension runtime surfaces", () => {
it("runs changed-smoke for Docker-covered core runtime surfaces", () => {
expect(detectChangedScope(["src/plugins/loader.ts"])).toEqual({
runNode: true,
runMacos: false,
@ -394,11 +398,42 @@ describe("detectChangedScope", () => {
runAndroid: false,
runWindows: false,
runSkillsPython: false,
runChangedSmoke: true,
runChangedSmoke: false,
runControlUiI18n: false,
});
});
it("splits install smoke into fast and full scopes", () => {
expect(detectInstallSmokeScope([])).toEqual({
runFastInstallSmoke: true,
runFullInstallSmoke: true,
});
expect(detectInstallSmokeScope(["docs/ci.md"])).toEqual({
runFastInstallSmoke: false,
runFullInstallSmoke: false,
});
expect(detectInstallSmokeScope(["scripts/install.sh"])).toEqual({
runFastInstallSmoke: true,
runFullInstallSmoke: true,
});
expect(detectInstallSmokeScope(["Dockerfile"])).toEqual({
runFastInstallSmoke: true,
runFullInstallSmoke: true,
});
expect(detectInstallSmokeScope([bundledPluginFile("matrix", "package.json")])).toEqual({
runFastInstallSmoke: true,
runFullInstallSmoke: false,
});
expect(detectInstallSmokeScope(["src/plugins/loader.ts"])).toEqual({
runFastInstallSmoke: true,
runFullInstallSmoke: false,
});
expect(detectInstallSmokeScope([bundledPluginFile("matrix", "index.ts")])).toEqual({
runFastInstallSmoke: false,
runFullInstallSmoke: false,
});
});
it("keeps changed-smoke off for runtime-surface tests", () => {
expect(detectChangedScope(["src/plugins/loader.test.ts"])).toEqual({
runNode: true,
@ -483,6 +518,8 @@ describe("detectChangedScope", () => {
run_windows: "false",
run_skills_python: "false",
run_changed_smoke: "false",
run_fast_install_smoke: "false",
run_full_install_smoke: "false",
run_control_ui_i18n: "false",
});
});

View file

@ -148,8 +148,11 @@ describe("bun global install smoke", () => {
"OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE: openclaw-dockerfile-smoke:local",
);
expect(workflow).toContain("format('{0}-manual-{1}', github.workflow, github.run_id)");
expect(workflow).toContain("OPENCLAW_CI_FORCE_INSTALL_SMOKE");
expect(workflow).toContain('if [ "$force_install_smoke" = "true" ]; then');
expect(workflow).toContain("OPENCLAW_CI_FORCE_FULL_INSTALL_SMOKE");
expect(workflow).toContain('if [ "$force_full_install_smoke" = "true" ]; then');
expect(workflow).toContain("install-smoke-fast:");
expect(workflow).toContain("run_fast_install_smoke");
expect(workflow).toContain("run_full_install_smoke");
expect(workflow).toContain('OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL: "1"');
});
});