mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
ci: route narrow ci changes through fast path
This commit is contained in:
+64
-24
@@ -40,6 +40,7 @@ jobs:
|
||||
has_changed_extensions: ${{ steps.manifest.outputs.has_changed_extensions }}
|
||||
changed_extensions_matrix: ${{ steps.manifest.outputs.changed_extensions_matrix }}
|
||||
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
|
||||
run_checks_fast_core: ${{ steps.manifest.outputs.run_checks_fast_core }}
|
||||
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
|
||||
checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }}
|
||||
channel_contracts_matrix: ${{ steps.manifest.outputs.channel_contracts_matrix }}
|
||||
@@ -130,6 +131,9 @@ jobs:
|
||||
OPENCLAW_CI_RUN_MACOS: ${{ steps.changed_scope.outputs.run_macos || 'false' }}
|
||||
OPENCLAW_CI_RUN_ANDROID: ${{ steps.changed_scope.outputs.run_android || 'false' }}
|
||||
OPENCLAW_CI_RUN_WINDOWS: ${{ steps.changed_scope.outputs.run_windows || 'false' }}
|
||||
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ steps.changed_scope.outputs.run_node_fast_only || 'false' }}
|
||||
OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS: ${{ steps.changed_scope.outputs.run_node_fast_plugin_contracts || 'false' }}
|
||||
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
|
||||
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ steps.changed_scope.outputs.run_skills_python || 'false' }}
|
||||
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
|
||||
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
|
||||
@@ -173,12 +177,23 @@ jobs:
|
||||
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
|
||||
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
|
||||
const runNode = parseBoolean(process.env.OPENCLAW_CI_RUN_NODE) && !docsOnly;
|
||||
const runNodeFastOnly =
|
||||
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_ONLY);
|
||||
const runNodeFull = runNode && !runNodeFastOnly;
|
||||
const runNodeFastPluginContracts =
|
||||
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_PLUGIN_CONTRACTS);
|
||||
const runNodeFastCiRouting =
|
||||
runNode && parseBoolean(process.env.OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING);
|
||||
const runChecksFastCore = runNodeFull || runNodeFastPluginContracts || runNodeFastCiRouting;
|
||||
const runMacos =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository;
|
||||
const runAndroid =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository;
|
||||
const runWindows =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly && isCanonicalRepository;
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) &&
|
||||
!docsOnly &&
|
||||
!runNodeFastOnly &&
|
||||
isCanonicalRepository;
|
||||
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
|
||||
const runControlUiI18n =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
|
||||
@@ -191,7 +206,7 @@ jobs:
|
||||
? DEFAULT_EXTENSION_TEST_SHARD_COUNT
|
||||
: Math.max(DEFAULT_EXTENSION_TEST_SHARD_COUNT, 36);
|
||||
const extensionShardMatrix = createMatrix(
|
||||
runNode
|
||||
runNodeFull
|
||||
? createExtensionTestShards({
|
||||
shardCount: extensionTestShardCount,
|
||||
}).map((shard) => ({
|
||||
@@ -207,7 +222,33 @@ jobs:
|
||||
}))
|
||||
: [],
|
||||
);
|
||||
const nodeTestShards = runNode
|
||||
const checksFastCoreTasks = [];
|
||||
if (runNodeFull) {
|
||||
checksFastCoreTasks.push(
|
||||
{ check_name: "checks-fast-bundled", runtime: "node", task: "bundled" },
|
||||
{
|
||||
check_name: "checks-fast-contracts-plugins",
|
||||
runtime: "node",
|
||||
task: "contracts-plugins",
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if (runNodeFastPluginContracts) {
|
||||
checksFastCoreTasks.push({
|
||||
check_name: "checks-fast-contracts-plugins",
|
||||
runtime: "node",
|
||||
task: runNodeFastCiRouting ? "contracts-plugins-ci-routing" : "contracts-plugins",
|
||||
});
|
||||
} else if (runNodeFastCiRouting) {
|
||||
checksFastCoreTasks.push({
|
||||
check_name: "checks-fast-ci-routing",
|
||||
runtime: "node",
|
||||
task: "ci-routing",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const nodeTestShards = runNodeFull
|
||||
? createNodeTestShards().map((shard) => ({
|
||||
check_name: shard.checkName,
|
||||
runtime: "node",
|
||||
@@ -232,25 +273,17 @@ jobs:
|
||||
run_windows: runWindows,
|
||||
has_changed_extensions: hasChangedExtensions,
|
||||
changed_extensions_matrix: changedExtensionsMatrix,
|
||||
run_build_artifacts: runNode,
|
||||
run_checks_fast: runNode,
|
||||
checks_fast_core_matrix: createMatrix(
|
||||
runNode
|
||||
? [
|
||||
{ check_name: "checks-fast-bundled", runtime: "node", task: "bundled" },
|
||||
{
|
||||
check_name: "checks-fast-contracts-plugins",
|
||||
runtime: "node",
|
||||
task: "contracts-plugins",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
run_build_artifacts: runNodeFull,
|
||||
run_checks_fast_core: runChecksFastCore,
|
||||
run_checks_fast: runNodeFull,
|
||||
checks_fast_core_matrix: createMatrix(checksFastCoreTasks),
|
||||
channel_contracts_matrix: createMatrix(
|
||||
runNodeFull ? createChannelContractTestShards() : [],
|
||||
),
|
||||
channel_contracts_matrix: createMatrix(runNode ? createChannelContractTestShards() : []),
|
||||
checks_node_extensions_matrix: extensionShardMatrix,
|
||||
run_checks: runNode,
|
||||
run_checks: runNodeFull,
|
||||
checks_matrix: createMatrix(
|
||||
runNode
|
||||
runNodeFull
|
||||
? [
|
||||
{ check_name: "checks-node-channels", runtime: "node", task: "channels" },
|
||||
]
|
||||
@@ -269,9 +302,9 @@ jobs:
|
||||
}))
|
||||
: [],
|
||||
),
|
||||
run_check: runNode,
|
||||
run_check_additional: runNode,
|
||||
run_build_smoke: runNode,
|
||||
run_check: runNodeFull,
|
||||
run_check_additional: runNodeFull,
|
||||
run_build_smoke: runNodeFull,
|
||||
run_check_docs: docsChanged,
|
||||
run_control_ui_i18n: runControlUiI18n,
|
||||
run_skills_python_job: runSkillsPython,
|
||||
@@ -662,7 +695,7 @@ jobs:
|
||||
contents: read
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast == 'true'
|
||||
if: needs.preflight.outputs.run_checks_fast_core == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
@@ -739,6 +772,13 @@ jobs:
|
||||
contracts-plugins)
|
||||
pnpm test:contracts:plugins
|
||||
;;
|
||||
contracts-plugins-ci-routing)
|
||||
pnpm test:contracts:plugins
|
||||
pnpm test src/scripts/ci-changed-scope.test.ts test/scripts/test-projects.test.ts
|
||||
;;
|
||||
ci-routing)
|
||||
pnpm test src/scripts/ci-changed-scope.test.ts test/scripts/test-projects.test.ts
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported checks-fast task: $TASK" >&2
|
||||
exit 1
|
||||
@@ -1044,7 +1084,7 @@ jobs:
|
||||
contents: read
|
||||
name: checks-node-compat-node22
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_node == 'true' && github.event_name == 'push'
|
||||
if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'push'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
|
||||
@@ -90,6 +90,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.
|
||||
CI routing-only edits and narrow plugin contract helper/test-routing edits use a fast Node-only manifest path: preflight, security, and a single `checks-fast-core` task. That path avoids build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the changed files are limited to the routing or helper surfaces that the fast task exercises directly.
|
||||
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 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 agents delete shared-workspace CLI smoke, 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 and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. `main` pushes, including merge commits, do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`; it runs on the nightly schedule and from the release checks workflow, and manual `install-smoke` dispatches can opt into it, but pull requests and `main` pushes do not run it. 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 with a weighted scheduler and `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default main-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_PARALLELISM` and the provider-sensitive tail-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM`. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7` so npm install and multi-service lanes do not overcommit Docker while lighter lanes still fill available slots. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=0` or another millisecond value. The local aggregate preflights Docker, removes stale OpenClaw E2E containers, emits active-lane status, persists lane timings for longest-first ordering, and supports `OPENCLAW_DOCKER_ALL_DRY_RUN=1` for scheduler inspection. It stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. 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 bundled update matrix is split by update target so repeated npm update and doctor repair passes can shard with other bundled checks.
|
||||
|
||||
|
||||
@@ -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 {{ runFastOnly: boolean; runPluginContracts: boolean; runCiRouting: boolean }} NodeFastScope */
|
||||
/** @typedef {{ runFastInstallSmoke: boolean; runFullInstallSmoke: boolean }} InstallSmokeScope */
|
||||
|
||||
const FULL_SCOPE = {
|
||||
@@ -49,6 +50,13 @@ const FAST_INSTALL_SMOKE_SCOPE_RE =
|
||||
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)\//;
|
||||
const NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE =
|
||||
/^(src\/plugins\/contracts\/(?:inventory\/bundled-capability-metadata|registry)\.ts$|test\/helpers\/plugins\/tts-contract-suites\.ts$|scripts\/test-projects(?:\.test-support)?\.mjs$|test\/scripts\/test-projects\.test\.ts$)/;
|
||||
const NODE_FAST_CI_ROUTING_SCOPE_RE =
|
||||
/^(scripts\/ci-changed-scope\.mjs$|src\/scripts\/ci-changed-scope\.test\.ts$|\.github\/workflows\/ci\.yml$)/;
|
||||
const NODE_FAST_SCOPE_RE = new RegExp(
|
||||
`${NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE.source}|${NODE_FAST_CI_ROUTING_SCOPE_RE.source}`,
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {string[]} changedPaths
|
||||
@@ -144,6 +152,42 @@ export function detectChangedScope(changedPaths) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} changedPaths
|
||||
* @returns {NodeFastScope}
|
||||
*/
|
||||
export function detectNodeFastScope(changedPaths) {
|
||||
if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
|
||||
return { runFastOnly: false, runPluginContracts: false, runCiRouting: false };
|
||||
}
|
||||
|
||||
let hasNonDocs = false;
|
||||
let runPluginContracts = false;
|
||||
let runCiRouting = false;
|
||||
|
||||
for (const rawPath of changedPaths) {
|
||||
const path = rawPath.trim();
|
||||
if (!path || DOCS_PATH_RE.test(path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
hasNonDocs = true;
|
||||
runPluginContracts ||= NODE_FAST_PLUGIN_CONTRACT_SCOPE_RE.test(path);
|
||||
runCiRouting ||= NODE_FAST_CI_ROUTING_SCOPE_RE.test(path);
|
||||
|
||||
if (!NODE_FAST_SCOPE_RE.test(path)) {
|
||||
return { runFastOnly: false, runPluginContracts: false, runCiRouting: false };
|
||||
}
|
||||
}
|
||||
|
||||
const runFastOnly = hasNonDocs && (runPluginContracts || runCiRouting);
|
||||
return {
|
||||
runFastOnly,
|
||||
runPluginContracts: runFastOnly && runPluginContracts,
|
||||
runCiRouting: runFastOnly && runCiRouting,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {InstallSmokeScope}
|
||||
@@ -211,6 +255,7 @@ export function writeGitHubOutput(
|
||||
runFastInstallSmoke: scope.runChangedSmoke,
|
||||
runFullInstallSmoke: scope.runChangedSmoke,
|
||||
},
|
||||
nodeFastScope = { runFastOnly: false, runPluginContracts: false, runCiRouting: false },
|
||||
) {
|
||||
if (!outputPath) {
|
||||
throw new Error("GITHUB_OUTPUT is required");
|
||||
@@ -221,6 +266,13 @@ export function writeGitHubOutput(
|
||||
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_node_fast_only=${nodeFastScope.runFastOnly}\n`, "utf8");
|
||||
appendFileSync(
|
||||
outputPath,
|
||||
`run_node_fast_plugin_contracts=${nodeFastScope.runPluginContracts}\n`,
|
||||
"utf8",
|
||||
);
|
||||
appendFileSync(outputPath, `run_node_fast_ci_routing=${nodeFastScope.runCiRouting}\n`, "utf8");
|
||||
appendFileSync(
|
||||
outputPath,
|
||||
`run_fast_install_smoke=${installSmokeScope.runFastInstallSmoke}\n`,
|
||||
@@ -268,6 +320,7 @@ if (isDirectRun()) {
|
||||
detectChangedScope(changedPaths),
|
||||
process.env.GITHUB_OUTPUT,
|
||||
detectInstallSmokeScope(changedPaths),
|
||||
detectNodeFastScope(changedPaths),
|
||||
);
|
||||
} catch {
|
||||
writeGitHubOutput(FULL_SCOPE);
|
||||
|
||||
@@ -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, detectInstallSmokeScope, listChangedPaths } =
|
||||
const { detectChangedScope, detectInstallSmokeScope, detectNodeFastScope, listChangedPaths } =
|
||||
(await import("../../scripts/ci-changed-scope.mjs")) as unknown as {
|
||||
detectChangedScope: (paths: string[]) => {
|
||||
runNode: boolean;
|
||||
@@ -20,6 +20,11 @@ const { detectChangedScope, detectInstallSmokeScope, listChangedPaths } =
|
||||
runFastInstallSmoke: boolean;
|
||||
runFullInstallSmoke: boolean;
|
||||
};
|
||||
detectNodeFastScope: (paths: string[]) => {
|
||||
runFastOnly: boolean;
|
||||
runPluginContracts: boolean;
|
||||
runCiRouting: boolean;
|
||||
};
|
||||
listChangedPaths: (base: string, head?: string) => string[];
|
||||
};
|
||||
|
||||
@@ -486,6 +491,54 @@ describe("detectChangedScope", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("identifies plugin contract helper changes as fast Node-only CI scope", () => {
|
||||
const bundledCapabilityMetadataPath = [
|
||||
"src/plugins/contracts",
|
||||
"inventory/bundled-capability-metadata.ts",
|
||||
].join("/");
|
||||
expect(
|
||||
detectNodeFastScope([
|
||||
bundledCapabilityMetadataPath,
|
||||
"src/plugins/contracts/registry.ts",
|
||||
"test/helpers/plugins/tts-contract-suites.ts",
|
||||
"scripts/test-projects.test-support.mjs",
|
||||
"test/scripts/test-projects.test.ts",
|
||||
]),
|
||||
).toEqual({
|
||||
runFastOnly: true,
|
||||
runPluginContracts: true,
|
||||
runCiRouting: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("identifies CI routing changes as fast Node-only CI scope", () => {
|
||||
expect(
|
||||
detectNodeFastScope([
|
||||
".github/workflows/ci.yml",
|
||||
"scripts/ci-changed-scope.mjs",
|
||||
"src/scripts/ci-changed-scope.test.ts",
|
||||
"docs/ci.md",
|
||||
]),
|
||||
).toEqual({
|
||||
runFastOnly: true,
|
||||
runPluginContracts: false,
|
||||
runCiRouting: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps broad source changes on the full Node CI scope", () => {
|
||||
expect(
|
||||
detectNodeFastScope([
|
||||
"src/plugins/contracts/manifest-loader.ts",
|
||||
"src/plugins/contracts/registry.ts",
|
||||
]),
|
||||
).toEqual({
|
||||
runFastOnly: false,
|
||||
runPluginContracts: false,
|
||||
runCiRouting: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("treats base and head as literal git args", () => {
|
||||
const markerPath = path.join(
|
||||
os.tmpdir(),
|
||||
@@ -527,6 +580,9 @@ describe("detectChangedScope", () => {
|
||||
run_windows: "false",
|
||||
run_skills_python: "false",
|
||||
run_changed_smoke: "false",
|
||||
run_node_fast_only: "false",
|
||||
run_node_fast_plugin_contracts: "false",
|
||||
run_node_fast_ci_routing: "false",
|
||||
run_fast_install_smoke: "false",
|
||||
run_full_install_smoke: "false",
|
||||
run_control_ui_i18n: "false",
|
||||
|
||||
Reference in New Issue
Block a user