diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index aa7d4add3d1..3706f831076 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -367,7 +367,7 @@ jobs: echo "- Target SHA: \`${TARGET_SHA}\`" } >> "$GITHUB_STEP_SUMMARY" - dispatch_and_wait plugin-prerelease.yml -f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" + dispatch_and_wait plugin-prerelease.yml -f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true release_checks: name: Run release/live/Docker/QA validation diff --git a/.github/workflows/plugin-prerelease.yml b/.github/workflows/plugin-prerelease.yml index fab6ddb88b0..20dc8e503f8 100644 --- a/.github/workflows/plugin-prerelease.yml +++ b/.github/workflows/plugin-prerelease.yml @@ -13,6 +13,11 @@ on: required: false default: "" type: string + full_release_validation: + description: Enable release-only Docker prerelease lanes from Full Release Validation + required: false + default: false + type: boolean permissions: contents: read @@ -54,6 +59,7 @@ jobs: id: manifest env: EXPECTED_SHA: ${{ inputs.expected_sha }} + FULL_RELEASE_VALIDATION: ${{ inputs.full_release_validation && 'true' || 'false' }} run: | node --input-type=module <<'EOF' import { appendFileSync } from "node:fs"; @@ -65,6 +71,7 @@ jobs: encoding: "utf8", }).trim(); const expectedSha = (process.env.EXPECTED_SHA ?? "").trim(); + const fullReleaseValidation = process.env.FULL_RELEASE_VALIDATION === "true"; if (expectedSha && expectedSha !== checkoutRevision) { console.error( `target_ref resolved to ${checkoutRevision}, expected ${expectedSha}`, @@ -171,7 +178,7 @@ jobs: const runStatic = staticChecks.length > 0; const runNode = nodeShards.length > 0; const runExtensions = extensionShards.length > 0; - const runDocker = dockerLanes.length > 0; + const runDocker = fullReleaseValidation && dockerLanes.length > 0; const runSuite = runStatic || runNode || runExtensions || runDocker; const manifest = { @@ -342,7 +349,7 @@ jobs: plugin-prerelease-docker-suite: name: plugin-prerelease-docker-suite needs: [preflight] - if: needs.preflight.outputs.run_plugin_prerelease_docker == 'true' + if: ${{ inputs.full_release_validation && needs.preflight.outputs.run_plugin_prerelease_docker == 'true' }} permissions: actions: read contents: read diff --git a/docs/ci.md b/docs/ci.md index 913ba852f26..28d46dc3ea1 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -369,8 +369,10 @@ checks, Python skills, Windows, macOS, and Control UI i18n. Standalone manual CI dispatches run Android only with `include_android=true`; the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension -batch sweep, and plugin prerelease Docker lanes are excluded from CI and run in -the separate `Plugin Prerelease` workflow. Manual runs use a +batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker +prerelease suite runs only when `Full Release Validation` dispatches the +separate `Plugin Prerelease` workflow with the release-validation gate enabled. +Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 7ea5239ada0..c45e9d431ab 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -129,6 +129,9 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { const pluginManifestScript = pluginPreflight.steps.find( (step) => step.name === "Build plugin prerelease manifest", ).run; + const pluginManifestEnv = pluginPreflight.steps.find( + (step) => step.name === "Build plugin prerelease manifest", + ).env; const normalCiScript = releaseWorkflow.jobs.normal_ci.steps.find( (step) => step.name === "Dispatch and monitor CI", ).run; @@ -163,7 +166,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { ); expect(normalCiScript).not.toContain("full_release_validation=true"); expect(pluginPrereleaseScript).toContain( - 'dispatch_and_wait plugin-prerelease.yml -f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA"', + 'dispatch_and_wait plugin-prerelease.yml -f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true', ); expect(pluginManifestScript).toContain("await import("); expect(pluginManifestScript).toContain('"./scripts/lib/plugin-prerelease-test-plan.mjs"'); @@ -177,6 +180,19 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { default: "main", type: "string", }); + expect(pluginWorkflow.on.workflow_dispatch.inputs.full_release_validation).toMatchObject({ + default: false, + type: "boolean", + }); + expect(pluginManifestEnv).toMatchObject({ + FULL_RELEASE_VALIDATION: "${{ inputs.full_release_validation && 'true' || 'false' }}", + }); + expect(pluginManifestScript).toContain( + 'const fullReleaseValidation = process.env.FULL_RELEASE_VALIDATION === "true";', + ); + expect(pluginManifestScript).toContain( + "const runDocker = fullReleaseValidation && dockerLanes.length > 0;", + ); expect(pluginPreflight.outputs).toMatchObject({ checkout_revision: "${{ steps.manifest.outputs.checkout_revision }}", plugin_prerelease_docker_lanes: @@ -209,7 +225,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { staticShard.steps.find((step) => step.name === "Run plugin prerelease static shard").run, ).toContain('bash -c "$PLUGIN_PRERELEASE_COMMAND"'); expect(dockerSuite).toMatchObject({ - if: "needs.preflight.outputs.run_plugin_prerelease_docker == 'true'", + if: "${{ inputs.full_release_validation && needs.preflight.outputs.run_plugin_prerelease_docker == 'true' }}", needs: ["preflight"], uses: "./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml", with: {