mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
fix(ci): block release fanout on expired plugin compatibility (#155971)
* fix(ci): gate release fanout on plugin compatibility * fix(ci): complete compatibility gate admission * fix(ci): gate published release checks on compatibility
This commit is contained in:
parent
1e18169ed8
commit
a298fa9b67
7 changed files with 310 additions and 39 deletions
104
.github/workflows/full-release-validation.yml
vendored
104
.github/workflows/full-release-validation.yml
vendored
|
|
@ -203,6 +203,7 @@ jobs:
|
|||
source_admission_json: ${{ steps.publication_admission.outputs.json }}
|
||||
validation_purpose: ${{ steps.publication_request.outputs.validation_purpose }}
|
||||
publication_selection_json: ${{ steps.publication_request.outputs.publication_selection_json }}
|
||||
plugin_compatibility_required: ${{ steps.plugin_compatibility.outputs.required }}
|
||||
steps:
|
||||
- name: Setup supported Node runtime
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
|
|
@ -436,6 +437,26 @@ jobs:
|
|||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Detect target plugin compatibility gate
|
||||
id: plugin_compatibility
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET_SHA: ${{ steps.resolve.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if jq -e '.scripts["plugins:boundary-report:ci"] | type == "string"' target/package.json >/dev/null; then
|
||||
echo 'required=true' >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
gate_introduction=38ba27834dd3f98c19d5833e0598dfef3abb7587
|
||||
relationship="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${gate_introduction}...${TARGET_SHA}" --jq .status)"
|
||||
if [[ "$relationship" == "ahead" || "$relationship" == "identical" ]]; then
|
||||
echo 'Current target is missing plugins:boundary-report:ci.' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo 'required=false' >> "$GITHUB_OUTPUT"
|
||||
echo '::warning::Frozen target predates the plugin compatibility release gate; skipping target-owned compatibility readiness.'
|
||||
|
||||
- name: Validate release inputs
|
||||
id: release_inputs
|
||||
env:
|
||||
|
|
@ -876,10 +897,53 @@ jobs:
|
|||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
plugin_compatibility_readiness:
|
||||
name: Enforce plugin compatibility release readiness
|
||||
needs: [resolve_target]
|
||||
if: needs.resolve_target.outputs.plugin_compatibility_required == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout target source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ needs.resolve_target.outputs.sha }}
|
||||
fetch-depth: 1
|
||||
filter: blob:none
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Checkout trusted package-manager setup
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
path: .release-harness
|
||||
sparse-checkout: .github/actions/setup-pnpm-store-cache
|
||||
sparse-checkout-cone-mode: false
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Setup target package manager
|
||||
uses: ./.release-harness/.github/actions/setup-pnpm-store-cache
|
||||
with:
|
||||
package-manager-file: package.json
|
||||
lockfile-path: pnpm-lock.yaml
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache-mode: restore
|
||||
|
||||
- name: Install target dependencies
|
||||
env:
|
||||
CI: "true"
|
||||
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
|
||||
|
||||
- name: Enforce target compatibility readiness
|
||||
run: pnpm plugins:boundary-report:ci
|
||||
|
||||
evidence_reuse:
|
||||
name: Check for reusable validation evidence
|
||||
needs: [resolve_target]
|
||||
if: inputs.rerun_group == 'all' && inputs.reuse_evidence && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release-ci/'))
|
||||
needs: [resolve_target, plugin_compatibility_readiness]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && inputs.rerun_group == 'all' && inputs.reuse_evidence && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release-ci/')) }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
|
|
@ -1052,10 +1116,10 @@ jobs:
|
|||
|
||||
docker_runtime_assets_preflight:
|
||||
name: Verify Docker runtime image assets
|
||||
needs: [resolve_target, evidence_reuse]
|
||||
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
|
||||
# Release image preparation checks runtime-assets on both native architectures.
|
||||
# Alpha has no release image producer, so it retains this standalone proof.
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && inputs.rerun_group == 'all' && contains(needs.resolve_target.outputs.target_version, '-alpha.') && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && inputs.rerun_group == 'all' && contains(needs.resolve_target.outputs.target_version, '-alpha.') && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
|
|
@ -1080,8 +1144,8 @@ jobs:
|
|||
|
||||
normal_ci:
|
||||
name: Run normal full CI
|
||||
needs: [resolve_target, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && contains(fromJSON('["all","ci"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && contains(fromJSON('["all","ci"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
|
|
@ -1470,8 +1534,8 @@ jobs:
|
|||
|
||||
plugin_prerelease_independent:
|
||||
name: Run plugin prerelease independent validation
|
||||
needs: [resolve_target, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
|
|
@ -1522,8 +1586,8 @@ jobs:
|
|||
|
||||
release_checks_independent:
|
||||
name: Run release checks independent validation
|
||||
needs: [resolve_target, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && contains(fromJSON('["all","install-smoke","live-e2e","qa-parity","qa-live"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && contains(fromJSON('["all","install-smoke","live-e2e","qa-parity","qa-live"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
|
|
@ -1561,8 +1625,8 @@ jobs:
|
|||
|
||||
release_checks_candidate:
|
||||
name: Run release checks candidate validation
|
||||
needs: [resolve_target, evidence_reuse, candidate_acquisition]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && (needs.resolve_target.outputs.release_candidate_artifact_required != 'true' || (needs.candidate_acquisition.result == 'success' && needs.candidate_acquisition.outputs.state == 'ready')) && (contains(fromJSON('["all","cross-os","package"]'), inputs.rerun_group) || (inputs.rerun_group == 'live-e2e' && needs.resolve_target.outputs.live_suite_filter == '')) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse, candidate_acquisition]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && (needs.resolve_target.outputs.release_candidate_artifact_required != 'true' || (needs.candidate_acquisition.result == 'success' && needs.candidate_acquisition.outputs.state == 'ready')) && (contains(fromJSON('["all","cross-os","package"]'), inputs.rerun_group) || (inputs.rerun_group == 'live-e2e' && needs.resolve_target.outputs.live_suite_filter == '')) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
|
|
@ -1600,8 +1664,8 @@ jobs:
|
|||
|
||||
npm_telegram:
|
||||
name: Run package Telegram E2E
|
||||
needs: [resolve_target, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && inputs.telegram_waiver == '' && needs.resolve_target.outputs.coverage_policy != 'npm-beta-v1' && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group) && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '') && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && inputs.telegram_waiver == '' && needs.resolve_target.outputs.coverage_policy != 'npm-beta-v1' && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group) && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '') && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
continue-on-error: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
|
|
@ -1625,8 +1689,8 @@ jobs:
|
|||
|
||||
performance:
|
||||
name: Run product performance evidence
|
||||
needs: [resolve_target, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && needs.resolve_target.outputs.coverage_policy != 'npm-beta-v1' && contains(fromJSON('["all","performance"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
|
||||
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && needs.resolve_target.outputs.coverage_policy != 'npm-beta-v1' && contains(fromJSON('["all","performance"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
|
||||
runs-on: ${{ vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }}
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
|
|
@ -1647,8 +1711,8 @@ jobs:
|
|||
|
||||
prepare_npm_package:
|
||||
name: Prepare release npm artifacts
|
||||
needs: [resolve_target, evidence_reuse]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && (inputs.rerun_group == 'all' || needs.resolve_target.outputs.candidate_required == 'true') }}
|
||||
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
|
||||
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && (inputs.rerun_group == 'all' || needs.resolve_target.outputs.candidate_required == 'true') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 360
|
||||
outputs:
|
||||
|
|
@ -1736,8 +1800,8 @@ jobs:
|
|||
|
||||
prepare_docker_release:
|
||||
name: Prepare release Docker artifacts
|
||||
needs: [resolve_target, evidence_reuse]
|
||||
if: ${{ always() && inputs.rerun_group == 'all' && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && !contains(needs.resolve_target.outputs.target_version, '-alpha.') }}
|
||||
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
|
||||
if: ${{ always() && inputs.rerun_group == 'all' && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && !contains(needs.resolve_target.outputs.target_version, '-alpha.') }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 360
|
||||
outputs:
|
||||
|
|
|
|||
18
.github/workflows/openclaw-npm-preflight.yml
vendored
18
.github/workflows/openclaw-npm-preflight.yml
vendored
|
|
@ -194,6 +194,24 @@ jobs:
|
|||
OPENCLAW_LOCAL_CHECK: "0"
|
||||
run: pnpm check --include-test-types --include-architecture
|
||||
|
||||
- name: Enforce plugin compatibility release readiness
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if jq -e '.scripts["plugins:boundary-report:ci"] | type == "string"' package.json >/dev/null; then
|
||||
pnpm plugins:boundary-report:ci
|
||||
exit 0
|
||||
fi
|
||||
gate_introduction=38ba27834dd3f98c19d5833e0598dfef3abb7587
|
||||
target_sha="$(git rev-parse HEAD)"
|
||||
relationship="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${gate_introduction}...${target_sha}" --jq .status)"
|
||||
if [[ "$relationship" != "behind" ]]; then
|
||||
echo "Target is not proven to predate plugins:boundary-report:ci (relationship: $relationship)." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo '::warning::Frozen target predates the plugin compatibility release gate; skipping target-owned compatibility readiness.'
|
||||
|
||||
- name: Seal source-check evidence
|
||||
id: source_evidence
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -854,6 +854,7 @@ describe("retained publication admission", () => {
|
|||
github: { run_attempt: 1 },
|
||||
needs: {
|
||||
resolve_target: { result: "success" },
|
||||
plugin_compatibility_readiness: { result: "success" },
|
||||
evidence_reuse: { result: "failure" },
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1291,6 +1291,7 @@ globalThis.Date = class extends OriginalDate {
|
|||
candidate_required: steps.candidate_request!.outputs.required,
|
||||
},
|
||||
},
|
||||
plugin_compatibility_readiness: { result: "success" },
|
||||
evidence_reuse: { result: "skipped", outputs: { reuse: "false" } },
|
||||
},
|
||||
});
|
||||
|
|
@ -1925,6 +1926,7 @@ describe("FRV publication source admission", () => {
|
|||
id === "docker_runtime_assets_preflight" ? "2026.9.9-alpha.1" : "2026.9.9",
|
||||
},
|
||||
},
|
||||
plugin_compatibility_readiness: { result: "success" },
|
||||
evidence_reuse: { result: "skipped", outputs: { reuse: "false" } },
|
||||
},
|
||||
}),
|
||||
|
|
@ -2284,6 +2286,7 @@ describe("FRV publication source admission", () => {
|
|||
id === "docker_runtime_assets_preflight" ? "2026.9.9-alpha.1" : "2026.9.9",
|
||||
},
|
||||
},
|
||||
plugin_compatibility_readiness: { result: "success" },
|
||||
evidence_reuse: { result: "skipped", outputs: { reuse: "false" } },
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,13 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
|
||||
const workflowPath = ".github/workflows/openclaw-npm-release.yml";
|
||||
const preflightWorkflowPath = ".github/workflows/openclaw-npm-preflight.yml";
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
type Step = {
|
||||
env?: Record<string, string>;
|
||||
|
|
@ -70,7 +63,7 @@ function step(job: Job | undefined, name: string): Step {
|
|||
}
|
||||
|
||||
function runControlUiArtifactStep(options: { artifactPresent: boolean }) {
|
||||
const root = mkdtempSync(join(tmpdir(), "openclaw-npm-preflight-ui-"));
|
||||
const root = tempDirs.make("openclaw-npm-preflight-ui-");
|
||||
const binDir = join(root, "bin");
|
||||
const artifactPath = join(root, "dist", "control-ui", "index.html");
|
||||
const invocationPath = join(root, "pnpm-invocation.txt");
|
||||
|
|
@ -122,10 +115,54 @@ printf '<!doctype html>\\n' > "${artifactPath}"
|
|||
: null;
|
||||
const artifactExists = existsSync(artifactPath);
|
||||
const targetHasTsxLoader = existsSync(join(root, "scripts", "tsx.mjs"));
|
||||
rmSync(root, { force: true, recursive: true });
|
||||
return { artifactExists, invocation, result, targetHasTsxLoader };
|
||||
}
|
||||
|
||||
function runPluginCompatibilityGate(options: { hasScript: boolean; relationship: string }) {
|
||||
const root = tempDirs.make("openclaw-npm-plugin-compat-");
|
||||
const binDir = join(root, "bin");
|
||||
const invocationPath = join(root, "pnpm-invocation.txt");
|
||||
const apiPath = join(root, "gh-invocation.txt");
|
||||
mkdirSync(binDir);
|
||||
writeFileSync(
|
||||
join(root, "package.json"),
|
||||
JSON.stringify({
|
||||
scripts: options.hasScript ? { "plugins:boundary-report:ci": "node check.mjs" } : {},
|
||||
}),
|
||||
);
|
||||
for (const [name, script] of [
|
||||
["git", `#!/usr/bin/env bash\nprintf '%040d\\n' 0\n`],
|
||||
[
|
||||
"gh",
|
||||
`#!/usr/bin/env bash\nprintf '%s\\n' "$*" > ${JSON.stringify(apiPath)}\nprintf '%s\\n' "$RELATIONSHIP"\n`,
|
||||
],
|
||||
["pnpm", `#!/usr/bin/env bash\nprintf '%s\\n' "$*" > ${JSON.stringify(invocationPath)}\n`],
|
||||
] as const) {
|
||||
const path = join(binDir, name);
|
||||
writeFileSync(path, script);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
const gate = step(
|
||||
workflow(preflightWorkflowPath).jobs?.check_openclaw_npm,
|
||||
"Enforce plugin compatibility release readiness",
|
||||
);
|
||||
const result = spawnSync("bash", ["--noprofile", "--norc", "-c", gate.run ?? ""], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
GITHUB_REPOSITORY: "openclaw/openclaw",
|
||||
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
||||
RELATIONSHIP: options.relationship,
|
||||
},
|
||||
});
|
||||
const invocation = existsSync(invocationPath)
|
||||
? readFileSync(invocationPath, "utf8").trim()
|
||||
: null;
|
||||
const apiInvocation = existsSync(apiPath) ? readFileSync(apiPath, "utf8").trim() : null;
|
||||
return { apiInvocation, invocation, result };
|
||||
}
|
||||
|
||||
describe("minimal npm extended-stable workflow", () => {
|
||||
it("bounds every git fetch operation", () => {
|
||||
const source = [workflowPath, preflightWorkflowPath]
|
||||
|
|
@ -154,6 +191,10 @@ describe("minimal npm extended-stable workflow", () => {
|
|||
parsed.jobs?.check_openclaw_npm,
|
||||
"Check source, test types, and architecture",
|
||||
);
|
||||
const pluginCompatibility = step(
|
||||
parsed.jobs?.check_openclaw_npm,
|
||||
"Enforce plugin compatibility release readiness",
|
||||
);
|
||||
const trustedCheckout = step(
|
||||
parsed.jobs?.check_openclaw_npm,
|
||||
"Checkout trusted package source preflight",
|
||||
|
|
@ -185,12 +226,18 @@ describe("minimal npm extended-stable workflow", () => {
|
|||
);
|
||||
}
|
||||
expect(sourceCheck.run).toBe("pnpm check --include-test-types --include-architecture");
|
||||
expect(pluginCompatibility.run).toContain('.scripts["plugins:boundary-report:ci"]');
|
||||
expect(pluginCompatibility.run).toContain("38ba27834dd3f98c19d5833e0598dfef3abb7587");
|
||||
expect(pluginCompatibility.run).toContain("Target is not proven to predate");
|
||||
expect(pluginCompatibility.run).toContain("Frozen target predates");
|
||||
expect(pluginCompatibility.run).toContain("pnpm plugins:boundary-report:ci");
|
||||
expect(metadata).toContain("--unshallow origin");
|
||||
expect(metadata).toContain('"+refs/tags/v*:refs/tags/v*"');
|
||||
const sourceSteps = parsed.jobs?.check_openclaw_npm?.steps ?? [];
|
||||
const prepareSteps = parsed.jobs?.prepare_openclaw_npm?.steps ?? [];
|
||||
expect(sourceSteps.indexOf(trustedCheckout)).toBeLessThan(sourceSteps.indexOf(sourceAncestry));
|
||||
expect(sourceSteps.indexOf(sourceAncestry)).toBeLessThan(sourceSteps.indexOf(sourceCheck));
|
||||
expect(sourceSteps.indexOf(sourceCheck)).toBeLessThan(sourceSteps.indexOf(pluginCompatibility));
|
||||
expect(prepareSteps.indexOf(tideclawAncestry)).toBeGreaterThan(
|
||||
prepareSteps.findIndex(
|
||||
(candidate) => candidate.name === "Checkout trusted package source preflight",
|
||||
|
|
@ -203,6 +250,52 @@ describe("minimal npm extended-stable workflow", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "current target with the gate",
|
||||
hasScript: true,
|
||||
relationship: "ahead",
|
||||
status: 0,
|
||||
invocation: "plugins:boundary-report:ci",
|
||||
api: false,
|
||||
},
|
||||
{
|
||||
label: "historical target before the gate",
|
||||
hasScript: false,
|
||||
relationship: "behind",
|
||||
status: 0,
|
||||
invocation: null,
|
||||
api: true,
|
||||
},
|
||||
{
|
||||
label: "current target missing the gate",
|
||||
hasScript: false,
|
||||
relationship: "ahead",
|
||||
status: 1,
|
||||
invocation: null,
|
||||
api: true,
|
||||
},
|
||||
{
|
||||
label: "diverged target missing the gate",
|
||||
hasScript: false,
|
||||
relationship: "diverged",
|
||||
status: 1,
|
||||
invocation: null,
|
||||
api: true,
|
||||
},
|
||||
])("enforces plugin compatibility admission for $label", (testCase) => {
|
||||
const run = runPluginCompatibilityGate(testCase);
|
||||
expect(run.result.status, run.result.stderr).toBe(testCase.status);
|
||||
expect(run.invocation).toBe(testCase.invocation);
|
||||
expect(run.apiInvocation !== null).toBe(testCase.api);
|
||||
if (testCase.relationship === "behind") {
|
||||
expect(run.result.stdout).toContain("Frozen target predates");
|
||||
}
|
||||
if (testCase.status === 1) {
|
||||
expect(run.result.stderr).toContain("not proven to predate");
|
||||
}
|
||||
});
|
||||
|
||||
it("adds extended-stable without adding policy or verifier contracts", () => {
|
||||
const raw = readFileSync(workflowPath, "utf8");
|
||||
const parsed = workflow();
|
||||
|
|
|
|||
|
|
@ -9245,7 +9245,11 @@ describe("package artifact reuse", () => {
|
|||
const qualify = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "qualify_npm_package");
|
||||
const candidate = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "candidate_acquisition");
|
||||
for (const job of [prepare, docker]) {
|
||||
expect(jobNeeds(job)).toEqual(["resolve_target", "evidence_reuse"]);
|
||||
expect(jobNeeds(job)).toEqual([
|
||||
"resolve_target",
|
||||
"plugin_compatibility_readiness",
|
||||
"evidence_reuse",
|
||||
]);
|
||||
}
|
||||
expect(jobNeeds(qualify)).toEqual(["resolve_target", "prepare_npm_package"]);
|
||||
expect(qualify.if).toBe(
|
||||
|
|
@ -12760,6 +12764,18 @@ printf '%s\\n' "$DEEPSEEK_API_KEY" "$DEEPINFRA_API_KEY"`,
|
|||
resolveTargetJob,
|
||||
"Checkout target package manifest",
|
||||
);
|
||||
const detectPluginCompatibility = workflowStep(
|
||||
resolveTargetJob,
|
||||
"Detect target plugin compatibility gate",
|
||||
);
|
||||
const pluginCompatibilityJob = workflowJob(
|
||||
FULL_RELEASE_VALIDATION_WORKFLOW,
|
||||
"plugin_compatibility_readiness",
|
||||
);
|
||||
const enforcePluginCompatibility = workflowStep(
|
||||
pluginCompatibilityJob,
|
||||
"Enforce target compatibility readiness",
|
||||
);
|
||||
const toolingIdentity = workflowStep(resolveTargetJob, "Resolve trusted workflow identity");
|
||||
const releaseInputValidation = workflowStep(resolveTargetJob, "Validate release inputs");
|
||||
const evidenceReuseStep = workflowStep(evidenceReuseJob, "Find reusable validation evidence");
|
||||
|
|
@ -12797,6 +12813,68 @@ printf '%s\\n' "$DEEPSEEK_API_KEY" "$DEEPINFRA_API_KEY"`,
|
|||
expect(resolveTargetSteps.indexOf(targetManifestCheckout)).toBeLessThan(
|
||||
resolveTargetSteps.indexOf(releaseInputValidation),
|
||||
);
|
||||
expect(resolveTargetSteps.indexOf(targetManifestCheckout)).toBeLessThan(
|
||||
resolveTargetSteps.indexOf(detectPluginCompatibility),
|
||||
);
|
||||
expect(detectPluginCompatibility.run).toContain('.scripts["plugins:boundary-report:ci"]');
|
||||
expect(detectPluginCompatibility.run).toContain("38ba27834dd3f98c19d5833e0598dfef3abb7587");
|
||||
expect(detectPluginCompatibility.run).toContain("Current target is missing");
|
||||
expect(detectPluginCompatibility.run).toContain("required=false");
|
||||
expect(resolveTargetJob.outputs?.plugin_compatibility_required).toBe(
|
||||
"${{ steps.plugin_compatibility.outputs.required }}",
|
||||
);
|
||||
expect(pluginCompatibilityJob.needs).toEqual(["resolve_target"]);
|
||||
expect(pluginCompatibilityJob.if).toBe(
|
||||
"needs.resolve_target.outputs.plugin_compatibility_required == 'true'",
|
||||
);
|
||||
expect(enforcePluginCompatibility.run).toBe("pnpm plugins:boundary-report:ci");
|
||||
for (const jobName of [
|
||||
"docker_runtime_assets_preflight",
|
||||
"normal_ci",
|
||||
"plugin_prerelease_independent",
|
||||
"release_checks_independent",
|
||||
"release_checks_candidate",
|
||||
"npm_telegram",
|
||||
"performance",
|
||||
"prepare_npm_package",
|
||||
"prepare_docker_release",
|
||||
]) {
|
||||
const job = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, jobName);
|
||||
expect(jobNeeds(job), jobName).toContain("plugin_compatibility_readiness");
|
||||
expect(String(job.if), jobName).toContain("needs.plugin_compatibility_readiness.result");
|
||||
}
|
||||
const candidateCondition = String(releaseChecksJob.if).replace(
|
||||
/^\$\{\{\s*([\s\S]*?)\s*\}\}$/u,
|
||||
"$1",
|
||||
);
|
||||
for (const [compatibilityResult, admitted] of [
|
||||
["success", true],
|
||||
["skipped", true],
|
||||
["failure", false],
|
||||
["cancelled", false],
|
||||
] as const) {
|
||||
const result = runInNewContext(candidateCondition, {
|
||||
github: { run_attempt: 1 },
|
||||
inputs: { release_package_spec: "openclaw@next", rerun_group: "cross-os" },
|
||||
needs: {
|
||||
resolve_target: {
|
||||
result: "success",
|
||||
outputs: { live_suite_filter: "", release_candidate_artifact_required: "false" },
|
||||
},
|
||||
plugin_compatibility_readiness: { result: compatibilityResult },
|
||||
evidence_reuse: { result: "success", outputs: { reuse: "false" } },
|
||||
candidate_acquisition: { result: "skipped", outputs: {} },
|
||||
},
|
||||
always: () => true,
|
||||
contains: (values: string | string[], value: string) => values.includes(value),
|
||||
fromJSON: JSON.parse,
|
||||
});
|
||||
expect(Boolean(result), compatibilityResult).toBe(admitted);
|
||||
}
|
||||
expect(jobNeeds(evidenceReuseJob)).toContain("plugin_compatibility_readiness");
|
||||
expect(evidenceReuseJob.if).toContain("always()");
|
||||
expect(evidenceReuseJob.if).toContain("needs.resolve_target.result == 'success'");
|
||||
expect(evidenceReuseJob.if).toContain("needs.plugin_compatibility_readiness.result");
|
||||
expect(resolveTargetJob.outputs?.trusted_workflow_json).toBe(
|
||||
"${{ steps.tooling_identity.outputs.json }}",
|
||||
);
|
||||
|
|
@ -12821,7 +12899,11 @@ printf '%s\\n' "$DEEPSEEK_API_KEY" "$DEEPINFRA_API_KEY"`,
|
|||
"target_context_ref must be a canonical OpenClaw release branch or tag.",
|
||||
]);
|
||||
expect(npmTelegramJob.name).toBe("Run package Telegram E2E");
|
||||
expect(npmTelegramJob.needs).toEqual(["resolve_target", "evidence_reuse"]);
|
||||
expect(npmTelegramJob.needs).toEqual([
|
||||
"resolve_target",
|
||||
"plugin_compatibility_readiness",
|
||||
"evidence_reuse",
|
||||
]);
|
||||
expect(npmTelegramJob["timeout-minutes"]).toBe(15);
|
||||
expect(performanceJob["timeout-minutes"]).toBe(15);
|
||||
expect(npmTelegramJob.if).toContain(
|
||||
|
|
@ -15352,6 +15434,7 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
|||
const candidateBinding = workflowJob(FULL_RELEASE_CANDIDATE_WORKFLOW, "resolve_candidate");
|
||||
expect(jobNeeds(workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "evidence_reuse"))).toEqual([
|
||||
"resolve_target",
|
||||
"plugin_compatibility_readiness",
|
||||
]);
|
||||
expect(jobNeeds(candidateAcquisition)).toEqual([
|
||||
"resolve_target",
|
||||
|
|
@ -15363,6 +15446,7 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
|||
expect(jobNeeds(candidateBinding)).toEqual(["discover", "prepare"]);
|
||||
expect(jobNeeds(releaseChecksParent)).toEqual([
|
||||
"resolve_target",
|
||||
"plugin_compatibility_readiness",
|
||||
"evidence_reuse",
|
||||
"candidate_acquisition",
|
||||
]);
|
||||
|
|
@ -15371,6 +15455,10 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
|||
);
|
||||
const fullParentPath = [
|
||||
timeoutForProfile(fullRelease.jobs?.resolve_target?.["timeout-minutes"], "full"),
|
||||
timeoutForProfile(
|
||||
fullRelease.jobs?.plugin_compatibility_readiness?.["timeout-minutes"],
|
||||
"full",
|
||||
),
|
||||
timeoutForProfile(fullRelease.jobs?.evidence_reuse?.["timeout-minutes"], "full"),
|
||||
timeoutForProfile(fullReleaseCandidate.jobs?.discover?.["timeout-minutes"], "full"),
|
||||
timeoutForProfile(liveE2e.jobs?.validate_selected_ref?.["timeout-minutes"], "full"),
|
||||
|
|
@ -15382,9 +15470,9 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
|||
timeoutForProfile(candidateBinding["timeout-minutes"], "full"),
|
||||
timeoutForProfile(releaseChecksParent["timeout-minutes"], "full"),
|
||||
];
|
||||
expect(fullParentPath).toEqual([10, 10, 10, 30, 90, 15, 5, 15]);
|
||||
expect(fullParentPath).toEqual([10, 10, 10, 10, 30, 90, 15, 5, 15]);
|
||||
const fullParentTimeoutFloor = fullParentPath.reduce((total, timeout) => total + timeout, 0);
|
||||
expect(fullParentTimeoutFloor).toBe(185);
|
||||
expect(fullParentTimeoutFloor).toBe(195);
|
||||
expect(FULL_RELEASE_WAIT_TIMEOUT_MINUTES).toBe(diagnosticDrainTimeout);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1204,7 +1204,11 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
|
|||
expect(fullReleaseWorkflow.jobs[jobName]["runs-on"]).toBe("ubuntu-24.04");
|
||||
}
|
||||
expect(fullReleaseWorkflow.jobs.normal_ci["timeout-minutes"]).toBe(15);
|
||||
expect(fullReleaseWorkflow.jobs.normal_ci.needs).toEqual(["resolve_target", "evidence_reuse"]);
|
||||
expect(fullReleaseWorkflow.jobs.normal_ci.needs).toEqual([
|
||||
"resolve_target",
|
||||
"plugin_compatibility_readiness",
|
||||
"evidence_reuse",
|
||||
]);
|
||||
expect(fullReleaseWorkflow.jobs.normal_ci.if).toContain(
|
||||
"needs.resolve_target.result == 'success'",
|
||||
);
|
||||
|
|
@ -1212,7 +1216,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
|
|||
"needs.evidence_reuse.outputs.reuse != 'true'",
|
||||
);
|
||||
expect(fullReleaseWorkflow.jobs.docker_runtime_assets_preflight.if).toBe(
|
||||
"${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('[\"success\",\"skipped\"]'), needs.evidence_reuse.result) && inputs.rerun_group == 'all' && contains(needs.resolve_target.outputs.target_version, '-alpha.') && needs.evidence_reuse.outputs.reuse != 'true' }}",
|
||||
"${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('[\"success\",\"skipped\"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('[\"success\",\"skipped\"]'), needs.evidence_reuse.result) && inputs.rerun_group == 'all' && contains(needs.resolve_target.outputs.target_version, '-alpha.') && needs.evidence_reuse.outputs.reuse != 'true' }}",
|
||||
);
|
||||
expect(fullReleaseWorkflow.jobs.docker_runtime_assets_preflight["timeout-minutes"]).toBe(20);
|
||||
const dockerPreflightStep = fullReleaseWorkflow.jobs.docker_runtime_assets_preflight.steps.find(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue