mirror of
https://github.com/openclaw/openclaw.git
synced 2026-11-01 04:58:23 +00:00
Some checks are pending
ClawSweeper Dispatch / dispatch (push) Waiting to run
CodeQL / scope (push) Waiting to run
CodeQL / Security High () (push) Blocked by required conditions
Control UI Locale Refresh / resolve-base (push) Waiting to run
Control UI Locale Refresh / Verify generated PR App permissions (push) Blocked by required conditions
Control UI Locale Refresh / Refresh (push) Blocked by required conditions
Control UI Locale Refresh / Commit control UI locale refresh (push) Blocked by required conditions
Docs Sync Publish Repo / sync-publish-repo (push) Waiting to run
Docs / docs (push) Waiting to run
Native App Locale Refresh / resolve-base (push) Waiting to run
Native App Locale Refresh / Verify generated PR App permissions (push) Blocked by required conditions
Native App Locale Refresh / Refresh native ar (push) Blocked by required conditions
Native App Locale Refresh / Refresh native de (push) Blocked by required conditions
Native App Locale Refresh / Refresh native es (push) Blocked by required conditions
Native App Locale Refresh / Refresh native fa (push) Blocked by required conditions
Native App Locale Refresh / Refresh native fr (push) Blocked by required conditions
Native App Locale Refresh / Refresh native hi (push) Blocked by required conditions
Native App Locale Refresh / Refresh native id (push) Blocked by required conditions
Native App Locale Refresh / Refresh native it (push) Blocked by required conditions
Native App Locale Refresh / Refresh native ja-JP (push) Blocked by required conditions
Native App Locale Refresh / Refresh native ko (push) Blocked by required conditions
Native App Locale Refresh / Refresh native nl (push) Blocked by required conditions
Native App Locale Refresh / Refresh native pl (push) Blocked by required conditions
Native App Locale Refresh / Refresh native pt-BR (push) Blocked by required conditions
Native App Locale Refresh / Refresh native ru (push) Blocked by required conditions
Native App Locale Refresh / Refresh native sv (push) Blocked by required conditions
Native App Locale Refresh / Refresh native th (push) Blocked by required conditions
Native App Locale Refresh / Refresh native tr (push) Blocked by required conditions
Native App Locale Refresh / Refresh native uk (push) Blocked by required conditions
Native App Locale Refresh / Refresh native vi (push) Blocked by required conditions
Native App Locale Refresh / Refresh native zh-CN (push) Blocked by required conditions
Native App Locale Refresh / Refresh native zh-TW (push) Blocked by required conditions
Native App Locale Refresh / Commit native locale refresh (push) Blocked by required conditions
Node Runtime Conformance / scope (push) Waiting to run
Node Runtime Conformance / TypeScript contracts (push) Blocked by required conditions
Node Runtime Conformance / Rust workspace (push) Blocked by required conditions
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Waiting to run
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Blocked by required conditions
Plugin Init Scaffold Validation / scope (push) Waiting to run
Plugin Init Scaffold Validation / Validate provider scaffold (push) Blocked by required conditions
Plugin NPM Release / preview_plugins_npm (push) Waiting to run
Plugin NPM Release / Validate release publish approval (push) Blocked by required conditions
Plugin NPM Release / preview_plugin_pack (push) Blocked by required conditions
Plugin NPM Release / Preflight plugin npm package () (push) Blocked by required conditions
Plugin NPM Release / Seal prepared plugin npm release (push) Blocked by required conditions
Plugin NPM Release / Trusted publisher OIDC exchange (push) Blocked by required conditions
Plugin NPM Release / approve_plugins_npm_release (push) Blocked by required conditions
Plugin NPM Release / Publish plugin npm package () (push) Blocked by required conditions
Plugin NPM Release / verify_plugins_npm (push) Blocked by required conditions
Vitest Cache Warm / dependencies (push) Waiting to run
Vitest Cache Warm / warm (push) Waiting to run
Workflow Sanity / actionlint (push) Waiting to run
Workflow Sanity / generated-doc-baselines (push) Waiting to run
* ci: default daily tests to Bun and retain dual release validation * test(ci): align fixtures with the daily Bun policy * fix(ci): preserve Node-only gateway test routing Resolve split gateway membership through the existing CI inventories and make the native-launch artifact assertion independent of directory enumeration order. * test(ci): align Gateway routing with fork-fixed contracts
1066 lines
51 KiB
YAML
1066 lines
51 KiB
YAML
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.21.0"
|
|
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.x"
|
|
|
|
- name: Setup manifest pnpm
|
|
uses: ./.github/actions/setup-pnpm-store-cache
|
|
with:
|
|
cache-mode: restore
|
|
node-version: "24.x"
|
|
|
|
- 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_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 }}
|