diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index bb7332cb2ea3..067f1ff30d38 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -210,10 +210,36 @@ jobs: fi } >> "$GITHUB_STEP_SUMMARY" + docker_runtime_assets_preflight: + name: Verify Docker runtime-assets prune path + needs: [resolve_target] + if: inputs.rerun_group == 'all' + runs-on: ubuntu-24.04 + timeout-minutes: 45 + permissions: + contents: read + steps: + - name: Checkout target SHA + uses: actions/checkout@v6 + with: + ref: ${{ needs.resolve_target.outputs.sha }} + fetch-depth: 1 + persist-credentials: false + + - name: Verify Docker runtime-assets prune path + env: + DOCKER_BUILDKIT: "1" + run: | + set -euo pipefail + timeout --foreground --kill-after=30s 35m docker build \ + --target runtime-assets \ + --build-arg OPENCLAW_EXTENSIONS="matrix" \ + . + normal_ci: name: Run normal full CI - needs: [resolve_target] - if: contains(fromJSON('["all","ci"]'), inputs.rerun_group) + needs: [resolve_target, docker_runtime_assets_preflight] + if: ${{ always() && contains(fromJSON('["all","ci"]'), inputs.rerun_group) && (inputs.rerun_group != 'all' || needs.docker_runtime_assets_preflight.result == 'success') }} runs-on: ubuntu-24.04 timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }} outputs: @@ -312,8 +338,8 @@ jobs: plugin_prerelease: name: Run plugin prerelease validation - needs: [resolve_target] - if: contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group) + needs: [resolve_target, docker_runtime_assets_preflight] + if: ${{ always() && contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group) && (inputs.rerun_group != 'all' || needs.docker_runtime_assets_preflight.result == 'success') }} runs-on: ubuntu-24.04 timeout-minutes: ${{ inputs.release_profile == 'full' && 300 || inputs.release_profile == 'stable' && 240 || 60 }} outputs: @@ -412,8 +438,8 @@ jobs: release_checks: name: Run release/live/Docker/QA validation - needs: [resolve_target] - if: contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group) + needs: [resolve_target, docker_runtime_assets_preflight] + if: ${{ always() && contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group) && (inputs.rerun_group != 'all' || needs.docker_runtime_assets_preflight.result == 'success') }} runs-on: ubuntu-24.04 timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }} outputs: @@ -565,8 +591,8 @@ jobs: prepare_release_package: name: Prepare release package artifact - needs: [resolve_target] - if: ${{ inputs.npm_telegram_package_spec == '' && inputs.release_package_spec == '' && inputs.rerun_group == 'all' && inputs.release_profile == 'full' }} + needs: [resolve_target, docker_runtime_assets_preflight] + if: ${{ always() && inputs.npm_telegram_package_spec == '' && inputs.release_package_spec == '' && inputs.rerun_group == 'all' && inputs.release_profile == 'full' && needs.docker_runtime_assets_preflight.result == 'success' }} runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: @@ -735,7 +761,7 @@ jobs: summary: name: Verify full validation - needs: [resolve_target, normal_ci, plugin_prerelease, release_checks, npm_telegram] + needs: [resolve_target, docker_runtime_assets_preflight, normal_ci, plugin_prerelease, release_checks, npm_telegram] if: always() runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -751,6 +777,8 @@ jobs: PLUGIN_PRERELEASE_RESULT: ${{ needs.plugin_prerelease.result }} RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }} NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }} + DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT: ${{ needs.docker_runtime_assets_preflight.result }} + RERUN_GROUP: ${{ inputs.rerun_group }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} CHILD_WORKFLOW_REF: ${{ github.ref_name }} run: | @@ -798,6 +826,7 @@ jobs: echo echo "| Child | Result | Minutes | Head SHA | Run |" echo "| --- | --- | ---: | --- | --- |" + echo "| \`docker_runtime_assets_preflight\` | \`${DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT}\` | | current workflow | |" } >> "$GITHUB_STEP_SUMMARY" append_child_row() { @@ -933,23 +962,29 @@ jobs: } failed=0 + required_after_preflight=1 + if [[ "$RERUN_GROUP" == "all" && "$DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT" != "success" ]]; then + echo "::error::Docker runtime-assets preflight ended with ${DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT}." + failed=1 + required_after_preflight=0 + fi append_child_overview if [[ "$NORMAL_CI_RESULT" == "skipped" && -z "${NORMAL_CI_RUN_ID// }" ]]; then - check_child "normal_ci" "" 0 || failed=1 + check_child "normal_ci" "" "$required_after_preflight" || failed=1 else check_child "normal_ci" "$NORMAL_CI_RUN_ID" 1 || failed=1 fi if [[ "$PLUGIN_PRERELEASE_RESULT" == "skipped" && -z "${PLUGIN_PRERELEASE_RUN_ID// }" ]]; then - check_child "plugin_prerelease" "" 0 || failed=1 + check_child "plugin_prerelease" "" "$required_after_preflight" || failed=1 else check_child "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID" 1 || failed=1 fi if [[ "$RELEASE_CHECKS_RESULT" == "skipped" && -z "${RELEASE_CHECKS_RUN_ID// }" ]]; then - check_child "release_checks" "" 0 || failed=1 + check_child "release_checks" "" "$required_after_preflight" || failed=1 else check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 || failed=1 fi diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index 4c2cc3d26f5a..f0293f987706 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -337,16 +337,6 @@ jobs: PACKAGE_VERSION="$(node -p "require('./package.json').version")" node --import tsx scripts/openclaw-npm-prepublish-verify.ts "$TARBALL_PATH" "$PACKAGE_VERSION" - - name: Verify Docker runtime-assets prune path - env: - DOCKER_BUILDKIT: "1" - run: | - set -euo pipefail - timeout --foreground --kill-after=30s 35m docker build \ - --target runtime-assets \ - --build-arg OPENCLAW_EXTENSIONS="matrix" \ - . - - name: Upload dependency release evidence uses: actions/upload-artifact@v7 with: @@ -415,8 +405,8 @@ jobs: echo "Real publish requires full_release_validation_run_id from a successful Full Release Validation run." >&2 exit 1 fi - if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then - echo "Real publish requires release_publish_run_id from the approved OpenClaw Release Publish workflow." >&2 + if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" && "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then + echo "Workflow-dispatched real publish requires release_publish_run_id from the approved OpenClaw Release Publish workflow." >&2 exit 1 fi @@ -427,6 +417,14 @@ jobs: EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }} run: | set -euo pipefail + if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then + if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then + echo "OpenClaw npm publish dispatched by another workflow must include release_publish_run_id." >&2 + exit 1 + fi + echo "Direct OpenClaw npm publish; relying on this workflow's npm-release environment approval." + exit 0 + fi if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then echo "OpenClaw npm publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2 exit 1 diff --git a/.github/workflows/plugin-clawhub-release.yml b/.github/workflows/plugin-clawhub-release.yml index aa75ceeaf73c..cdf5ae1ce6e5 100644 --- a/.github/workflows/plugin-clawhub-release.yml +++ b/.github/workflows/plugin-clawhub-release.yml @@ -217,8 +217,12 @@ jobs: run: | set -euo pipefail if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then - echo "Plugin ClawHub publish must be dispatched by OpenClaw Release Publish after its npm-release gate." >&2 - exit 1 + if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then + echo "Plugin ClawHub publish dispatched by another workflow must include release_publish_run_id." >&2 + exit 1 + fi + echo "Direct Plugin ClawHub Release dispatch; relying on this workflow's clawhub-plugin-release environment approval." + exit 0 fi if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then echo "Plugin ClawHub publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2 diff --git a/.github/workflows/plugin-npm-release.yml b/.github/workflows/plugin-npm-release.yml index 1ccde1682ad3..819e1ba1f8d2 100644 --- a/.github/workflows/plugin-npm-release.yml +++ b/.github/workflows/plugin-npm-release.yml @@ -194,8 +194,12 @@ jobs: run: | set -euo pipefail if [[ -z "${RELEASE_PUBLISH_RUN_ID// }" ]]; then - echo "Plugin npm publish must be dispatched by OpenClaw Release Publish after its npm-release gate." >&2 - exit 1 + if [[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]]; then + echo "Plugin npm publish dispatched by another workflow must include release_publish_run_id." >&2 + exit 1 + fi + echo "Direct Plugin NPM Release dispatch; relying on this workflow's npm-release environment approval." + exit 0 fi if [[ "${GITHUB_ACTOR}" != "github-actions[bot]" ]]; then echo "Plugin npm publish must be dispatched by the OpenClaw Release Publish workflow, not directly by ${GITHUB_ACTOR}." >&2 diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index ce91008ebb1f..086ec3bfffe4 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -721,9 +721,12 @@ describe("package artifact reuse", () => { expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}"); expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@"'); expect(preparePackageJob.name).toBe("Prepare release package artifact"); - expect(preparePackageJob.needs).toEqual(["resolve_target"]); + expect(preparePackageJob.needs).toEqual(["resolve_target", "docker_runtime_assets_preflight"]); expect(preparePackageJob.if).toContain("inputs.rerun_group == 'all'"); expect(preparePackageJob.if).toContain("inputs.release_profile == 'full'"); + expect(preparePackageJob.if).toContain( + "needs.docker_runtime_assets_preflight.result == 'success'", + ); expectTextToIncludeAll( workflowStep(preparePackageJob, "Resolve release package artifact").run, [ @@ -883,6 +886,7 @@ describe("package artifact reuse", () => { it("keeps release publish creation compatible with gh api and prerelease notes", () => { const workflow = readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8"); const npmWorkflow = readFileSync(".github/workflows/openclaw-npm-release.yml", "utf8"); + const fullReleaseWorkflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8"); expect(workflow).toContain("timeout-minutes: 60"); expect(workflow).toContain("environment: npm-release"); @@ -898,12 +902,20 @@ describe("package artifact reuse", () => { expect(npmWorkflow).toContain("preflight-manifest.json"); expect(npmWorkflow).toContain("Verify full release validation run metadata"); expect(npmWorkflow).toContain("Verify full release validation target"); - expect(npmWorkflow).toContain("Verify Docker runtime-assets prune path"); - expect(npmWorkflow).toContain("--target runtime-assets"); + expect(npmWorkflow).not.toContain("Verify Docker runtime-assets prune path"); + expect(fullReleaseWorkflow).toContain("docker_runtime_assets_preflight"); + expect(fullReleaseWorkflow).toContain("Verify Docker runtime-assets prune path"); + expect(fullReleaseWorkflow).toContain("--target runtime-assets"); + expect(fullReleaseWorkflow).toContain("inputs.rerun_group == 'all'"); + expect(fullReleaseWorkflow).toContain( + "needs.docker_runtime_assets_preflight.result == 'success'", + ); expect(npmWorkflow).toContain("full_release_validation_run_id"); expect(npmWorkflow).toContain("release_publish_run_id"); expect(npmWorkflow).toContain("Real publish requires full_release_validation_run_id"); - expect(npmWorkflow).toContain("Real publish requires release_publish_run_id"); + expect(npmWorkflow).toContain( + "Workflow-dispatched real publish requires release_publish_run_id", + ); expect(npmWorkflow).toContain("tarballSha256"); expect(workflow).toContain("Checkout release SHA"); expect(workflow).toContain('git show "${TARGET_SHA}:CHANGELOG.md" > "${changelog_file}"'); @@ -962,6 +974,9 @@ describe("package artifact reuse", () => { expect(pluginNpmWorkflow).toContain("Validate release publish approval run"); expect(clawHubWorkflow).toContain("Validate release publish approval run"); expect(openclawNpmWorkflow).toContain("Validate release publish approval run"); + expect(pluginNpmWorkflow).toContain("Direct Plugin NPM Release dispatch"); + expect(clawHubWorkflow).toContain("Direct Plugin ClawHub Release dispatch"); + expect(openclawNpmWorkflow).toContain("Direct OpenClaw npm publish"); expect(pluginNpmWorkflow).toContain('GITHUB_ACTOR}" != "github-actions[bot]"'); expect(clawHubWorkflow).toContain('GITHUB_ACTOR}" != "github-actions[bot]"'); expect(openclawNpmWorkflow).toContain('GITHUB_ACTOR}" != "github-actions[bot]"'); diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index a6928bed046d..51dd3402b52f 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -481,6 +481,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { expect(releaseChecksWorkflow.jobs.summary["runs-on"]).toBe("ubuntu-24.04"); for (const jobName of [ "resolve_target", + "docker_runtime_assets_preflight", "normal_ci", "plugin_prerelease", "release_checks", @@ -493,6 +494,18 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { expect(fullReleaseWorkflow.jobs.normal_ci["timeout-minutes"]).toBe( "${{ inputs.release_profile != 'minimum' && 240 || 60 }}", ); + expect(fullReleaseWorkflow.jobs.normal_ci.needs).toEqual([ + "resolve_target", + "docker_runtime_assets_preflight", + ]); + expect(fullReleaseWorkflow.jobs.docker_runtime_assets_preflight.if).toBe( + "inputs.rerun_group == 'all'", + ); + expect( + fullReleaseWorkflow.jobs.docker_runtime_assets_preflight.steps.find( + (step) => step.name === "Verify Docker runtime-assets prune path", + ).run, + ).toContain("--target runtime-assets"); expect(fullReleaseWorkflow.jobs.plugin_prerelease["timeout-minutes"]).toBe( "${{ inputs.release_profile == 'full' && 300 || inputs.release_profile == 'stable' && 240 || 60 }}", );