name: Full Release Validation on: workflow_dispatch: inputs: ref: description: Branch, tag, or full commit SHA to validate required: true default: main type: string expected_sha: description: Optional full Validation SHA that ref must resolve to required: false default: "" type: string trusted_workflow_json: description: Trusted release tooling identity JSON required: false default: "" type: string target_context_ref: description: Optional canonical release branch or tag context for an exact-SHA target required: false default: "" type: string allow_unreleased_changelog: description: Allow current-tree packaging to use Unreleased notes; release contexts require this explicit opt-in required: false default: false type: boolean skip_package_telegram_e2e: description: Defer beta Package Acceptance Telegram E2E while preserving focused npm Telegram validation required: false default: false type: boolean provider: description: Provider lane for cross-OS onboarding and the end-to-end agent turn required: false default: openai type: choice options: - openai - anthropic - minimax mode: description: Which cross-OS release lanes to run required: false default: both type: choice options: - fresh - upgrade - both release_profile: description: Release coverage profile for live/Docker/provider breadth required: false default: stable type: choice options: - beta - stable - full run_release_soak: description: Run exhaustive live/Docker and upgrade-survivor soak lanes; forced on for stable and full release profiles required: false default: false type: boolean fail_fast: description: Cancel only an exact active child after its first blocking job; false drains all children to completion required: false default: false type: boolean rerun_group: description: Validation group to run required: false default: all type: choice options: - all - ci - plugin-prerelease - install-smoke - cross-os - live-e2e - package - qa-parity - qa-live - npm-telegram - performance plugin_prerelease_node_exclude_patterns_json: description: Exact Plugin Prerelease Node test paths omitted only for frozen-target validation required: false default: "[]" type: string reuse_evidence: description: Reuse matching green product validation for the same target or a changelog-only Release SHA required: false default: true type: boolean live_suite_filter: description: Optional exact live/E2E suite id, or comma-separated QA live lane ids (qa-live-matrix, qa-live-telegram, qa-live-discord, qa-live-whatsapp, qa-live-slack); blank runs all selected live suites required: false default: "" type: string cross_os_suite_filter: description: Optional focused cross-OS suite filter, e.g. windows/packaged-upgrade or packaged-fresh required: false default: "" type: string npm_telegram_package_spec: description: Optional published package spec for the focused package Telegram E2E rerun required: false default: "" type: string release_package_spec: description: Optional published package spec for release checks and package lanes; blank builds a SHA package artifact required: false default: "" type: string evidence_package_spec: description: Optional published package spec to prove in the release evidence report required: false default: "" type: string dispatch_release_evidence: description: Dispatch the validated run to openclaw/releases after child proof succeeds required: false default: false type: boolean package_acceptance_package_spec: description: Optional published package spec for Package Acceptance; blank uses the SHA-built release artifact required: false default: "" type: string codex_plugin_spec: description: Optional Codex plugin install spec for live Docker package checks; blank derives from release_package_spec or packs the selected ref required: false default: "" type: string npm_telegram_provider_mode: description: Provider mode for the focused package Telegram E2E rerun required: false default: mock-openai type: choice options: - mock-openai - live-frontier npm_telegram_scenario: description: Optional comma-separated Telegram scenario ids for the focused package Telegram E2E rerun required: false default: "" type: string permissions: actions: write contents: read concurrency: group: full-release-validation-${{ inputs.expected_sha || inputs.ref }}-${{ github.sha }}-${{ inputs.rerun_group }} cancel-in-progress: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" GH_REPO: ${{ github.repository }} # Read retries and one-shot dispatch recovery share this classifier; dispatch POSTs never retry. GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN: "HTTP 5[0-9][0-9]|Server Error|invalid character .* looking for beginning of value|error connecting to|context deadline exceeded|connection reset by peer|connection refused|TLS handshake timeout|i/o timeout|network is unreachable|(^|[^A-Za-z0-9_])EOF([^A-Za-z0-9_]|$)|ETIMEDOUT|ECONNRESET|EAI_AGAIN" NODE_VERSION: "24.16.0" RELEASE_ISOLATION_TOOLING_CONTRACT: "2" jobs: resolve_target: name: Resolve target ref runs-on: ubuntu-24.04 timeout-minutes: 10 outputs: sha: ${{ steps.resolve.outputs.sha }} trusted_workflow_json: ${{ steps.tooling_identity.outputs.json }} live_suite_filter: ${{ steps.filters.outputs.live_suite_filter }} cross_os_suite_filter: ${{ steps.filters.outputs.cross_os_suite_filter }} steps: - name: Checkout trusted workflow helper uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} path: workflow fetch-depth: 1 persist-credentials: false submodules: false - name: Resolve trusted workflow identity id: tooling_identity env: GH_TOKEN: ${{ github.token }} REQUESTED_IDENTITY_JSON: ${{ inputs.trusted_workflow_json }} WORKFLOW_CONTRACT: ${{ env.RELEASE_ISOLATION_TOOLING_CONTRACT }} WORKFLOW_FULL_REF: ${{ github.ref }} WORKFLOW_REF: ${{ github.ref_name }} WORKFLOW_SHA: ${{ github.sha }} run: | set -euo pipefail identity="$( node workflow/scripts/release-tooling-identity.mjs resolve \ --repository "$GITHUB_REPOSITORY" \ --workflow-contract "$WORKFLOW_CONTRACT" \ --workflow-ref "$WORKFLOW_REF" \ --workflow-full-ref "$WORKFLOW_FULL_REF" \ --workflow-sha "$WORKFLOW_SHA" \ --requested-identity-json "$REQUESTED_IDENTITY_JSON" )" echo "json=${identity}" >> "$GITHUB_OUTPUT" - name: Resolve target SHA id: resolve env: TARGET_REF: ${{ inputs.ref }} TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} EXPECTED_SHA: ${{ inputs.expected_sha }} run: | bash workflow/scripts/github/resolve-openclaw-ref.sh \ --ref "$TARGET_REF" \ --expected-sha "$EXPECTED_SHA" \ --github-output "$GITHUB_OUTPUT" - name: Validate suite filters id: filters env: RERUN_GROUP: ${{ inputs.rerun_group }} RAW_LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }} RAW_CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }} RELEASE_FILTER_VALIDATOR: workflow/scripts/github/validate-release-suite-filters.sh run: | set -euo pipefail source "$RELEASE_FILTER_VALIDATOR" validate_release_suite_filters \ "$RERUN_GROUP" \ "$RAW_LIVE_SUITE_FILTER" \ "$RAW_CROSS_OS_SUITE_FILTER" \ controller { printf 'live_suite_filter=%s\n' "$RELEASE_FILTER_LIVE_SUITE_FILTER" printf 'cross_os_suite_filter=%s\n' "$RELEASE_FILTER_CROSS_OS_SUITE_FILTER" } >> "$GITHUB_OUTPUT" - name: Checkout target package manifest uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ steps.resolve.outputs.sha }} path: target sparse-checkout: package.json sparse-checkout-cone-mode: false fetch-depth: 1 persist-credentials: false submodules: false - name: Validate release inputs env: GH_TOKEN: ${{ github.token }} RELEASE_PROFILE: ${{ inputs.release_profile }} SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }} TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} TARGET_REF: ${{ inputs.ref }} TARGET_SHA: ${{ steps.resolve.outputs.sha }} run: | set -euo pipefail if [[ "$SKIP_PACKAGE_TELEGRAM_E2E" == "true" && "$RELEASE_PROFILE" != "beta" ]]; then echo "skip_package_telegram_e2e is allowed only for release_profile=beta." >&2 exit 1 fi context_ref="${TARGET_CONTEXT_REF:-$TARGET_REF}" context_ref="${context_ref#refs/heads/}" context_ref="${context_ref#refs/tags/}" target_version="$(jq -er '.version | select(type == "string")' target/package.json)" extended_stable_line="" release_version_pattern="" expected_version="" identity_kind="" if [[ "$context_ref" =~ ^release/([0-9]{4}\.([1-9]|1[0-2])\.[1-9][0-9]*)$ ]]; then expected_version="${BASH_REMATCH[1]}" release_version_pattern="${expected_version//./\\.}" identity_kind="release branch" if [[ "$target_version" != "$expected_version" && ! "$target_version" =~ ^${release_version_pattern}-beta\.[1-9][0-9]*$ ]]; then echo "Target package version ${target_version} does not belong to release branch ${context_ref}; expected ${expected_version} or a beta prerelease of it." >&2 exit 1 fi elif [[ "$context_ref" =~ ^extended-stable/([0-9]{4}\.([1-9]|1[0-2])\.33)$ ]]; then extended_stable_line="${BASH_REMATCH[1]%.33}" identity_kind="extended-stable branch" elif [[ "$context_ref" =~ ^v([0-9]{4}\.([1-9]|1[0-2])\.[1-9][0-9]*(-(alpha|beta)\.[1-9][0-9]*)?)$ ]]; then expected_version="${BASH_REMATCH[1]}" identity_kind="release tag" elif [[ -n "$TARGET_CONTEXT_REF" ]]; then echo "target_context_ref must be a canonical OpenClaw release branch or tag." >&2 exit 1 fi if [[ "$identity_kind" == "extended-stable branch" ]]; then if [[ ! "$target_version" =~ ^([0-9]{4}\.([1-9]|1[0-2]))\.([1-9][0-9]*)$ ]] || [[ "${BASH_REMATCH[1]}" != "$extended_stable_line" ]] || (( 10#${BASH_REMATCH[3]} < 33 )); then echo "Target package version ${target_version} does not belong to extended-stable branch ${context_ref}; expected a final ${extended_stable_line}.PATCH version with PATCH >= 33." >&2 exit 1 fi elif [[ -n "$expected_version" && "$identity_kind" != "release branch" && "$target_version" != "$expected_version" ]]; then echo "Target package version ${target_version} does not match ${identity_kind} ${context_ref}; expected ${expected_version}." >&2 exit 1 fi if [[ -n "$TARGET_CONTEXT_REF" ]]; then if [[ ! "$TARGET_REF" =~ ^[a-f0-9]{40}$ || "$TARGET_REF" != "$TARGET_SHA" ]]; then echo "target_context_ref requires ref to be the resolved full Validation SHA." >&2 exit 1 fi repository_url="https://github.com/${GITHUB_REPOSITORY}.git" if [[ "$identity_kind" == "release tag" ]]; then remote_sha="$(git ls-remote --tags "$repository_url" "refs/tags/${context_ref}^{}" | awk 'NR == 1 { print $1 }')" if [[ -z "$remote_sha" ]]; then remote_sha="$(git ls-remote --tags "$repository_url" "refs/tags/${context_ref}" | awk 'NR == 1 { print $1 }')" fi if [[ "$remote_sha" != "$TARGET_SHA" ]]; then echo "Target SHA ${TARGET_SHA} does not match release tag ${context_ref} at ${remote_sha:-missing}." >&2 exit 1 fi else remote_sha="$(git ls-remote --heads "$repository_url" "refs/heads/${context_ref}" | awk 'NR == 1 { print $1 }')" if [[ -z "$remote_sha" ]]; then echo "Release context branch ${context_ref} does not resolve." >&2 exit 1 fi comparison_status="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${TARGET_SHA}...${remote_sha}" --jq .status)" if [[ "$comparison_status" != "ahead" && "$comparison_status" != "identical" ]]; then echo "Target SHA ${TARGET_SHA} is not reachable from release context branch ${context_ref} at ${remote_sha}." >&2 exit 1 fi fi fi - name: Summarize target env: TARGET_REF: ${{ inputs.ref }} TARGET_SHA: ${{ steps.resolve.outputs.sha }} TOOLING_SHA: ${{ github.sha }} NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} RELEASE_PROFILE: ${{ inputs.release_profile }} RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }} FAIL_FAST: ${{ inputs.fail_fast }} SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }} ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }} RERUN_GROUP: ${{ inputs.rerun_group }} LIVE_SUITE_FILTER: ${{ steps.filters.outputs.live_suite_filter }} CROSS_OS_SUITE_FILTER: ${{ steps.filters.outputs.cross_os_suite_filter }} PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }} run: | plugin_prerelease_node_exclusions="$(jq -c . <<< "$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON")" { echo "## Full release validation" echo echo "- Target ref: \`${TARGET_REF}\`" echo "- Validation SHA: \`${TARGET_SHA}\`" echo "- Tooling SHA: \`${TOOLING_SHA}\`" echo "- Frozen tuple: \`${TARGET_SHA} / ${TOOLING_SHA} / ${RERUN_GROUP}\`" echo "- Release soak lanes: \`${RUN_RELEASE_SOAK}\`" echo "- Fail fast: \`${FAIL_FAST}\`" echo "- Package Acceptance Telegram E2E deferred: \`${SKIP_PACKAGE_TELEGRAM_E2E}\`" echo "- Allow Unreleased changelog packaging: \`${ALLOW_UNRELEASED_CHANGELOG}\`" echo "- Rerun group: \`${RERUN_GROUP}\`" if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then echo "- Live suite filter: \`${LIVE_SUITE_FILTER}\`" fi if [[ -n "${CROSS_OS_SUITE_FILTER// }" ]]; then echo "- Cross-OS suite filter: \`${CROSS_OS_SUITE_FILTER}\`" fi if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "ci" ]]; then echo "- Normal CI: \`CI\` with \`target_ref=${TARGET_SHA}\`" else echo "- Normal CI: skipped by rerun group" fi if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "performance" ]]; then echo "- Product performance: \`OpenClaw Performance\` with \`target_ref=${TARGET_SHA}\`" else echo "- Product performance: skipped by rerun group" fi if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "plugin-prerelease" ]]; then echo "- Plugin prerelease: \`Plugin Prerelease\` with \`target_ref=${TARGET_SHA}\`" echo "- Plugin prerelease Node exclusions: \`${plugin_prerelease_node_exclusions}\`" else echo "- Plugin prerelease: skipped by rerun group" fi if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "install-smoke" || "$RERUN_GROUP" == "cross-os" || "$RERUN_GROUP" == "live-e2e" || "$RERUN_GROUP" == "package" || "$RERUN_GROUP" == "qa-parity" || "$RERUN_GROUP" == "qa-live" ]]; then echo "- Release/live/Docker/package/QA: \`OpenClaw Release Checks\`" else echo "- Release/live/Docker/package/QA: skipped by rerun group" fi if [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then echo "- Published release package: \`${RELEASE_PACKAGE_SPEC}\`" fi if [[ "$RERUN_GROUP" == "npm-telegram" && -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then echo "- Published-package Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`" elif [[ "$RERUN_GROUP" == "npm-telegram" && -n "${RELEASE_PACKAGE_SPEC// }" ]]; then echo "- Published-package Telegram E2E: \`${RELEASE_PACKAGE_SPEC}\`" elif [[ "$RERUN_GROUP" == "npm-telegram" ]]; then echo "- Package Telegram E2E: focused rerun requires \`release_package_spec\` or \`npm_telegram_package_spec\`" elif [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "package" ]]; then if [[ "$SKIP_PACKAGE_TELEGRAM_E2E" == "true" ]]; then echo "- Package Telegram E2E: deferred by \`skip_package_telegram_e2e\`" else echo "- Package Telegram E2E: OpenClaw Release Checks Package Acceptance" fi else echo "- Package Telegram E2E: skipped by rerun group" fi if [[ -n "${EVIDENCE_PACKAGE_SPEC// }" ]]; then echo "- Private evidence package proof: \`${EVIDENCE_PACKAGE_SPEC}\`" fi if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`" elif [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then echo "- Package Acceptance package spec: \`${RELEASE_PACKAGE_SPEC}\`" else echo "- Package Acceptance package spec: SHA-built release artifact" fi if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`" fi } >> "$GITHUB_STEP_SUMMARY" 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/')) runs-on: ubuntu-24.04 timeout-minutes: 10 outputs: reuse: ${{ steps.find.outputs.reuse }} evidence_run_id: ${{ steps.find.outputs.evidence_run_id }} evidence_root_run_id: ${{ steps.find.outputs.evidence_root_run_id }} evidence_run_url: ${{ steps.find.outputs.evidence_run_url }} evidence_sha: ${{ steps.find.outputs.evidence_sha }} evidence_policy: ${{ steps.find.outputs.evidence_policy }} evidence_manifest: ${{ steps.find.outputs.evidence_manifest }} changed_paths: ${{ steps.find.outputs.changed_paths }} steps: - name: Checkout trusted workflow helper uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} path: workflow fetch-depth: 1 persist-credentials: false submodules: false - name: Checkout target SHA uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ needs.resolve_target.outputs.sha }} path: target fetch-depth: 1 persist-credentials: false submodules: false - name: Find reusable validation evidence id: find env: GH_TOKEN: ${{ github.token }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} WORKFLOW_REF: ${{ github.ref_name }} RELEASE_PROFILE: ${{ inputs.release_profile }} RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }} PROVIDER: ${{ inputs.provider }} MODE: ${{ inputs.mode }} TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} LIVE_SUITE_FILTER: ${{ needs.resolve_target.outputs.live_suite_filter }} CROSS_OS_SUITE_FILTER: ${{ needs.resolve_target.outputs.cross_os_suite_filter }} RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }} NPM_TELEGRAM_SCENARIO: ${{ inputs.npm_telegram_scenario }} SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }} ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }} PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }} TRUSTED_WORKFLOW_JSON: ${{ needs.resolve_target.outputs.trusted_workflow_json }} run: | set -euo pipefail # Lane-selection inputs must match the prior run's manifest exactly; # a default-input run must not stand in for a focused one. inputs_json="$(jq -nc \ --arg provider "$PROVIDER" \ --arg mode "$MODE" \ --arg targetContextRef "$TARGET_CONTEXT_REF" \ --arg liveSuiteFilter "$LIVE_SUITE_FILTER" \ --arg crossOsSuiteFilter "$CROSS_OS_SUITE_FILTER" \ --arg releasePackageSpec "$RELEASE_PACKAGE_SPEC" \ --arg packageAcceptancePackageSpec "$PACKAGE_ACCEPTANCE_PACKAGE_SPEC" \ --arg codexPluginSpec "$CODEX_PLUGIN_SPEC" \ --arg npmTelegramPackageSpec "$NPM_TELEGRAM_PACKAGE_SPEC" \ --arg npmTelegramProviderMode "$NPM_TELEGRAM_PROVIDER_MODE" \ --arg npmTelegramScenario "$NPM_TELEGRAM_SCENARIO" \ --arg skipPackageTelegramE2e "$SKIP_PACKAGE_TELEGRAM_E2E" \ --arg allowUnreleasedChangelog "$ALLOW_UNRELEASED_CHANGELOG" \ --arg pluginPrereleaseNodeExcludePatternsJson "$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON" \ '{ provider: $provider, mode: $mode, targetContextRef: $targetContextRef, liveSuiteFilter: $liveSuiteFilter, crossOsSuiteFilter: $crossOsSuiteFilter, releasePackageSpec: $releasePackageSpec, packageAcceptancePackageSpec: $packageAcceptancePackageSpec, codexPluginSpec: $codexPluginSpec, npmTelegramPackageSpec: $npmTelegramPackageSpec, npmTelegramProviderMode: $npmTelegramProviderMode, npmTelegramScenario: $npmTelegramScenario, skipPackageTelegramE2e: $skipPackageTelegramE2e, allowUnreleasedChangelog: $allowUnreleasedChangelog, pluginPrereleaseNodeExcludePatternsJson: $pluginPrereleaseNodeExcludePatternsJson }')" trusted_workflow_json="${TRUSTED_WORKFLOW_JSON}" trusted_workflow_ref="$(jq -er '.ref | select(type == "string" and length > 0)' <<< "$trusted_workflow_json")" trusted_workflow_full_ref="$(jq -er '.fullRef | select(type == "string" and length > 0)' <<< "$trusted_workflow_json")" trusted_workflow_sha="$(jq -er '.sha | select(type == "string" and test("^[0-9a-f]{40}$"))' <<< "$trusted_workflow_json")" bash workflow/scripts/github/find-reusable-release-validation.sh \ --target-sha "$TARGET_SHA" \ --workflow-sha "$GITHUB_SHA" \ --workflow-ref "$WORKFLOW_REF" \ --trusted-workflow-ref "$trusted_workflow_ref" \ --trusted-workflow-full-ref "$trusted_workflow_full_ref" \ --trusted-workflow-sha "$trusted_workflow_sha" \ --release-profile "$RELEASE_PROFILE" \ --run-release-soak "$RUN_RELEASE_SOAK" \ --inputs-json "$inputs_json" \ --repo "$GITHUB_REPOSITORY" \ --repo-dir target \ --github-output "$GITHUB_OUTPUT" - name: Summarize evidence reuse env: REUSE: ${{ steps.find.outputs.reuse }} REUSE_REASON: ${{ steps.find.outputs.reuse_reason }} EVIDENCE_RUN_URL: ${{ steps.find.outputs.evidence_run_url }} EVIDENCE_SHA: ${{ steps.find.outputs.evidence_sha }} CHANGED_PATHS: ${{ steps.find.outputs.changed_paths }} run: | changed_paths_summary="$(jq -r 'if length == 0 then "none" else join(", ") end' <<< "${CHANGED_PATHS:-[]}")" { echo "## Validation evidence reuse" echo if [[ "$REUSE" == "true" ]]; then echo "- Reusing evidence: ${EVIDENCE_RUN_URL}" echo "- Evidence SHA: \`${EVIDENCE_SHA}\`" echo "- Reused validation changed paths: \`${changed_paths_summary}\`" else echo "- No reusable evidence: ${REUSE_REASON:-unknown}" fi } >> "$GITHUB_STEP_SUMMARY" docker_runtime_assets_preflight: name: Verify Docker runtime image assets needs: [resolve_target, evidence_reuse] if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && inputs.rerun_group == 'all' && needs.evidence_reuse.outputs.reuse != 'true' }} runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: contents: read steps: - name: Checkout target SHA uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 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 --kill-after=30s 15m docker build \ --target runtime-assets \ --build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \ . prepare_release_candidate: name: Prepare shared release candidate needs: [resolve_target, evidence_reuse] if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && needs.evidence_reuse.outputs.reuse != 'true' && inputs.release_package_spec == '' && inputs.package_acceptance_package_spec == '' && (contains(fromJSON('["all","plugin-prerelease","cross-os","package"]'), inputs.rerun_group) || (inputs.rerun_group == 'live-e2e' && needs.resolve_target.outputs.live_suite_filter == '')) }} permissions: actions: read contents: read packages: read pull-requests: read uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml with: ref: ${{ needs.resolve_target.outputs.sha }} prepare_only: true include_repo_e2e: false include_release_path_suites: false include_openwebui: false include_live_suites: false enable_prepublish_plugin_registry: true published_upgrade_survivor_scenarios: ${{ (inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full') && 'reported-issues' || '' }} allow_frozen_target_scenario_omissions: ${{ inputs.target_context_ref != '' }} allow_unreleased_changelog: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }} release_test_profile: ${{ inputs.release_profile }} shared_image_artifact_namespace: full-release shared_image_policy: no-push-artifact 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('["all","ci"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }} runs-on: ubuntu-24.04 timeout-minutes: 15 outputs: run_id: ${{ steps.dispatch.outputs.run_id }} run_attempt: ${{ steps.dispatch.outputs.run_attempt }} url: ${{ steps.dispatch.outputs.url }} steps: - name: Dispatch CI id: dispatch env: GH_TOKEN: ${{ github.token }} CHILD_WORKFLOW_KIND: ci TARGET_REF: ${{ inputs.ref }} TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} CHILD_WORKFLOW_REF: ${{ github.ref_name }} PARENT_WORKFLOW_SHA: ${{ github.sha }} run: &full_release_child_dispatch | set -euo pipefail gh_with_retry() { local output status attempt for attempt in 1 2 3 4 5 6; do set +e output="$(gh "$@" 2>&1)" status=$? set -e if [[ "$status" -eq 0 ]]; then printf '%s\n' "$output" return 0 fi if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"HTTP 429"* || "$output" == *"abuse detection"* || "$output" == *"Sorry. Your account was suspended"* || "$output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2 sleep $((attempt * 10)) continue fi printf '%s\n' "$output" >&2 return "$status" done printf '%s\n' "$output" >&2 return "$status" } validate_child_run() { local candidate_run_id="$1" local candidate_run_json attempt if [[ ! "$candidate_run_id" =~ ^[0-9]+$ ]]; then echo "::error::Refusing to adopt invalid ${workflow} run ID ${candidate_run_id}." >&2 return 1 fi # GitHub materializes a dispatched run asynchronously: the API can 404 the # fresh run id (read-after-write lag) and can briefly report the default # display_title before run-name evaluates. Both are retryable; a REAL run # object with mismatched identity fields still refuses immediately. for attempt in $(seq 1 12); do if ! candidate_run_json="$( gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${candidate_run_id}" )" || ! jq -e --argjson id "$candidate_run_id" '.id == $id' \ <<< "$candidate_run_json" >/dev/null 2>&1; then echo "Waiting for ${workflow} run ${candidate_run_id} to become readable (attempt ${attempt})." >&2 sleep 5 continue fi if ! jq -e \ --argjson workflow_id "$expected_workflow_id" \ --arg branch "$CHILD_WORKFLOW_REF" \ '(.workflow_id == $workflow_id) and (.head_branch == $branch) and (.event == "workflow_dispatch")' \ <<< "$candidate_run_json" >/dev/null; then echo "::error::Refusing to adopt unvalidated ${workflow} run ${candidate_run_id}." >&2 jq '{id, workflow_id, path, display_title, head_branch, head_sha, event, html_url}' \ <<< "$candidate_run_json" >&2 return 1 fi if jq -e --arg title "$dispatch_run_name" '.display_title == $title' \ <<< "$candidate_run_json" >/dev/null; then printf '%s\n' "$candidate_run_json" return 0 fi echo "Waiting for ${workflow} run ${candidate_run_id} display title (attempt ${attempt})." >&2 sleep 5 done echo "::error::Refusing to adopt ${workflow} run ${candidate_run_id}: run never became readable with display title ${dispatch_run_name}." >&2 jq '{id, workflow_id, path, display_title, head_branch, head_sha, event, html_url}' \ <<< "$candidate_run_json" >&2 || printf '%s\n' "$candidate_run_json" >&2 return 1 } dispatch_child() { local workflow="$1" local dispatch_run_name="$2" shift 2 local dispatch_output dispatch_status dispatch_run_ids matches_json match_count run_id run_json child_head_sha child_run_attempt url encoded_workflow_ref current_workflow_sha expected_workflow_id encoded_workflow_ref="$(jq -rn --arg value "$CHILD_WORKFLOW_REF" '$value | @uri')" current_workflow_sha="$( gh_with_retry api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}" --jq .sha )" if [[ "$current_workflow_sha" != "$PARENT_WORKFLOW_SHA" ]]; then echo "::error::Child workflow ref ${CHILD_WORKFLOW_REF} moved to ${current_workflow_sha}, expected ${PARENT_WORKFLOW_SHA}; refusing dispatch." >&2 return 1 fi expected_workflow_id="$( gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}" --jq .id )" # The dispatch POST is one-shot: adopt its returned identity when available. # If absent or ambiguous, exact-name recovery avoids a duplicate child. set +e dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)" dispatch_status=$? set -e printf '%s\n' "$dispatch_output" if [[ "$dispatch_status" -ne 0 && ! "$dispatch_output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then echo "::error::${workflow} dispatch failed with non-ambiguous status ${dispatch_status}; refusing adoption polling." >&2 exit "$dispatch_status" fi run_id="" dispatch_run_ids="$( sed -nE "s#^https://github[.]com/${GITHUB_REPOSITORY}/actions/runs/([0-9]+)\$#\1#p" \ <<< "$dispatch_output" | sort -u )" if [[ "$dispatch_run_ids" == *$'\n'* ]]; then echo "::error::${workflow} dispatch returned multiple run identities; refusing to guess." >&2 exit 1 elif [[ -n "$dispatch_run_ids" ]]; then run_id="$dispatch_run_ids" else for _ in $(seq 1 60); do if matches_json="$( DISPATCH_RUN_NAME="$dispatch_run_name" CHILD_WORKFLOW_REF="$CHILD_WORKFLOW_REF" \ gh_with_retry api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \ -F event=workflow_dispatch \ -F per_page=100 \ --jq '[.workflow_runs[] | select(.display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF) | .id]' )"; then match_count="$(jq 'length' <<< "$matches_json")" if (( match_count > 1 )); then echo "::error::Multiple runs matched ${dispatch_run_name}; refusing to guess." >&2 exit 1 fi if (( match_count == 1 )); then run_id="$(jq -r '.[0]' <<< "$matches_json")" break fi fi sleep 5 done fi if [[ -z "$run_id" ]]; then echo "::error::Could not find exact dispatched run ${dispatch_run_name}; dispatch status ${dispatch_status}. The dispatch was not retried to avoid creating a duplicate child." >&2 exit 1 fi run_json="$(validate_child_run "$run_id")" { echo "- Adopted child: \`${workflow}\` run \`${run_id}\`" echo "- Release Decision owns blocking policy; Diagnostic Drain owns terminal collection." } >> "$GITHUB_STEP_SUMMARY" child_head_sha="$(jq -r '.head_sha // ""' <<< "$run_json")" if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then echo "::error::${workflow} child run used workflow SHA ${child_head_sha}, expected parent workflow SHA ${PARENT_WORKFLOW_SHA}." >&2 exit 1 fi if [[ "$dispatch_status" -ne 0 ]]; then echo "::warning::${workflow} dispatch returned status ${dispatch_status}; adopted exact run ${run_id}." >&2 fi child_run_attempt="$(jq -r '.run_attempt // ""' <<< "$run_json")" url="$(jq -r '.html_url // ""' <<< "$run_json")" if [[ ! "$child_run_attempt" =~ ^[1-9][0-9]*$ || -z "${url// }" ]]; then echo "::error::${workflow} child run omitted its attempt or URL." >&2 exit 1 fi echo "Dispatched ${workflow}: ${url} (attempt ${child_run_attempt})" echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" echo "run_attempt=${child_run_attempt}" >> "$GITHUB_OUTPUT" echo "url=${url}" >> "$GITHUB_OUTPUT" } case "$CHILD_WORKFLOW_KIND" in ci) { echo "### Normal CI" echo echo "- Target ref: \`${TARGET_REF}\`" echo "- Target SHA: \`${TARGET_SHA}\`" } >> "$GITHUB_STEP_SUMMARY" dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-ci" dispatch_run_name="CI ${dispatch_id}" args=(-f target_ref="$TARGET_SHA" -f include_android=true -f dispatch_id="$dispatch_id") if [[ "$TARGET_REF" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?$ ]]; then args+=(-f historical_target_tag="$TARGET_REF") elif [[ "$TARGET_CONTEXT_REF" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?$ ]]; then args+=(-f historical_target_tag="$TARGET_CONTEXT_REF") elif [[ "$TARGET_CONTEXT_REF" =~ ^(release/[0-9]{4}\.[0-9]+\.[0-9]+|extended-stable/[0-9]{4}\.[0-9]+\.33)$ ]]; then args+=(-f target_context_ref="$TARGET_CONTEXT_REF") fi dispatch_child ci.yml "$dispatch_run_name" "${args[@]}" ;; plugin-prerelease) plugin_prerelease_node_exclusions="$( jq -c . <<< "$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON" )" { echo "### Plugin prerelease" echo echo "- Target ref: \`${TARGET_REF}\`" echo "- Target SHA: \`${TARGET_SHA}\`" echo "- Frozen-target Node test omissions: \`${plugin_prerelease_node_exclusions}\`" } >> "$GITHUB_STEP_SUMMARY" dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-plugin-prerelease" dispatch_run_name="Plugin Prerelease ${dispatch_id}" args=(-f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true -f dispatch_id="$dispatch_id" -f node_test_exclude_patterns_json="$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON") if [[ -n "${CANDIDATE_ARTIFACT_JSON// }" ]]; then args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON") fi dispatch_child plugin-prerelease.yml "$dispatch_run_name" "${args[@]}" ;; release-checks) { echo "### Release/live/Docker/QA validation" echo echo "- Target ref: \`${TARGET_REF}\`" echo "- Target SHA: \`${TARGET_SHA}\`" echo "- Provider: \`${PROVIDER}\`" echo "- Cross-OS mode: \`${MODE}\`" echo "- Release profile: \`${RELEASE_PROFILE}\`" echo "- Release soak lanes: \`${RUN_RELEASE_SOAK}\`" echo "- Rerun group: \`${RERUN_GROUP}\`" if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then echo "- Live suite filter: \`${LIVE_SUITE_FILTER}\`" fi if [[ -n "${CROSS_OS_SUITE_FILTER// }" ]]; then echo "- Cross-OS suite filter: \`${CROSS_OS_SUITE_FILTER}\`" fi if [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then echo "- Release package spec: \`${RELEASE_PACKAGE_SPEC}\`" fi if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`" fi if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`" fi echo "- Package Telegram E2E deferred: \`${SKIP_PACKAGE_TELEGRAM_E2E}\`" } >> "$GITHUB_STEP_SUMMARY" args=( -f ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f provider="$PROVIDER" -f mode="$MODE" -f release_profile="$RELEASE_PROFILE" -f run_release_soak="$RUN_RELEASE_SOAK" -f fail_fast="$FAIL_FAST" -f allow_unreleased_changelog="$ALLOW_UNRELEASED_CHANGELOG" -f skip_package_telegram_e2e="$SKIP_PACKAGE_TELEGRAM_E2E" -f rerun_group="$RERUN_GROUP" ) if [[ -n "${TARGET_CONTEXT_REF// }" ]]; then args+=(-f target_context_ref="$TARGET_CONTEXT_REF") args+=(-f allow_frozen_target_scenario_omissions=true) fi if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then args+=(-f live_suite_filter="$LIVE_SUITE_FILTER") fi if [[ -n "${CROSS_OS_SUITE_FILTER// }" ]]; then args+=(-f cross_os_suite_filter="$CROSS_OS_SUITE_FILTER") fi if [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then args+=(-f release_package_spec="$RELEASE_PACKAGE_SPEC") fi if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then args+=(-f package_acceptance_package_spec="$PACKAGE_ACCEPTANCE_PACKAGE_SPEC") fi if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then args+=(-f codex_plugin_spec="$CODEX_PLUGIN_SPEC") fi if [[ -n "${CANDIDATE_ARTIFACT_JSON// }" ]]; then args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON") fi dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-release-checks" dispatch_run_name="OpenClaw Release Checks ${dispatch_id}" args+=(-f dispatch_id="$dispatch_id") dispatch_child openclaw-release-checks.yml "$dispatch_run_name" "${args[@]}" ;; npm-telegram) args=(-f package_spec="$PACKAGE_SPEC" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE") if [[ -n "${SCENARIO// }" ]]; then args+=(-f scenario="$SCENARIO") fi dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-npm-telegram" dispatch_run_name="NPM Telegram Beta E2E ${dispatch_id}" args+=(-f dispatch_id="$dispatch_id") dispatch_child npm-telegram-beta-e2e.yml "$dispatch_run_name" "${args[@]}" ;; performance) fail_on_regression=true if [[ "$RELEASE_PROFILE" == "beta" ]]; then fail_on_regression=false fi { echo "### Product performance" echo echo "- Target SHA: \`${TARGET_SHA}\`" echo "- Profile: \`release\`" echo "- Repeat: \`3\`" echo "- Deep profile: \`false\`" echo "- Live OpenAI candidate: \`false\`" echo "- Regression gate: \`${fail_on_regression}\`" echo "- Report publication: disabled (artifacts only)" if [[ "$RELEASE_PROFILE" == "beta" ]]; then echo "- Release impact: advisory" else echo "- Release impact: blocking" fi } >> "$GITHUB_STEP_SUMMARY" dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" dispatch_run_name="OpenClaw Performance ${dispatch_id}" args=( -f target_ref="$TARGET_SHA" -f profile=release -f repeat=3 -f deep_profile=false -f live_openai_candidate=false -f fail_on_regression="$fail_on_regression" -f publish_reports=false -f dispatch_id="$dispatch_id" ) dispatch_child openclaw-performance.yml "$dispatch_run_name" "${args[@]}" ;; *) echo "::error::Unsupported full-release child workflow kind ${CHILD_WORKFLOW_KIND}." >&2 exit 2 ;; esac plugin_prerelease: name: Run plugin prerelease validation needs: [resolve_target, evidence_reuse, prepare_release_candidate] if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && (needs.prepare_release_candidate.result == 'success' || needs.prepare_release_candidate.result == 'skipped') && contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }} runs-on: ubuntu-24.04 timeout-minutes: 15 outputs: run_id: ${{ steps.dispatch.outputs.run_id }} run_attempt: ${{ steps.dispatch.outputs.run_attempt }} url: ${{ steps.dispatch.outputs.url }} steps: - name: Dispatch plugin prerelease id: dispatch env: GH_TOKEN: ${{ github.token }} CHILD_WORKFLOW_KIND: plugin-prerelease TARGET_REF: ${{ inputs.ref }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} CHILD_WORKFLOW_REF: ${{ github.ref_name }} PARENT_WORKFLOW_SHA: ${{ github.sha }} CANDIDATE_ARTIFACT_JSON: ${{ needs.prepare_release_candidate.outputs.candidate_artifact_json }} PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }} run: *full_release_child_dispatch release_checks: name: Run release/live/Docker/QA validation needs: [resolve_target, evidence_reuse, prepare_release_candidate] if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && (needs.prepare_release_candidate.result == 'success' || needs.prepare_release_candidate.result == 'skipped') && contains(fromJSON('["all","install-smoke","cross-os","live-e2e","package","qa-parity","qa-live"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }} runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 15 outputs: run_id: ${{ steps.dispatch.outputs.run_id }} run_attempt: ${{ steps.dispatch.outputs.run_attempt }} url: ${{ steps.dispatch.outputs.url }} steps: - name: Dispatch release checks id: dispatch env: GH_TOKEN: ${{ github.token }} CHILD_WORKFLOW_KIND: release-checks TARGET_REF: ${{ inputs.ref }} TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} CHILD_WORKFLOW_REF: ${{ github.ref_name }} PARENT_WORKFLOW_SHA: ${{ github.sha }} PROVIDER: ${{ inputs.provider }} MODE: ${{ inputs.mode }} RELEASE_PROFILE: ${{ inputs.release_profile }} RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }} FAIL_FAST: ${{ inputs.fail_fast }} ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }} RERUN_GROUP: ${{ inputs.rerun_group }} LIVE_SUITE_FILTER: ${{ needs.resolve_target.outputs.live_suite_filter }} CROSS_OS_SUITE_FILTER: ${{ needs.resolve_target.outputs.cross_os_suite_filter }} RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} CANDIDATE_ARTIFACT_JSON: ${{ needs.prepare_release_candidate.outputs.candidate_artifact_json }} SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }} run: *full_release_child_dispatch 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('["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 outputs: run_id: ${{ steps.dispatch.outputs.run_id }} run_attempt: ${{ steps.dispatch.outputs.run_attempt }} url: ${{ steps.dispatch.outputs.url }} steps: - name: Dispatch npm Telegram E2E id: dispatch env: GH_TOKEN: ${{ github.token }} CHILD_WORKFLOW_KIND: npm-telegram CHILD_WORKFLOW_REF: ${{ github.ref_name }} PARENT_WORKFLOW_SHA: ${{ github.sha }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec || inputs.release_package_spec }} PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }} SCENARIO: ${{ inputs.npm_telegram_scenario }} run: *full_release_child_dispatch performance: name: Run product performance evidence needs: [resolve_target, evidence_reuse] if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["all","performance"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }} runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 15 outputs: run_id: ${{ steps.dispatch.outputs.run_id }} run_attempt: ${{ steps.dispatch.outputs.run_attempt }} url: ${{ steps.dispatch.outputs.url }} steps: - name: Dispatch OpenClaw Performance id: dispatch env: GH_TOKEN: ${{ github.token }} CHILD_WORKFLOW_KIND: performance RELEASE_PROFILE: ${{ inputs.release_profile }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} CHILD_WORKFLOW_REF: ${{ github.ref_name }} PARENT_WORKFLOW_SHA: ${{ github.sha }} run: *full_release_child_dispatch release_execution_plan: name: Seal release execution plan needs: [ resolve_target, evidence_reuse, docker_runtime_assets_preflight, prepare_release_candidate, normal_ci, plugin_prerelease, release_checks, npm_telegram, performance, ] if: always() runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 15 outputs: sha256: ${{ steps.plan.outputs.sha256 }} source_parent_attempt: ${{ steps.plan.outputs.source_parent_attempt }} steps: - name: Checkout release execution plan tooling uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} sparse-checkout: | scripts/full-release-validation-state.mjs scripts/full-release-validation-policy.mjs scripts/release-ci-summary.mjs scripts/lib/plain-gh.mjs sparse-checkout-cone-mode: false persist-credentials: false - name: Restore immutable release execution plan if: ${{ github.run_attempt != 1 }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: full-release-execution-plan-${{ github.run_id }} path: ${{ runner.temp }}/full-release-execution-plan - name: Seal immutable release execution plan id: plan env: GH_TOKEN: ${{ github.token }} RELEASE_PROFILE: ${{ inputs.release_profile }} RERUN_GROUP: ${{ inputs.rerun_group }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} EVIDENCE_REUSE: ${{ needs.evidence_reuse.outputs.reuse }} EVIDENCE_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_run_id }} EVIDENCE_ROOT_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_root_run_id }} EVIDENCE_RUN_URL: ${{ needs.evidence_reuse.outputs.evidence_run_url }} EVIDENCE_SHA: ${{ needs.evidence_reuse.outputs.evidence_sha }} EVIDENCE_POLICY: ${{ needs.evidence_reuse.outputs.evidence_policy }} EVIDENCE_CHANGED_PATHS: ${{ needs.evidence_reuse.outputs.changed_paths || '[]' }} TRUSTED_WORKFLOW_JSON: ${{ needs.resolve_target.outputs.trusted_workflow_json }} RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} LIVE_SUITE_FILTER: ${{ needs.resolve_target.outputs.live_suite_filter }} NORMAL_CI_RESULT: ${{ needs.normal_ci.result }} NORMAL_CI_RUN_ID: ${{ needs.normal_ci.outputs.run_id }} NORMAL_CI_RUN_ATTEMPT: ${{ needs.normal_ci.outputs.run_attempt }} NORMAL_CI_URL: ${{ needs.normal_ci.outputs.url }} PLUGIN_PRERELEASE_RESULT: ${{ needs.plugin_prerelease.result }} PLUGIN_PRERELEASE_RUN_ID: ${{ needs.plugin_prerelease.outputs.run_id }} PLUGIN_PRERELEASE_RUN_ATTEMPT: ${{ needs.plugin_prerelease.outputs.run_attempt }} PLUGIN_PRERELEASE_URL: ${{ needs.plugin_prerelease.outputs.url }} RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }} RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }} RELEASE_CHECKS_RUN_ATTEMPT: ${{ needs.release_checks.outputs.run_attempt }} RELEASE_CHECKS_URL: ${{ needs.release_checks.outputs.url }} NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }} NPM_TELEGRAM_RUN_ID: ${{ needs.npm_telegram.outputs.run_id }} NPM_TELEGRAM_RUN_ATTEMPT: ${{ needs.npm_telegram.outputs.run_attempt }} NPM_TELEGRAM_URL: ${{ needs.npm_telegram.outputs.url }} PERFORMANCE_RESULT: ${{ needs.performance.result }} PERFORMANCE_RUN_ID: ${{ needs.performance.outputs.run_id }} PERFORMANCE_RUN_ATTEMPT: ${{ needs.performance.outputs.run_attempt }} PERFORMANCE_URL: ${{ needs.performance.outputs.url }} RESOLVE_TARGET_RESULT: ${{ needs.resolve_target.result }} DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT: ${{ needs.docker_runtime_assets_preflight.result }} PREPARE_RELEASE_CANDIDATE_RESULT: ${{ needs.prepare_release_candidate.result }} FULL_RELEASE_RESTORE_PLAN: ${{ github.run_attempt != 1 }} FULL_RELEASE_EXECUTION_PLAN_PATH: ${{ runner.temp }}/full-release-execution-plan/full-release-execution-plan.json run: | set -euo pipefail if [[ "$FULL_RELEASE_RESTORE_PLAN" != "true" ]]; then export FULL_RELEASE_PLAN_INPUTS_JSON="$( jq -cn \ --arg parentRunId "$GITHUB_RUN_ID" \ --arg parentRunAttempt "$GITHUB_RUN_ATTEMPT" \ --arg workflowRef "$GITHUB_REF_NAME" \ --arg workflowSha "$GITHUB_SHA" \ --argjson trustedWorkflow "$TRUSTED_WORKFLOW_JSON" \ --arg evidenceReuse "$EVIDENCE_REUSE" \ --arg evidenceRunId "$EVIDENCE_RUN_ID" \ --arg evidenceRootRunId "$EVIDENCE_ROOT_RUN_ID" \ --arg evidenceRunUrl "$EVIDENCE_RUN_URL" \ --arg evidenceSha "$EVIDENCE_SHA" \ --arg evidencePolicy "$EVIDENCE_POLICY" \ --argjson evidenceChangedPaths "$EVIDENCE_CHANGED_PATHS" \ --arg rerunGroup "$RERUN_GROUP" \ --arg releasePackageSpec "$RELEASE_PACKAGE_SPEC" \ --arg packageAcceptancePackageSpec "$PACKAGE_ACCEPTANCE_PACKAGE_SPEC" \ --arg npmTelegramPackageSpec "$NPM_TELEGRAM_PACKAGE_SPEC" \ --arg liveSuiteFilter "$LIVE_SUITE_FILTER" \ --arg resolveTargetResult "$RESOLVE_TARGET_RESULT" \ --arg dockerPreflightResult "$DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT" \ --arg prepareCandidateResult "$PREPARE_RELEASE_CANDIDATE_RESULT" \ --arg normalCiResult "$NORMAL_CI_RESULT" \ --arg normalCiRunId "$NORMAL_CI_RUN_ID" \ --arg normalCiRunAttempt "$NORMAL_CI_RUN_ATTEMPT" \ --arg normalCiUrl "$NORMAL_CI_URL" \ --arg pluginPrereleaseResult "$PLUGIN_PRERELEASE_RESULT" \ --arg pluginPrereleaseRunId "$PLUGIN_PRERELEASE_RUN_ID" \ --arg pluginPrereleaseRunAttempt "$PLUGIN_PRERELEASE_RUN_ATTEMPT" \ --arg pluginPrereleaseUrl "$PLUGIN_PRERELEASE_URL" \ --arg releaseChecksResult "$RELEASE_CHECKS_RESULT" \ --arg releaseChecksRunId "$RELEASE_CHECKS_RUN_ID" \ --arg releaseChecksRunAttempt "$RELEASE_CHECKS_RUN_ATTEMPT" \ --arg releaseChecksUrl "$RELEASE_CHECKS_URL" \ --arg npmTelegramResult "$NPM_TELEGRAM_RESULT" \ --arg npmTelegramRunId "$NPM_TELEGRAM_RUN_ID" \ --arg npmTelegramRunAttempt "$NPM_TELEGRAM_RUN_ATTEMPT" \ --arg npmTelegramUrl "$NPM_TELEGRAM_URL" \ --arg performanceResult "$PERFORMANCE_RESULT" \ --arg performanceRunId "$PERFORMANCE_RUN_ID" \ --arg performanceRunAttempt "$PERFORMANCE_RUN_ATTEMPT" \ --arg performanceUrl "$PERFORMANCE_URL" \ '{ parentRunId: $parentRunId, parentRunAttempt: $parentRunAttempt, workflowRef: $workflowRef, workflowSha: $workflowSha, trustedWorkflow: $trustedWorkflow, evidenceReuse: $evidenceReuse, evidenceRunId: $evidenceRunId, evidenceRootRunId: $evidenceRootRunId, evidenceRunUrl: $evidenceRunUrl, evidenceSha: $evidenceSha, evidencePolicy: $evidencePolicy, evidenceChangedPaths: $evidenceChangedPaths, rerunGroup: $rerunGroup, releasePackageSpec: $releasePackageSpec, packageAcceptancePackageSpec: $packageAcceptancePackageSpec, npmTelegramPackageSpec: $npmTelegramPackageSpec, liveSuiteFilter: $liveSuiteFilter, resolveTargetResult: $resolveTargetResult, dockerPreflightResult: $dockerPreflightResult, prepareCandidateResult: $prepareCandidateResult, children: { normalCi: {result: $normalCiResult, runId: $normalCiRunId, runAttempt: $normalCiRunAttempt, url: $normalCiUrl}, pluginPrerelease: {result: $pluginPrereleaseResult, runId: $pluginPrereleaseRunId, runAttempt: $pluginPrereleaseRunAttempt, url: $pluginPrereleaseUrl}, releaseChecks: {result: $releaseChecksResult, runId: $releaseChecksRunId, runAttempt: $releaseChecksRunAttempt, url: $releaseChecksUrl}, npmTelegram: {result: $npmTelegramResult, runId: $npmTelegramRunId, runAttempt: $npmTelegramRunAttempt, url: $npmTelegramUrl}, productPerformance: {result: $performanceResult, runId: $performanceRunId, runAttempt: $performanceRunAttempt, url: $performanceUrl} } }' )" fi node scripts/full-release-validation-state.mjs plan - name: Upload immutable release execution plan if: ${{ always() && github.run_attempt == 1 }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: full-release-execution-plan-${{ github.run_id }} path: ${{ runner.temp }}/full-release-execution-plan if-no-files-found: error release_decision: name: Release Decision needs: [resolve_target, release_execution_plan] if: always() runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 720 outputs: state: ${{ steps.state.outputs.state }} steps: - name: Checkout release decision tooling uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} sparse-checkout: | scripts/full-release-validation-state.mjs scripts/full-release-validation-policy.mjs sparse-checkout-cone-mode: false persist-credentials: false - name: Download immutable release execution plan uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: full-release-execution-plan-${{ github.run_id }} path: ${{ runner.temp }}/full-release-execution-plan - name: Evaluate release decision id: state continue-on-error: true env: GH_TOKEN: ${{ github.token }} FAIL_FAST: ${{ inputs.fail_fast }} RELEASE_PROFILE: ${{ inputs.release_profile }} RERUN_GROUP: ${{ inputs.rerun_group }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} FULL_RELEASE_STATE_MODE: decision FULL_RELEASE_EXECUTION_PLAN_PATH: ${{ runner.temp }}/full-release-execution-plan/full-release-execution-plan.json FULL_RELEASE_STATE_PATH: ${{ runner.temp }}/full-release-decision/full-release-decision.json run: &full_release_state | set -euo pipefail node scripts/full-release-validation-state.mjs "$FULL_RELEASE_STATE_MODE" - name: Upload release decision if: always() && steps.state.outputs.state != '' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: full-release-decision-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/full-release-decision if-no-files-found: error - name: Enforce release decision if: always() env: RELEASE_DECISION_STATE: ${{ steps.state.outputs.state }} run: | set -euo pipefail if [[ "$RELEASE_DECISION_STATE" == "passed" ]]; then exit 0 fi echo "::error::Release Decision ended in ${RELEASE_DECISION_STATE:-orchestration_error}." exit 1 diagnostic_drain: name: Diagnostic Drain needs: [resolve_target, release_execution_plan] if: always() runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 720 outputs: state: ${{ steps.state.outputs.state }} normal_ci_conclusion: ${{ steps.state.outputs.normalCi_conclusion }} plugin_prerelease_conclusion: ${{ steps.state.outputs.pluginPrerelease_conclusion }} release_checks_conclusion: ${{ steps.state.outputs.releaseChecks_conclusion }} npm_telegram_conclusion: ${{ steps.state.outputs.npmTelegram_conclusion }} performance_conclusion: ${{ steps.state.outputs.productPerformance_conclusion }} steps: - name: Checkout diagnostic drain tooling uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} sparse-checkout: | scripts/full-release-validation-state.mjs scripts/full-release-validation-policy.mjs scripts/release-ci-summary.mjs scripts/lib/plain-gh.mjs sparse-checkout-cone-mode: false persist-credentials: false - name: Download immutable release execution plan uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: full-release-execution-plan-${{ github.run_id }} path: ${{ runner.temp }}/full-release-execution-plan - name: Drain child diagnostics id: state continue-on-error: true env: GH_TOKEN: ${{ github.token }} FAIL_FAST: "false" RELEASE_PROFILE: ${{ inputs.release_profile }} RERUN_GROUP: ${{ inputs.rerun_group }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} FULL_RELEASE_STATE_MODE: drain FULL_RELEASE_EXECUTION_PLAN_PATH: ${{ runner.temp }}/full-release-execution-plan/full-release-execution-plan.json FULL_RELEASE_STATE_PATH: ${{ runner.temp }}/full-release-diagnostics/full-release-diagnostic-manifest.json run: *full_release_state - name: Upload diagnostic drain manifest if: always() && steps.state.outputs.state != '' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: full-release-diagnostics-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/full-release-diagnostics if-no-files-found: error - name: Enforce diagnostic drain integrity if: always() env: DIAGNOSTIC_DRAIN_STATE: ${{ steps.state.outputs.state }} run: | set -euo pipefail case "$DIAGNOSTIC_DRAIN_STATE" in passed|blocked_complete) exit 0 ;; *) echo "::error::Diagnostic Drain ended in ${DIAGNOSTIC_DRAIN_STATE:-orchestration_error}." exit 1 ;; esac summary: name: Verify full validation needs: [ resolve_target, evidence_reuse, docker_runtime_assets_preflight, prepare_release_candidate, normal_ci, plugin_prerelease, release_checks, npm_telegram, performance, release_execution_plan, release_decision, diagnostic_drain, ] if: always() runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - name: Checkout release state verifier uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.sha }} sparse-checkout: | scripts/full-release-validation-state.mjs scripts/full-release-validation-policy.mjs scripts/release-ci-summary.mjs scripts/lib/plain-gh.mjs sparse-checkout-cone-mode: false persist-credentials: false - name: Download immutable release execution plan uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: full-release-execution-plan-${{ github.run_id }} path: ${{ runner.temp }}/full-release-execution-plan - name: Download release decision attempts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: full-release-decision-${{ github.run_id }}-* path: ${{ runner.temp }}/full-release-decision-attempts - name: Download diagnostic drain attempts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: full-release-diagnostics-${{ github.run_id }}-* path: ${{ runner.temp }}/full-release-diagnostic-attempts - name: Select newest compatible release state artifacts id: selected_state env: RELEASE_PROFILE: ${{ inputs.release_profile }} RERUN_GROUP: ${{ inputs.rerun_group }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} RELEASE_EXECUTION_PLAN_PATH: ${{ runner.temp }}/full-release-execution-plan/full-release-execution-plan.json RELEASE_DECISION_ATTEMPTS_PATH: ${{ runner.temp }}/full-release-decision-attempts DIAGNOSTIC_DRAIN_ATTEMPTS_PATH: ${{ runner.temp }}/full-release-diagnostic-attempts RELEASE_DECISION_PATH: ${{ runner.temp }}/full-release-decision/full-release-decision.json DIAGNOSTIC_DRAIN_PATH: ${{ runner.temp }}/full-release-diagnostics/full-release-diagnostic-manifest.json run: node scripts/full-release-validation-state.mjs select - name: Verify exact release state artifacts env: RELEASE_PROFILE: ${{ inputs.release_profile }} RERUN_GROUP: ${{ inputs.rerun_group }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} RELEASE_EXECUTION_PLAN_PATH: ${{ runner.temp }}/full-release-execution-plan/full-release-execution-plan.json RELEASE_DECISION_PATH: ${{ runner.temp }}/full-release-decision/full-release-decision.json DIAGNOSTIC_DRAIN_PATH: ${{ runner.temp }}/full-release-diagnostics/full-release-diagnostic-manifest.json run: node scripts/full-release-validation-state.mjs verify - name: Request release evidence update if: ${{ inputs.dispatch_release_evidence }} env: RELEASES_DISPATCH_TOKEN: ${{ secrets.OPENCLAW_RELEASES_DISPATCH_TOKEN }} TARGET_REF: ${{ inputs.ref }} PACKAGE_SPEC: ${{ inputs.evidence_package_spec || inputs.npm_telegram_package_spec }} GITHUB_RUN_ID_VALUE: ${{ github.run_id }} RELEASE_EXECUTION_PLAN_PATH: ${{ runner.temp }}/full-release-execution-plan/full-release-execution-plan.json run: | set -euo pipefail EVIDENCE_REUSE="$(jq -r '.evidenceReuse.requested' "$RELEASE_EXECUTION_PLAN_PATH")" EVIDENCE_ROOT_RUN_ID="$(jq -r '.evidenceReuse.rootRunId // ""' "$RELEASE_EXECUTION_PLAN_PATH")" EVIDENCE_POLICY="$(jq -r '.evidenceReuse.policy // ""' "$RELEASE_EXECUTION_PLAN_PATH")" RELEASE_CHECKS_SELECTED="$(jq -r '.children[] | select(.key == "releaseChecks") | .selected' "$RELEASE_EXECUTION_PLAN_PATH")" if [[ "$RELEASE_CHECKS_SELECTED" != "true" && "$EVIDENCE_REUSE" != "true" ]]; then echo "Release checks were skipped by rerun group; skipping automatic release evidence update." exit 0 fi notes="Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after exact Release Decision and Diagnostic Drain artifacts passed shared policy verification." if [[ "$EVIDENCE_REUSE" == "true" && -n "${EVIDENCE_ROOT_RUN_ID// }" ]]; then notes="Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE}, which reused green product evidence from chain-root run ${EVIDENCE_ROOT_RUN_ID} under policy ${EVIDENCE_POLICY}." fi if [[ -z "${RELEASES_DISPATCH_TOKEN// }" ]]; then echo "OPENCLAW_RELEASES_DISPATCH_TOKEN is not configured; skipping automatic release evidence update." exit 0 fi evidence_package_spec="$PACKAGE_SPEC" if [[ -z "${evidence_package_spec// }" ]]; then tag_ref="${TARGET_REF#refs/tags/}" if [[ "$tag_ref" =~ ^v([0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?)$ ]]; then evidence_package_spec="openclaw@${BASH_REMATCH[1]}" fi fi release_id="${TARGET_REF#refs/tags/}" release_id="${release_id#v}" if [[ "$evidence_package_spec" =~ ^openclaw@(.+)$ ]]; then release_id="${BASH_REMATCH[1]}" fi release_id="$(printf '%s' "$release_id" | tr '/:@ ' '----' | tr -cd 'A-Za-z0-9._-')" if [[ -z "$release_id" ]]; then echo "::warning::Could not derive release evidence id from target ref '${TARGET_REF}'; skipping automatic release evidence update." exit 0 fi payload="$( jq -cn \ --arg full_validation_run_id "$GITHUB_RUN_ID_VALUE" \ --arg release_id "$release_id" \ --arg release_ref "$TARGET_REF" \ --arg package_spec "$evidence_package_spec" \ --arg notes "$notes" \ '{ event_type: "openclaw_full_release_validation_completed", client_payload: { full_validation_run_id: $full_validation_run_id, release_id: $release_id, release_ref: $release_ref, package_spec: $package_spec, notes: $notes } }' )" if ! curl --fail-with-body \ --connect-timeout 10 \ --max-time 30 \ -X POST \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${RELEASES_DISPATCH_TOKEN}" \ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/openclaw/releases/dispatches \ -d "$payload"; then echo "::warning::Automatic release evidence dispatch failed; child workflow validation remains authoritative." { echo "### Release evidence dispatch failed" echo echo "Child workflow validation remains authoritative. Backfill durable evidence from \`openclaw/releases\`:" echo echo "\`\`\`bash" echo "gh workflow run openclaw-release-evidence-from-full-validation.yml --repo openclaw/releases --ref main -f full_validation_run_id=${GITHUB_RUN_ID_VALUE} -f release_id=${release_id} -f release_ref=${TARGET_REF} -f package_spec=${evidence_package_spec}" echo "\`\`\`" } >> "$GITHUB_STEP_SUMMARY" fi - name: Write release validation manifest if: ${{ success() }} env: TARGET_REF: ${{ startsWith(github.ref, 'refs/heads/release-ci/') && needs.resolve_target.outputs.sha || inputs.ref }} RELEASE_PROFILE: ${{ inputs.release_profile }} RERUN_GROUP: ${{ inputs.rerun_group }} RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }} PROVIDER: ${{ inputs.provider }} MODE: ${{ inputs.mode }} TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }} LIVE_SUITE_FILTER: ${{ needs.resolve_target.outputs.live_suite_filter }} CROSS_OS_SUITE_FILTER: ${{ needs.resolve_target.outputs.cross_os_suite_filter }} RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }} CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }} NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }} NPM_TELEGRAM_SCENARIO: ${{ inputs.npm_telegram_scenario }} SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }} ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }} PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }} RELEASE_EXECUTION_PLAN_PATH: ${{ runner.temp }}/full-release-execution-plan/full-release-execution-plan.json DIAGNOSTIC_DRAIN_PATH: ${{ runner.temp }}/full-release-diagnostics/full-release-diagnostic-manifest.json run: | set -euo pipefail manifest_dir="${RUNNER_TEMP}/full-release-validation" mkdir -p "$manifest_dir" TARGET_SHA="$(jq -r '.targetSha' "$RELEASE_EXECUTION_PLAN_PATH")" NORMAL_CI_RUN_ID="$(jq -r '.children[] | select(.key == "normalCi") | .runId' "$RELEASE_EXECUTION_PLAN_PATH")" PLUGIN_PRERELEASE_RUN_ID="$(jq -r '.children[] | select(.key == "pluginPrerelease") | .runId' "$RELEASE_EXECUTION_PLAN_PATH")" RELEASE_CHECKS_RUN_ID="$(jq -r '.children[] | select(.key == "releaseChecks") | .runId' "$RELEASE_EXECUTION_PLAN_PATH")" NPM_TELEGRAM_RUN_ID="$(jq -r '.children[] | select(.key == "npmTelegram") | .runId' "$RELEASE_EXECUTION_PLAN_PATH")" PERFORMANCE_RUN_ID="$(jq -r '.children[] | select(.key == "productPerformance") | .runId' "$RELEASE_EXECUTION_PLAN_PATH")" EXECUTION_PLAN_SHA256="$(jq -r '.sha256' "$RELEASE_EXECUTION_PLAN_PATH")" SOURCE_PARENT_RUN_ATTEMPT="$(jq -r '.parentRunAttempt' "$RELEASE_EXECUTION_PLAN_PATH")" EVIDENCE_REUSE="$(jq -r '.evidenceReuse.requested' "$RELEASE_EXECUTION_PLAN_PATH")" EVIDENCE_RUN_ID="$(jq -r '.evidenceReuse.selectedRunId // ""' "$RELEASE_EXECUTION_PLAN_PATH")" EVIDENCE_ROOT_RUN_ID="$(jq -r '.evidenceReuse.rootRunId // ""' "$RELEASE_EXECUTION_PLAN_PATH")" EVIDENCE_SHA="$(jq -r '.evidenceReuse.evidenceSha // ""' "$RELEASE_EXECUTION_PLAN_PATH")" EVIDENCE_POLICY="$(jq -r '.evidenceReuse.policy // ""' "$RELEASE_EXECUTION_PLAN_PATH")" EVIDENCE_CHANGED_PATHS="$(jq -c '.evidenceReuse.changedPaths // []' "$RELEASE_EXECUTION_PLAN_PATH")" EVIDENCE_MANIFEST="$(jq -c '.evidenceReuse.sourceManifest // empty' "$RELEASE_EXECUTION_PLAN_PATH")" PERFORMANCE_CONCLUSION="$(jq -r '.children.productPerformance.conclusion // ""' "$DIAGNOSTIC_DRAIN_PATH")" if [[ "$EVIDENCE_REUSE" == "true" ]]; then # Inherit the evidence manifest (profile, soak, child runs) so future # reuse lookups and evidence consumers keep resolving the chain root. jq \ --arg runId "$GITHUB_RUN_ID" \ --arg runAttempt "$GITHUB_RUN_ATTEMPT" \ --arg workflowRef "$GITHUB_REF_NAME" \ --arg workflowSha "$GITHUB_SHA" \ --arg workflowFullRef "$GITHUB_REF" \ --arg workflowRefType "$GITHUB_REF_TYPE" \ --arg targetRef "$TARGET_REF" \ --arg targetSha "$TARGET_SHA" \ --arg evidenceRunId "$EVIDENCE_RUN_ID" \ --arg evidenceRootRunId "$EVIDENCE_ROOT_RUN_ID" \ --arg evidenceSha "$EVIDENCE_SHA" \ --arg evidencePolicy "$EVIDENCE_POLICY" \ --arg executionPlanSha256 "$EXECUTION_PLAN_SHA256" \ --arg sourceParentRunAttempt "$SOURCE_PARENT_RUN_ATTEMPT" \ --argjson evidenceChangedPaths "$EVIDENCE_CHANGED_PATHS" \ '. + { version: 3, runId: $runId, runAttempt: $runAttempt, workflowRef: $workflowRef, workflowSha: $workflowSha, workflowFullRef: $workflowFullRef, workflowRefType: $workflowRefType, targetRef: $targetRef, targetSha: $targetSha, evidenceReuse: { policy: $evidencePolicy, runId: $evidenceRootRunId, selectedRunId: $evidenceRunId, evidenceSha: $evidenceSha, changedPaths: $evidenceChangedPaths }, controls: ((.controls // {}) + { performanceReportPublication: "artifact-only" }), executionPlanSha256: $executionPlanSha256, sourceParentRunAttempt: ($sourceParentRunAttempt | tonumber) }' <<< "$EVIDENCE_MANIFEST" > "${manifest_dir}/full-release-validation-manifest.json" exit 0 fi jq -n \ --arg workflowName "Full Release Validation" \ --arg runId "$GITHUB_RUN_ID" \ --arg runAttempt "$GITHUB_RUN_ATTEMPT" \ --arg workflowRef "$GITHUB_REF_NAME" \ --arg workflowSha "$GITHUB_SHA" \ --arg workflowFullRef "$GITHUB_REF" \ --arg workflowRefType "$GITHUB_REF_TYPE" \ --arg targetRef "$TARGET_REF" \ --arg targetSha "$TARGET_SHA" \ --arg releaseProfile "$RELEASE_PROFILE" \ --arg rerunGroup "$RERUN_GROUP" \ --arg runReleaseSoak "$RUN_RELEASE_SOAK" \ --arg normalCiRunId "$NORMAL_CI_RUN_ID" \ --arg pluginPrereleaseRunId "$PLUGIN_PRERELEASE_RUN_ID" \ --arg releaseChecksRunId "$RELEASE_CHECKS_RUN_ID" \ --arg npmTelegramRunId "$NPM_TELEGRAM_RUN_ID" \ --arg performanceRunId "$PERFORMANCE_RUN_ID" \ --arg performanceConclusion "$PERFORMANCE_CONCLUSION" \ --arg executionPlanSha256 "$EXECUTION_PLAN_SHA256" \ --arg sourceParentRunAttempt "$SOURCE_PARENT_RUN_ATTEMPT" \ --arg provider "$PROVIDER" \ --arg mode "$MODE" \ --arg targetContextRef "$TARGET_CONTEXT_REF" \ --arg liveSuiteFilter "$LIVE_SUITE_FILTER" \ --arg crossOsSuiteFilter "$CROSS_OS_SUITE_FILTER" \ --arg releasePackageSpec "$RELEASE_PACKAGE_SPEC" \ --arg packageAcceptancePackageSpec "$PACKAGE_ACCEPTANCE_PACKAGE_SPEC" \ --arg codexPluginSpec "$CODEX_PLUGIN_SPEC" \ --arg npmTelegramPackageSpec "$NPM_TELEGRAM_PACKAGE_SPEC" \ --arg npmTelegramProviderMode "$NPM_TELEGRAM_PROVIDER_MODE" \ --arg npmTelegramScenario "$NPM_TELEGRAM_SCENARIO" \ --arg skipPackageTelegramE2e "$SKIP_PACKAGE_TELEGRAM_E2E" \ --arg allowUnreleasedChangelog "$ALLOW_UNRELEASED_CHANGELOG" \ --arg pluginPrereleaseNodeExcludePatternsJson "$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON" \ '{ version: 3, workflowName: $workflowName, runId: $runId, runAttempt: $runAttempt, workflowRef: $workflowRef, workflowSha: $workflowSha, workflowFullRef: $workflowFullRef, workflowRefType: $workflowRefType, targetRef: $targetRef, targetSha: $targetSha, releaseProfile: $releaseProfile, rerunGroup: $rerunGroup, runReleaseSoak: $runReleaseSoak, executionPlanSha256: $executionPlanSha256, sourceParentRunAttempt: ($sourceParentRunAttempt | tonumber), validationInputs: { provider: $provider, mode: $mode, targetContextRef: $targetContextRef, liveSuiteFilter: $liveSuiteFilter, crossOsSuiteFilter: $crossOsSuiteFilter, releasePackageSpec: $releasePackageSpec, packageAcceptancePackageSpec: $packageAcceptancePackageSpec, codexPluginSpec: $codexPluginSpec, npmTelegramPackageSpec: $npmTelegramPackageSpec, npmTelegramProviderMode: $npmTelegramProviderMode, npmTelegramScenario: $npmTelegramScenario, skipPackageTelegramE2e: $skipPackageTelegramE2e, allowUnreleasedChangelog: $allowUnreleasedChangelog, pluginPrereleaseNodeExcludePatternsJson: $pluginPrereleaseNodeExcludePatternsJson }, controls: { stableSoakRequired: ($releaseProfile == "stable" or $releaseProfile == "full"), performanceBlocking: ($releaseProfile != "beta"), performanceReportPublication: "artifact-only" }, childRuns: { normalCi: $normalCiRunId, pluginPrerelease: $pluginPrereleaseRunId, releaseChecks: $releaseChecksRunId, npmTelegram: $npmTelegramRunId, productPerformance: { runId: $performanceRunId, conclusion: $performanceConclusion, blocking: ($releaseProfile != "beta") } } }' > "${manifest_dir}/full-release-validation-manifest.json" - name: Validate release validation manifest env: RELEASE_PROFILE: ${{ inputs.release_profile }} RERUN_GROUP: ${{ inputs.rerun_group }} TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} RELEASE_EXECUTION_PLAN_PATH: ${{ runner.temp }}/full-release-execution-plan/full-release-execution-plan.json RELEASE_VALIDATION_MANIFEST_PATH: ${{ runner.temp }}/full-release-validation/full-release-validation-manifest.json run: node scripts/full-release-validation-state.mjs validate-manifest - name: Upload release validation manifest if: ${{ success() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: full-release-validation-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/full-release-validation if-no-files-found: error - name: Upload legacy release validation manifest alias if: ${{ success() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: full-release-validation-${{ github.run_id }} path: ${{ runner.temp }}/full-release-validation if-no-files-found: error overwrite: true