Repository navigation
Hourly Extension Tests #896
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Plugin Prerelease | |
| run-name: ${{ github.event_name == 'schedule' && 'Hourly Extension Tests' || inputs.dispatch_id != '' && format('Plugin Prerelease {0}', inputs.dispatch_id) || 'Plugin Prerelease' }} | |
| on: | |
| schedule: | |
| - cron: "37 * * * *" | |
| workflow_dispatch: | |
| inputs: | |
| target_ref: | |
| description: Branch, tag, or full commit SHA to validate | |
| required: false | |
| default: main | |
| type: string | |
| expected_sha: | |
| description: Optional full commit SHA that target_ref must resolve to | |
| required: false | |
| default: "" | |
| type: string | |
| target_context_ref: | |
| description: Canonical release context for an exact-SHA frozen-target validation | |
| required: false | |
| default: "" | |
| type: string | |
| allow_frozen_target_scenario_omissions: | |
| description: Trusted opt-in to accept documented harness differences in a frozen target | |
| required: false | |
| default: false | |
| type: boolean | |
| full_release_validation: | |
| description: Enable release-only Docker prerelease lanes from Full Release Validation | |
| required: false | |
| default: false | |
| type: boolean | |
| test_runtime_policy: | |
| description: Extension batch runtime override (node, bun-all, or dual-all); empty uses Bun, or both runtimes for full release validation | |
| required: false | |
| default: "" | |
| type: string | |
| phase: | |
| description: Plugin prerelease phase to run | |
| required: false | |
| default: all | |
| type: choice | |
| options: | |
| - all | |
| - independent | |
| - candidate | |
| node_test_exclude_patterns_json: | |
| description: Full Release Validation-only exact plugin test paths omitted for a frozen target | |
| required: false | |
| default: "[]" | |
| type: string | |
| extension_test_exclude_patterns_json: | |
| description: Full Release Validation-only exact extension test paths omitted for a frozen target | |
| required: false | |
| default: "[]" | |
| type: string | |
| qualification_baselines_json: | |
| description: Captured exact upgrade and survivor baselines from the qualification request | |
| required: false | |
| default: "" | |
| type: string | |
| dispatch_id: | |
| description: Optional parent workflow dispatch identifier | |
| required: false | |
| default: "" | |
| type: string | |
| candidate_artifact_json: | |
| description: Immutable package and Docker image artifact tuple from Full Release Validation | |
| required: false | |
| default: "" | |
| type: string | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.event_name == 'schedule' && 'plugin-prerelease-hourly-main' || format('plugin-prerelease-{0}-{1}-{2}', inputs.target_ref, github.sha, inputs.phase) }} | |
| cancel-in-progress: ${{ github.event_name != 'schedule' && inputs.target_ref == 'main' }} | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| resolve_target: | |
| name: Resolve target ref | |
| if: github.event_name != 'schedule' || (github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main') | |
| runs-on: &linux_runner_ubuntu >- | |
| ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', | |
| toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('ubuntu-24.04') }} | |
| timeout-minutes: 5 | |
| outputs: | |
| checkout_revision: ${{ steps.resolve.outputs.sha }} | |
| steps: | |
| # Candidate planning executes repository code; admission must own a separate runner. | |
| - name: Checkout trusted ref resolver | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.sha }} | |
| path: workflow | |
| sparse-checkout: scripts/github/resolve-openclaw-ref.sh | |
| sparse-checkout-cone-mode: false | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| submodules: false | |
| - name: Resolve target SHA | |
| id: resolve | |
| env: | |
| TARGET_REF: ${{ github.event_name == 'schedule' && github.sha || inputs.target_ref }} | |
| EXPECTED_SHA: ${{ github.event_name == 'schedule' && github.sha || inputs.expected_sha }} | |
| OPENCLAW_REF_REMOTE: ${{ github.server_url }}/${{ github.repository }}.git | |
| run: | | |
| bash workflow/scripts/github/resolve-openclaw-ref.sh \ | |
| --ref "$TARGET_REF" \ | |
| --expected-sha "$EXPECTED_SHA" \ | |
| --github-output "$GITHUB_OUTPUT" | |
| preflight: | |
| name: Build plugin prerelease plan | |
| needs: [resolve_target] | |
| runs-on: *linux_runner_ubuntu | |
| timeout-minutes: 15 | |
| outputs: | |
| run_plugin_prerelease_suite: ${{ steps.manifest.outputs.run_plugin_prerelease_suite }} | |
| run_plugin_prerelease_static: ${{ steps.manifest.outputs.run_plugin_prerelease_static }} | |
| plugin_prerelease_static_matrix: ${{ steps.manifest.outputs.plugin_prerelease_static_matrix }} | |
| run_plugin_prerelease_node: ${{ steps.manifest.outputs.run_plugin_prerelease_node }} | |
| plugin_prerelease_node_matrix: ${{ steps.manifest.outputs.plugin_prerelease_node_matrix }} | |
| node_test_exclude_patterns_json: ${{ steps.node_test_exclusions.outputs.patterns_json }} | |
| extension_test_exclude_patterns_json: ${{ steps.node_test_exclusions.outputs.extension_patterns_json }} | |
| run_plugin_prerelease_extensions: ${{ steps.manifest.outputs.run_plugin_prerelease_extensions }} | |
| plugin_prerelease_extension_matrix: ${{ steps.manifest.outputs.plugin_prerelease_extension_matrix }} | |
| run_plugin_prerelease_inspector: ${{ steps.manifest.outputs.run_plugin_prerelease_inspector }} | |
| run_plugin_prerelease_docker: ${{ steps.manifest.outputs.run_plugin_prerelease_docker }} | |
| plugin_prerelease_docker_lanes: ${{ steps.manifest.outputs.plugin_prerelease_docker_lanes }} | |
| upgrade_baseline: ${{ steps.qualification_baselines.outputs.baseline }} | |
| upgrade_baselines: ${{ steps.qualification_baselines.outputs.baselines }} | |
| steps: | |
| - name: Validate phase inputs | |
| env: | |
| CANDIDATE_ARTIFACT_JSON: ${{ inputs.candidate_artifact_json }} | |
| EXPECTED_SHA: ${{ inputs.expected_sha }} | |
| FULL_RELEASE_VALIDATION: ${{ inputs.full_release_validation && 'true' || 'false' }} | |
| PHASE: ${{ github.event_name == 'schedule' && 'independent' || inputs.phase }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| case "$PHASE" in | |
| all|independent) exit 0 ;; | |
| candidate) ;; | |
| *) | |
| echo "phase must be one of: all, independent, candidate" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| if [[ "$FULL_RELEASE_VALIDATION" != "true" ]]; then | |
| echo "phase=candidate requires full_release_validation=true." >&2 | |
| exit 1 | |
| fi | |
| if ! jq -e \ | |
| --arg sha "$EXPECTED_SHA" \ | |
| 'def digits: tostring | test("^[1-9][0-9]*$"); | |
| def hex40: type == "string" and test("^[a-f0-9]{40}$"); | |
| def hex64: type == "string" and test("^[a-f0-9]{64}$"); | |
| (.packageArtifactName | type == "string" and length > 0) and | |
| (.packageArtifactId | digits) and | |
| (.packageArtifactDigest | hex64) and | |
| (.packageArtifactRunId | digits) and | |
| (.packageArtifactRunAttempt | digits) and | |
| (.packageFileName | type == "string" and test("^[A-Za-z0-9][A-Za-z0-9._-]*\\.tgz$")) and | |
| (.packageSourceSha | hex40) and | |
| ($sha == "" or .packageSourceSha == $sha) and | |
| (.packageSha256 | hex64) and | |
| (.packageVersion | type == "string" and length > 0) and | |
| (.imageArtifactName | type == "string" and length > 0) and | |
| (.imageArtifactId | digits) and | |
| (.imageArtifactDigest | hex64) and | |
| (.imageArtifactRunId | digits) and | |
| (.imageArtifactRunAttempt | digits) and | |
| (.imageArchiveSha256 | hex64)' \ | |
| <<< "$CANDIDATE_ARTIFACT_JSON" >/dev/null; then | |
| echo "phase=candidate requires the complete immutable package and Docker image artifact tuple." >&2 | |
| exit 1 | |
| fi | |
| - name: Setup supported Node runtime | |
| uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 | |
| with: | |
| node-version: "24.22.1" | |
| package-manager-cache: false | |
| - name: Checkout target | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.resolve_target.outputs.checkout_revision }} | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| persist-credentials: false | |
| submodules: false | |
| - &checkout_exclusion_tooling | |
| name: Checkout trusted test exclusion tooling | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.workflow_sha }} | |
| path: .frv-tooling | |
| sparse-checkout: | | |
| scripts/frv-test-exclusions.mjs | |
| scripts/lib/release-upgrade-baseline.mjs | |
| scripts/lib/release-version.mjs | |
| scripts/lib/canonical-json.mjs | |
| sparse-checkout-cone-mode: false | |
| persist-credentials: false | |
| - name: Validate captured qualification baselines | |
| id: qualification_baselines | |
| env: | |
| QUALIFICATION_BASELINES_JSON: ${{ inputs.qualification_baselines_json }} | |
| TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "$QUALIFICATION_BASELINES_JSON" ]]; then exit 0; fi | |
| captured="$(node .frv-tooling/scripts/lib/release-upgrade-baseline.mjs \ | |
| --candidate-version "$(node -p 'require("./package.json").version')" \ | |
| --target-context-ref "$TARGET_CONTEXT_REF" \ | |
| --qualification-baselines-json "$QUALIFICATION_BASELINES_JSON")" | |
| printf 'baseline=%s\n' "$(jq -er .upgradeBaseline <<< "$captured")" >> "$GITHUB_OUTPUT" | |
| printf 'baselines=%s\n' "$(jq -er '.upgradeSurvivorBaselines | join(" ")' <<< "$captured")" >> "$GITHUB_OUTPUT" | |
| - name: Validate frozen-target Node exclusions | |
| id: node_test_exclusions | |
| env: | |
| FULL_RELEASE_VALIDATION: ${{ inputs.full_release_validation && 'true' || 'false' }} | |
| NODE_TEST_EXCLUDE_PATTERNS_JSON: ${{ github.event_name == 'schedule' && '[]' || inputs.node_test_exclude_patterns_json }} | |
| EXTENSION_TEST_EXCLUDE_PATTERNS_JSON: ${{ github.event_name == 'schedule' && '[]' || inputs.extension_test_exclude_patterns_json }} | |
| run: | | |
| node --input-type=module <<'EOF' | |
| import { appendFileSync } from "node:fs"; | |
| import { parseTestExclusions } from "./.frv-tooling/scripts/frv-test-exclusions.mjs"; | |
| const patterns = parseTestExclusions(process.env.NODE_TEST_EXCLUDE_PATTERNS_JSON, "plugins"); | |
| const extensions = parseTestExclusions(process.env.EXTENSION_TEST_EXCLUDE_PATTERNS_JSON, "extensions"); | |
| if (process.env.FULL_RELEASE_VALIDATION !== "true" && (patterns.length || extensions.length)) { | |
| throw new Error("Test exclusions require full_release_validation=true"); | |
| } | |
| appendFileSync(process.env.GITHUB_OUTPUT, | |
| `patterns_json=${JSON.stringify(patterns)}\nextension_patterns_json=${JSON.stringify(extensions)}\n`); | |
| EOF | |
| - name: Setup manifest TypeScript runtime | |
| uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 | |
| with: | |
| node-version: "24.22.1" | |
| - name: Setup manifest pnpm | |
| uses: ./.github/actions/setup-pnpm-store-cache | |
| with: | |
| cache-mode: restore | |
| node-version: "24.22.1" | |
| - name: Install manifest dependencies | |
| run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts | |
| - name: Build plugin prerelease manifest | |
| id: manifest | |
| env: | |
| FULL_RELEASE_VALIDATION: ${{ inputs.full_release_validation && 'true' || 'false' }} | |
| OPENCLAW_CI_TEST_RUNTIME_POLICY: ${{ inputs.test_runtime_policy }} | |
| PHASE: ${{ github.event_name == 'schedule' && 'independent' || inputs.phase }} | |
| NODE_TEST_EXCLUDE_PATTERNS_JSON: ${{ steps.node_test_exclusions.outputs.patterns_json }} | |
| EXTENSION_TEST_EXCLUDE_PATTERNS_JSON: ${{ steps.node_test_exclusions.outputs.extension_patterns_json }} | |
| run: | | |
| node --import tsx --input-type=module <<'EOF' | |
| import { appendFileSync, existsSync, globSync } from "node:fs"; | |
| import path from "node:path"; | |
| const createMatrix = (include) => ({ include }); | |
| const outputPath = process.env.GITHUB_OUTPUT; | |
| const fullReleaseValidation = process.env.FULL_RELEASE_VALIDATION === "true"; | |
| const hourly = process.env.GITHUB_EVENT_NAME === "schedule"; | |
| const phase = process.env.PHASE ?? "all"; | |
| const runtimePolicy = process.env.OPENCLAW_CI_TEST_RUNTIME_POLICY?.trim(); | |
| if (runtimePolicy && !["node", "bun-all", "dual-all"].includes(runtimePolicy)) { | |
| throw new Error("test_runtime_policy must be node, bun-all, or dual-all"); | |
| } | |
| let pluginPrereleasePlan = { staticChecks: [], dockerLanes: [] }; | |
| let resolveExtensionRuntime = () => ({ test_runtime_policy: "node", requires_bun: false }); | |
| let extensionShards = []; | |
| let nodeShards = []; | |
| const targetPlanPaths = { | |
| "plugin-prerelease-test-plan": "./scripts/lib/plugin-prerelease-test-plan.mts", | |
| "extension-test-plan": "./scripts/lib/extension-test-plan.mts", | |
| "bundled-plugin-source-utils": "./scripts/lib/bundled-plugin-source-utils.mts", | |
| "ci-node-test-plan": "./scripts/lib/ci-node-test-plan.mts", | |
| }; | |
| const targetPlanPath = (name) => { | |
| const mtsPath = targetPlanPaths[name]; | |
| return existsSync(mtsPath) ? mtsPath : mtsPath.replace(/\.mts$/u, ".mjs"); | |
| }; | |
| try { | |
| const planner = await import( | |
| targetPlanPath("plugin-prerelease-test-plan") | |
| ); | |
| pluginPrereleasePlan = planner.assertPluginPrereleaseTestPlanComplete(); | |
| if (typeof planner.resolvePluginPrereleaseExtensionRuntime === "function") { | |
| resolveExtensionRuntime = planner.resolvePluginPrereleaseExtensionRuntime; | |
| } | |
| } catch (error) { | |
| const errorCode = | |
| error && typeof error === "object" && "code" in error ? error.code : ""; | |
| const moduleUrl = | |
| error && typeof error === "object" && "url" in error ? String(error.url) : ""; | |
| if ( | |
| errorCode === "ERR_MODULE_NOT_FOUND" && | |
| (moduleUrl.endsWith("/scripts/lib/plugin-prerelease-test-plan.mjs") || | |
| moduleUrl.endsWith("/scripts/lib/plugin-prerelease-test-plan.mts")) | |
| ) { | |
| console.warn( | |
| "Plugin prerelease plan unavailable in target ref; skipping static and Docker plugin prerelease lanes.", | |
| ); | |
| } else { | |
| throw error; | |
| } | |
| } | |
| try { | |
| const planner = await import(targetPlanPath("extension-test-plan")); | |
| const extensionVitestArgs = JSON.parse( | |
| process.env.EXTENSION_TEST_EXCLUDE_PATTERNS_JSON || "[]", | |
| ).map((file) => `--exclude=${file}`); | |
| const allExtensionShards = planner.createExtensionTestShards({ | |
| shardCount: planner.DEFAULT_EXTENSION_TEST_SHARD_COUNT, | |
| }); | |
| const batchedExtensionIds = new Set(allExtensionShards.flatMap((shard) => shard.extensionIds)); | |
| const hasJobSplitter = typeof planner.splitExtensionTestJobTargets === "function"; | |
| // Codex alone took 36-60 min on hosted release runners (FRV 36644909314 timed out), | |
| // so it gets file-bounded jobs like Telegram instead of one batch job. | |
| const fileSplitExtensionIds = ["telegram", "codex"].filter((id) => batchedExtensionIds.has(id)); | |
| const fileSplitPlanGroups = hasJobSplitter | |
| ? fileSplitExtensionIds.flatMap((extensionId) => | |
| planner.createExtensionTestShards({ extensionIds: [extensionId], shardCount: 1 }) | |
| .flatMap((shard) => shard.planGroups) | |
| .map((group) => ({ ...group, family: extensionId })), | |
| ) | |
| : []; | |
| const sourcePlanGroups = new Map(); | |
| if (hasJobSplitter) { | |
| const sources = await import(targetPlanPath("bundled-plugin-source-utils")); | |
| // August frozen targets expose the public collector but not its tracked projection. | |
| const candidates = typeof sources.collectTrackedBundledPluginSourceCandidates === "function" | |
| ? sources.collectTrackedBundledPluginSourceCandidates(process.cwd()) | |
| : sources.collectBundledPluginSources({ repoRoot: process.cwd() }); | |
| if (!candidates) throw new Error("Cannot enumerate bundled plugin source metadata"); | |
| // Older planners omit manifest-only plugins; supplement only their missing owners. | |
| const sourceRoots = candidates.filter((entry) => entry.manifestPath && !entry.packageJsonPath && !batchedExtensionIds.has(entry.dirName)) | |
| .map((entry) => `extensions/${entry.dirName}`); | |
| const sourceFiles = [ | |
| ...planner.listExtensionTestFilesForRoots(sourceRoots), | |
| ...planner.listExtensionTestFilesForRoots(["extensions"]) | |
| .filter((file) => path.posix.dirname(file) === "extensions"), | |
| ]; | |
| for (const file of sourceFiles) { | |
| const config = planner.resolveExtensionTestConfig(file); | |
| const group = sourcePlanGroups.get(config) ?? { config, roots: [] }; | |
| group.roots.push(file); | |
| sourcePlanGroups.set(config, group); | |
| } | |
| } | |
| const genericExtensionIds = hasJobSplitter | |
| ? allExtensionShards | |
| .flatMap((shard) => shard.extensionIds) | |
| .filter((extensionId) => !fileSplitExtensionIds.includes(extensionId)) | |
| : []; | |
| const batchShards = await Promise.all(( | |
| hasJobSplitter | |
| ? planner.createExtensionTestShards({ | |
| extensionIds: genericExtensionIds, | |
| shardCount: planner.DEFAULT_EXTENSION_TEST_SHARD_COUNT, | |
| }) | |
| : allExtensionShards | |
| ).map(async (shard) => { | |
| const runtime = await resolveExtensionRuntime({ | |
| planGroups: shard.planGroups, | |
| fullReleaseValidation, | |
| vitestArgs: extensionVitestArgs, | |
| }); | |
| if (runtimePolicy && (runtime.test_runtime_policy !== runtimePolicy || runtime.requires_bun !== (runtimePolicy !== "node"))) { | |
| throw new Error(`Target ref does not support requested test_runtime_policy: ${runtimePolicy}`); | |
| } | |
| return { | |
| check_name: shard.checkName, | |
| extensions_csv: shard.extensionIds.join(","), | |
| exclusion_configs: shard.planGroups.map((group) => ({ | |
| config: group.config, | |
| includePatterns: group.roots.map((root) => root.endsWith(".test.ts") ? root : `${root}/**/*.test.ts`), | |
| })), | |
| vitest_config: "", | |
| vitest_max_workers: shard.extensionIds.some((extensionId) => | |
| extensionId.startsWith("memory-"), | |
| ) | |
| ? 4 | |
| : 1, | |
| runner: shard.extensionIds.some((extensionId) => extensionId.startsWith("memory-")) | |
| ? "blacksmith-16vcpu-ubuntu-2404" | |
| : [0, 1, 2, 3].includes(shard.index) | |
| ? "blacksmith-8vcpu-ubuntu-2404" | |
| : "blacksmith-4vcpu-ubuntu-2404", | |
| shard_index: shard.index + 1, | |
| task: "extensions-batch", | |
| ...runtime, | |
| }; | |
| })); | |
| const telegramShards = []; | |
| for (const group of [ | |
| ...fileSplitPlanGroups, | |
| ...[...sourcePlanGroups.values()].map((group) => ({ ...group, family: "source" })), | |
| ]) { | |
| const vitestConfig = (await import(`./${group.config}`)).default; | |
| const testConfig = vitestConfig.test ?? {}; | |
| const testDir = testConfig.dir ?? process.cwd(); | |
| const exclude = (testConfig.exclude ?? []).map((pattern) => | |
| path.isAbsolute(pattern) | |
| ? path.relative(testDir, pattern).replaceAll("\\", "/") | |
| : pattern, | |
| ); | |
| const testFiles = globSync(testConfig.include ?? [], { cwd: testDir, exclude }) | |
| .map((file) => | |
| path.relative(process.cwd(), path.resolve(testDir, file)).replaceAll("\\", "/"), | |
| ) | |
| .filter((file) => | |
| group.roots.some((root) => file === root || file.startsWith(`${root}/`)), | |
| ) | |
| .sort(); | |
| const chunks = planner.splitExtensionTestJobTargets(group.config, testFiles); | |
| for (const includePatterns of chunks) { | |
| if (includePatterns.length === 0) { | |
| continue; | |
| } | |
| const index = telegramShards.length; | |
| telegramShards.push({ | |
| check_name: `checks-node-extensions-${group.family}-shard-${index + 1}`, | |
| extensions_csv: [...new Set(includePatterns.map((file) => file.split("/")[1]))].join(","), | |
| includePatterns, | |
| vitest_config: group.config, | |
| exclusion_configs: [{ config: group.config, includePatterns }], | |
| vitest_max_workers: 1, | |
| runner: "blacksmith-8vcpu-ubuntu-2404", | |
| shard_index: planner.DEFAULT_EXTENSION_TEST_SHARD_COUNT + index + 1, | |
| task: "extension-file-shard", | |
| // Plain pnpm test does not consume the batch dual-runtime policy. | |
| test_runtime_policy: "node", | |
| requires_bun: false, | |
| }); | |
| } | |
| } | |
| extensionShards = [...batchShards, ...telegramShards]; | |
| } catch (error) { | |
| const errorCode = | |
| error && typeof error === "object" && "code" in error ? error.code : ""; | |
| const moduleUrl = | |
| error && typeof error === "object" && "url" in error ? String(error.url) : ""; | |
| if ( | |
| errorCode === "ERR_MODULE_NOT_FOUND" && | |
| (moduleUrl.endsWith("/scripts/lib/extension-test-plan.mjs") || | |
| moduleUrl.endsWith("/scripts/lib/extension-test-plan.mts")) | |
| ) { | |
| console.warn( | |
| "Extension test plan unavailable in target ref; skipping extension prerelease shards.", | |
| ); | |
| } else { | |
| throw error; | |
| } | |
| } | |
| try { | |
| const { createNodeTestShards } = await import(targetPlanPath("ci-node-test-plan")); | |
| nodeShards = createNodeTestShards({ | |
| includeReleaseOnlyPluginShards: true, | |
| }) | |
| .filter((shard) => shard.shardName === "agentic-plugins") | |
| .map((shard) => ({ | |
| check_name: shard.checkName, | |
| runtime: "node", | |
| task: "test-shard", | |
| shard_name: shard.shardName, | |
| configs: shard.configs, | |
| includePatterns: shard.includePatterns, | |
| runner: shard.runner, | |
| })); | |
| } catch (error) { | |
| const errorCode = | |
| error && typeof error === "object" && "code" in error ? error.code : ""; | |
| const moduleUrl = | |
| error && typeof error === "object" && "url" in error ? String(error.url) : ""; | |
| if ( | |
| errorCode === "ERR_MODULE_NOT_FOUND" && | |
| (moduleUrl.endsWith("/scripts/lib/ci-node-test-plan.mjs") || | |
| moduleUrl.endsWith("/scripts/lib/ci-node-test-plan.mts")) | |
| ) { | |
| console.warn( | |
| "Node test plan unavailable in target ref; skipping release-only plugin Node shard.", | |
| ); | |
| } else { | |
| throw error; | |
| } | |
| } | |
| const staticChecks = pluginPrereleasePlan.staticChecks.map((check) => ({ | |
| check_name: check.checkName, | |
| command: check.command, | |
| task: check.check, | |
| })); | |
| const dockerLanes = pluginPrereleasePlan.dockerLanes; | |
| const runIndependent = phase !== "candidate"; | |
| const runCandidate = phase !== "independent"; | |
| const runStatic = runIndependent && !hourly && staticChecks.length > 0; | |
| const runNode = runIndependent && !hourly && nodeShards.length > 0; | |
| const runExtensions = runIndependent && extensionShards.length > 0; | |
| if (hourly && !runExtensions) { | |
| throw new Error("Hourly plugin validation requires the complete extension test inventory"); | |
| } | |
| const runInspector = runIndependent && !hourly && (runStatic || runNode || runExtensions); | |
| if (runIndependent && ((process.env.NODE_TEST_EXCLUDE_PATTERNS_JSON || "[]") !== "[]" || (process.env.EXTENSION_TEST_EXCLUDE_PATTERNS_JSON || "[]") !== "[]")) { | |
| const { parseTestExclusions, validateTestExclusions } = await import("./.frv-tooling/scripts/frv-test-exclusions.mjs"); | |
| await validateTestExclusions({ | |
| paths: parseTestExclusions(process.env.NODE_TEST_EXCLUDE_PATTERNS_JSON, "plugins"), | |
| configs: nodeShards.flatMap((row) => row.configs.map((config) => ({ config, includePatterns: row.includePatterns }))), | |
| }); | |
| await validateTestExclusions({ | |
| paths: parseTestExclusions(process.env.EXTENSION_TEST_EXCLUDE_PATTERNS_JSON, "extensions"), | |
| configs: extensionShards.flatMap((row) => row.exclusion_configs), | |
| }); | |
| } | |
| const runDocker = runCandidate && fullReleaseValidation && dockerLanes.length > 0; | |
| const runSuite = runStatic || runNode || runExtensions || runInspector || runDocker; | |
| const manifest = { | |
| run_plugin_prerelease_suite: runSuite, | |
| run_plugin_prerelease_static: runStatic, | |
| plugin_prerelease_static_matrix: createMatrix(staticChecks), | |
| run_plugin_prerelease_node: runNode, | |
| plugin_prerelease_node_matrix: createMatrix(nodeShards), | |
| run_plugin_prerelease_extensions: runExtensions, | |
| plugin_prerelease_extension_matrix: createMatrix(extensionShards), | |
| run_plugin_prerelease_inspector: runInspector, | |
| run_plugin_prerelease_docker: runDocker, | |
| plugin_prerelease_docker_lanes: dockerLanes.join(" "), | |
| }; | |
| for (const [key, value] of Object.entries(manifest)) { | |
| appendFileSync( | |
| outputPath, | |
| `${key}=${typeof value === "string" ? value : JSON.stringify(value)}\n`, | |
| "utf8", | |
| ); | |
| } | |
| EOF | |
| plugin-prerelease-static-shard: | |
| permissions: | |
| contents: read | |
| # Job-level skips happen before matrix expansion; retain a unique owner name. | |
| name: ${{ matrix.check_name || 'plugin-prerelease-static-shard' }} | |
| needs: [resolve_target, preflight] | |
| if: needs.preflight.outputs.run_plugin_prerelease_static == 'true' | |
| runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404'))) || (github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404') }} | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.plugin_prerelease_static_matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.resolve_target.outputs.checkout_revision }} | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| persist-credentials: false | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| cache-mode: restore | |
| install-bun: "false" | |
| - name: Run plugin prerelease static shard | |
| env: | |
| PLUGIN_PRERELEASE_COMMAND: ${{ matrix.command }} | |
| PLUGIN_PRERELEASE_TASK: ${{ matrix.task }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "Running ${PLUGIN_PRERELEASE_TASK}: ${PLUGIN_PRERELEASE_COMMAND}" | |
| bash -c "$PLUGIN_PRERELEASE_COMMAND" | |
| plugin-prerelease-node-shard: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name || 'plugin-prerelease-node-shard' }} | |
| needs: [resolve_target, preflight] | |
| if: needs.preflight.outputs.run_plugin_prerelease_node == 'true' | |
| runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (matrix.runner || 'ubuntu-24.04')))) || (github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (matrix.runner || 'ubuntu-24.04')) }} | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.preflight.outputs.plugin_prerelease_node_matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.resolve_target.outputs.checkout_revision }} | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| persist-credentials: false | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| cache-mode: restore | |
| install-bun: "false" | |
| - name: Configure Node test resources | |
| run: echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV" | |
| - *checkout_exclusion_tooling | |
| - name: Run release-only plugin Node shard | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=8192 | |
| NODE_TEST_EXCLUDE_PATTERNS_JSON: ${{ needs.preflight.outputs.node_test_exclude_patterns_json }} | |
| OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }} | |
| OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }} | |
| OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }} | |
| OPENCLAW_TEST_PROJECTS_PARALLEL: "2" | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| node --input-type=module <<'EOF' | |
| import { spawnSync } from "node:child_process"; | |
| import { writeFileSync } from "node:fs"; | |
| import { join } from "node:path"; | |
| const configs = JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]"); | |
| if (!Array.isArray(configs) || configs.length === 0) { | |
| console.error("Missing node test shard configs"); | |
| process.exit(1); | |
| } | |
| const includePatterns = JSON.parse( | |
| process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null", | |
| ); | |
| const excludePatterns = JSON.parse( | |
| process.env.NODE_TEST_EXCLUDE_PATTERNS_JSON ?? "[]", | |
| ); | |
| if ( | |
| !Array.isArray(excludePatterns) || | |
| excludePatterns.some((pattern) => typeof pattern !== "string") | |
| ) { | |
| console.error("Invalid frozen-target Node exclusions"); | |
| process.exit(1); | |
| } | |
| const childEnv = { ...process.env }; | |
| if (Array.isArray(includePatterns) && includePatterns.length > 0) { | |
| const includeFile = join( | |
| process.env.RUNNER_TEMP ?? ".", | |
| `node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`, | |
| ); | |
| writeFileSync(includeFile, JSON.stringify(includePatterns), "utf8"); | |
| childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile; | |
| } | |
| childEnv.FRV_TEST_EXCLUDE_PATHS_JSON = JSON.stringify(excludePatterns); | |
| childEnv.FRV_TEST_EXCLUDE_SCOPE = "plugins"; | |
| childEnv.FRV_TEST_CONFIGS_JSON = JSON.stringify(configs); | |
| const result = spawnSync(process.execPath, [".frv-tooling/scripts/frv-test-exclusions.mjs", "run", "--", "pnpm", "test", "--", ...configs], { | |
| env: childEnv, | |
| stdio: "inherit", | |
| }); | |
| process.exit(result.status ?? 1); | |
| EOF | |
| plugin-prerelease-extension-shard: | |
| permissions: | |
| contents: read | |
| name: ${{ matrix.check_name || 'plugin-prerelease-extension-shard' }} | |
| needs: [resolve_target, preflight] | |
| if: needs.preflight.outputs.run_plugin_prerelease_extensions == 'true' | |
| runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || matrix.runner))) || (github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || matrix.runner) }} | |
| timeout-minutes: ${{ matrix.test_runtime_policy == 'dual-all' && 120 || 60 }} | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 12 | |
| matrix: ${{ fromJson(needs.preflight.outputs.plugin_prerelease_extension_matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.resolve_target.outputs.checkout_revision }} | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| persist-credentials: false | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| cache-mode: restore | |
| install-bun: "false" | |
| - name: Setup pinned Bun test runtime | |
| if: matrix.requires_bun == true | |
| uses: ./.github/actions/setup-test-bun | |
| - *checkout_exclusion_tooling | |
| - name: Run extension shard | |
| env: | |
| NODE_OPTIONS: --max-old-space-size=8192 | |
| OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" | |
| OPENCLAW_EXTENSION_BATCH_PARALLEL: 2 | |
| OPENCLAW_VITEST_MAX_WORKERS: ${{ matrix.vitest_max_workers }} | |
| OPENCLAW_EXTENSION_BATCH: ${{ matrix.extensions_csv }} | |
| OPENCLAW_EXTENSION_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }} | |
| OPENCLAW_EXTENSION_TASK: ${{ matrix.task }} | |
| OPENCLAW_CI_TEST_RUNTIME_POLICY: ${{ matrix.test_runtime_policy || 'node' }} | |
| FRV_TEST_EXCLUDE_PATHS_JSON: ${{ needs.preflight.outputs.extension_test_exclude_patterns_json }} | |
| FRV_TEST_EXCLUDE_SCOPE: extensions | |
| FRV_TEST_CONFIGS_JSON: ${{ toJson(matrix.exclusion_configs) }} | |
| OPENCLAW_EXTENSION_VITEST_CONFIG: ${{ matrix.vitest_config }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| case "$OPENCLAW_EXTENSION_TASK" in | |
| extensions-batch) | |
| # The batch planner also uses exclusions when composing process targets. | |
| mapfile -t exclude_args < <(jq -r '.[] | "--exclude=" + .' <<< "$FRV_TEST_EXCLUDE_PATHS_JSON") | |
| exclusion_options=() | |
| if (( ${#exclude_args[@]} > 0 )); then exclusion_options=(--allow-empty-after-exclude); fi | |
| node .frv-tooling/scripts/frv-test-exclusions.mjs run -- pnpm test:extensions:batch "$OPENCLAW_EXTENSION_BATCH" "${exclusion_options[@]}" -- "${exclude_args[@]}" | |
| ;; | |
| extension-file-shard) | |
| include_file="${RUNNER_TEMP}/extension-test-include-${GITHUB_JOB}.json" | |
| trap 'rm -f -- "$include_file"' EXIT | |
| INCLUDE_FILE="$include_file" node --input-type=module <<'EOF' | |
| import { writeFileSync } from "node:fs"; | |
| const patterns = JSON.parse(process.env.OPENCLAW_EXTENSION_INCLUDE_PATTERNS_JSON); | |
| if (!Array.isArray(patterns) || patterns.length === 0) { | |
| throw new Error("Invalid extension file shard"); | |
| } | |
| writeFileSync(process.env.INCLUDE_FILE, JSON.stringify(patterns), "utf8"); | |
| EOF | |
| OPENCLAW_TEST_PROJECTS_PARALLEL=2 \ | |
| OPENCLAW_VITEST_INCLUDE_FILE="$include_file" \ | |
| node .frv-tooling/scripts/frv-test-exclusions.mjs run -- pnpm test -- "$OPENCLAW_EXTENSION_VITEST_CONFIG" | |
| trap - EXIT | |
| rm -f -- "$include_file" | |
| ;; | |
| *) | |
| echo "Unknown extension test task: $OPENCLAW_EXTENSION_TASK" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| plugin-prerelease-inspector: | |
| permissions: | |
| contents: read | |
| name: plugin-prerelease-inspector | |
| needs: [resolve_target, preflight] | |
| if: needs.preflight.outputs.run_plugin_prerelease_inspector == 'true' | |
| continue-on-error: true | |
| runs-on: *linux_runner_ubuntu | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ needs.resolve_target.outputs.checkout_revision }} | |
| fetch-depth: 1 | |
| fetch-tags: false | |
| persist-credentials: false | |
| submodules: false | |
| - name: Setup Node environment | |
| uses: ./.github/actions/setup-node-env | |
| with: | |
| cache-mode: restore | |
| install-bun: "false" | |
| - name: Run plugin inspector advisory sweep | |
| env: | |
| OPENCLAW_PLUGIN_INSPECTOR_VERSION: "0.3.27" | |
| OPENCLAW_PLUGIN_INSPECTOR_ROOT: .artifacts/plugin-inspector | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$OPENCLAW_PLUGIN_INSPECTOR_ROOT" | |
| set +e | |
| node --input-type=module <<'EOF' | |
| import { existsSync } from "node:fs"; | |
| import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; | |
| import path from "node:path"; | |
| const artifactRoot = process.env.OPENCLAW_PLUGIN_INSPECTOR_ROOT; | |
| if (!artifactRoot) { | |
| throw new Error("OPENCLAW_PLUGIN_INSPECTOR_ROOT is required"); | |
| } | |
| const readJson = async (filePath) => JSON.parse(await readFile(filePath, "utf8")); | |
| const inferSeams = (pluginManifest, packageJson) => { | |
| const contracts = Object.keys(pluginManifest?.contracts ?? {}); | |
| if (contracts.includes("tools")) { | |
| return ["dynamic-tool"]; | |
| } | |
| const openclawPackage = packageJson?.openclaw ?? {}; | |
| if (openclawPackage.extensions || openclawPackage.runtimeExtensions) { | |
| return ["plugin-runtime"]; | |
| } | |
| return ["plugin-metadata"]; | |
| }; | |
| const extensionRoot = path.resolve("extensions"); | |
| const fixtures = []; | |
| for (const entry of await readdir(extensionRoot, { withFileTypes: true })) { | |
| if (!entry.isDirectory()) { | |
| continue; | |
| } | |
| const relativePath = `extensions/${entry.name}`; | |
| const packagePath = path.join(extensionRoot, entry.name, "package.json"); | |
| const manifestPath = path.join(extensionRoot, entry.name, "openclaw.plugin.json"); | |
| if (!existsSync(packagePath) || !existsSync(manifestPath)) { | |
| continue; | |
| } | |
| const packageJson = await readJson(packagePath); | |
| const pluginManifest = await readJson(manifestPath); | |
| fixtures.push({ | |
| id: entry.name, | |
| name: pluginManifest.name ?? packageJson.name ?? entry.name, | |
| path: relativePath, | |
| priority: "high", | |
| repo: "local", | |
| seams: inferSeams(pluginManifest, packageJson), | |
| why: "bundled OpenClaw plugin prerelease advisory fixture", | |
| }); | |
| } | |
| fixtures.sort((left, right) => left.id.localeCompare(right.id)); | |
| if (fixtures.length === 0) { | |
| throw new Error("No bundled plugin fixtures found under extensions/"); | |
| } | |
| await mkdir(artifactRoot, { recursive: true }); | |
| const config = `${JSON.stringify( | |
| { | |
| version: 1, | |
| submoduleRoot: ".", | |
| openclaw: { | |
| defaultCheckoutPath: ".", | |
| }, | |
| fixtures, | |
| }, | |
| null, | |
| 2, | |
| )}\n`; | |
| await writeFile("plugin-inspector.config.json", config, "utf8"); | |
| await writeFile(path.join(artifactRoot, "plugin-inspector.config.json"), config, "utf8"); | |
| EOF | |
| config_status=$? | |
| set -e | |
| echo "$config_status" > "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/config-exit-code.txt" | |
| if [ "$config_status" -eq 0 ]; then | |
| set +e | |
| npm exec --yes "@openclaw/plugin-inspector@${OPENCLAW_PLUGIN_INSPECTOR_VERSION}" -- ci \ | |
| --config plugin-inspector.config.json \ | |
| --openclaw "$PWD" \ | |
| --out "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/reports" \ | |
| --json \ | |
| > "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/plugin-inspector-stdout.json" \ | |
| 2> "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/plugin-inspector-stderr.log" | |
| inspector_status=$? | |
| set -e | |
| else | |
| inspector_status=127 | |
| echo "Skipped plugin-inspector because config generation failed." \ | |
| > "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/plugin-inspector-stderr.log" | |
| fi | |
| echo "$inspector_status" > "$OPENCLAW_PLUGIN_INSPECTOR_ROOT/exit-code.txt" | |
| node --input-type=module <<'EOF' | |
| import { existsSync } from "node:fs"; | |
| import { appendFile, readFile, writeFile } from "node:fs/promises"; | |
| import path from "node:path"; | |
| const artifactRoot = process.env.OPENCLAW_PLUGIN_INSPECTOR_ROOT; | |
| const summaryPath = path.join(artifactRoot, "reports/plugin-inspector-ci-summary.json"); | |
| const markdownPath = path.join(artifactRoot, "reports/plugin-inspector-ci-summary.md"); | |
| const configExitCode = (await readFile(path.join(artifactRoot, "config-exit-code.txt"), "utf8")).trim(); | |
| const exitCode = (await readFile(path.join(artifactRoot, "exit-code.txt"), "utf8")).trim(); | |
| const lines = [ | |
| "## Plugin Inspector Advisory", | |
| "", | |
| `Inspector: @openclaw/plugin-inspector@${process.env.OPENCLAW_PLUGIN_INSPECTOR_VERSION}`, | |
| `Config exit code: ${configExitCode}`, | |
| `Exit code: ${exitCode}`, | |
| ]; | |
| if (existsSync(summaryPath)) { | |
| const summary = JSON.parse(await readFile(summaryPath, "utf8")); | |
| lines.push( | |
| `Status: ${String(summary.status ?? "unknown").toUpperCase()}`, | |
| "", | |
| "| Metric | Count |", | |
| "| --- | ---: |", | |
| `| Hard breakages | ${summary.summary?.breakages ?? 0} |`, | |
| `| Issues | ${summary.summary?.issues ?? 0} |`, | |
| `| P0 issues | ${summary.summary?.p0Issues ?? 0} |`, | |
| `| P1 issues | ${summary.summary?.p1Issues ?? 0} |`, | |
| `| Compat gaps | ${summary.summary?.compatGaps ?? 0} |`, | |
| `| Inspector gaps | ${summary.summary?.inspectorGaps ?? 0} |`, | |
| "", | |
| "This job is informational; Plugin Prerelease blocking status is unchanged.", | |
| ); | |
| await writeFile(path.join(artifactRoot, "advisory-summary.md"), `${lines.join("\n")}\n`, "utf8"); | |
| if (existsSync(markdownPath)) { | |
| lines.push("", "### Full inspector summary", ""); | |
| lines.push(await readFile(markdownPath, "utf8")); | |
| } | |
| } else { | |
| lines.push("", "No plugin-inspector CI summary was produced.", ""); | |
| lines.push("This job is informational; inspect the uploaded stdout/stderr artifacts."); | |
| await writeFile(path.join(artifactRoot, "advisory-summary.md"), `${lines.join("\n")}\n`, "utf8"); | |
| } | |
| await appendFile(process.env.GITHUB_STEP_SUMMARY, `${lines.join("\n")}\n`, "utf8"); | |
| EOF | |
| - name: Upload plugin inspector advisory artifacts | |
| if: always() | |
| uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| with: | |
| name: plugin-inspector-advisory | |
| path: .artifacts/plugin-inspector/** | |
| if-no-files-found: warn | |
| plugin-prerelease-docker-suite: | |
| name: plugin-prerelease-docker-suite | |
| needs: [resolve_target, preflight] | |
| if: ${{ inputs.full_release_validation && needs.preflight.outputs.run_plugin_prerelease_docker == 'true' }} | |
| permissions: | |
| actions: read | |
| contents: read | |
| packages: read | |
| pull-requests: read | |
| uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml | |
| with: | |
| runner_group: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP }} | |
| ref: ${{ needs.resolve_target.outputs.checkout_revision }} | |
| include_repo_e2e: false | |
| include_release_path_suites: false | |
| include_openwebui: false | |
| docker_lanes: ${{ needs.preflight.outputs.plugin_prerelease_docker_lanes }} | |
| targeted_docker_lane_group_size: 2 | |
| allow_unreleased_changelog: true | |
| allow_frozen_target_scenario_omissions: ${{ inputs.allow_frozen_target_scenario_omissions }} | |
| include_live_suites: false | |
| live_models_only: false | |
| published_upgrade_survivor_baseline: ${{ needs.preflight.outputs.upgrade_baseline || 'openclaw@latest' }} | |
| published_upgrade_survivor_baselines: ${{ needs.preflight.outputs.upgrade_baselines }} | |
| shared_image_artifact_namespace: plugin-prerelease | |
| package_artifact_name: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactName || '' }} | |
| package_artifact_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactId || '' }} | |
| package_artifact_digest: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactDigest || '' }} | |
| package_artifact_run_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactRunId || '' }} | |
| package_artifact_run_attempt: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageArtifactRunAttempt || '' }} | |
| package_file_name: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageFileName || '' }} | |
| package_source_sha: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageSourceSha || '' }} | |
| package_sha256: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageSha256 || '' }} | |
| package_version: ${{ fromJSON(inputs.candidate_artifact_json || '{}').packageVersion || '' }} | |
| enable_prepublish_plugin_registry: true | |
| prepublish_plugin_registry_artifact_name: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactName || '' }} | |
| prepublish_plugin_registry_artifact_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactId || '' }} | |
| prepublish_plugin_registry_artifact_digest: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactDigest || '' }} | |
| prepublish_plugin_registry_artifact_run_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunId || '' }} | |
| prepublish_plugin_registry_artifact_run_attempt: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }} | |
| prepublish_plugin_registry_manifest_sha256: ${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryManifestSha256 || '' }} | |
| shared_image_artifact_name: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactName || '' }} | |
| shared_image_artifact_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactId || '' }} | |
| shared_image_artifact_digest: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactDigest || '' }} | |
| shared_image_artifact_run_id: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactRunId || '' }} | |
| shared_image_artifact_run_attempt: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArtifactRunAttempt || '' }} | |
| shared_image_archive_sha256: ${{ fromJSON(inputs.candidate_artifact_json || '{}').imageArchiveSha256 || '' }} | |
| shared_image_policy: no-push-artifact | |
| plugin-prerelease-suite: | |
| permissions: | |
| contents: read | |
| name: plugin-prerelease-suite | |
| needs: | |
| - preflight | |
| - plugin-prerelease-static-shard | |
| - plugin-prerelease-node-shard | |
| - plugin-prerelease-extension-shard | |
| - plugin-prerelease-inspector | |
| - plugin-prerelease-docker-suite | |
| if: ${{ !cancelled() && always() && needs.preflight.outputs.run_plugin_prerelease_suite == 'true' }} | |
| runs-on: *linux_runner_ubuntu | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Verify plugin prerelease suite | |
| env: | |
| RUN_STATIC: ${{ needs.preflight.outputs.run_plugin_prerelease_static }} | |
| RUN_NODE: ${{ needs.preflight.outputs.run_plugin_prerelease_node }} | |
| RUN_EXTENSIONS: ${{ needs.preflight.outputs.run_plugin_prerelease_extensions }} | |
| RUN_DOCKER: ${{ needs.preflight.outputs.run_plugin_prerelease_docker }} | |
| STATIC_RESULT: ${{ needs.plugin-prerelease-static-shard.result }} | |
| NODE_RESULT: ${{ needs.plugin-prerelease-node-shard.result }} | |
| EXTENSIONS_RESULT: ${{ needs.plugin-prerelease-extension-shard.result }} | |
| INSPECTOR_RESULT: ${{ needs.plugin-prerelease-inspector.result }} | |
| DOCKER_RESULT: ${{ needs.plugin-prerelease-docker-suite.result }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| failed=0 | |
| check_required() { | |
| local name="$1" | |
| local required="$2" | |
| local status="$3" | |
| if [ "$required" != "true" ]; then | |
| return 0 | |
| fi | |
| if [ "$status" != "success" ]; then | |
| echo "::error::${name} ended with ${status}" | |
| failed=1 | |
| fi | |
| } | |
| check_required "plugin-prerelease-static" "$RUN_STATIC" "$STATIC_RESULT" | |
| check_required "plugin-prerelease-node" "$RUN_NODE" "$NODE_RESULT" | |
| check_required "plugin-prerelease-extensions" "$RUN_EXTENSIONS" "$EXTENSIONS_RESULT" | |
| check_required "plugin-prerelease-docker" "$RUN_DOCKER" "$DOCKER_RESULT" | |
| echo "plugin-prerelease-inspector advisory result: ${INSPECTOR_RESULT}" | |
| exit "$failed" | |
| seal_release_child_evidence: | |
| name: Seal full release child evidence | |
| needs: | |
| - resolve_target | |
| - preflight | |
| - plugin-prerelease-static-shard | |
| - plugin-prerelease-node-shard | |
| - plugin-prerelease-extension-shard | |
| - plugin-prerelease-inspector | |
| - plugin-prerelease-docker-suite | |
| - plugin-prerelease-suite | |
| if: ${{ always() && !cancelled() && github.event_name == 'workflow_dispatch' && startsWith(inputs.dispatch_id, 'full-release-validation-') }} | |
| permissions: | |
| contents: read | |
| actions: read | |
| uses: ./.github/workflows/full-release-child-evidence.yml | |
| with: | |
| role: ${{ inputs.phase == 'candidate' && 'pluginPrereleaseCandidate' || 'pluginPrereleaseIndependent' }} | |
| target_sha: ${{ needs.resolve_target.outputs.checkout_revision }} |