Files
openclaw/.github/workflows/full-release-validation.yml
Peter Steinberger 234df15a6d chore: refresh dependencies after seven-day cooldown (#128414)
* build(deps): refresh dependencies after cooldown

Apply dependency, toolchain, action, image, and exact tool updates released by the inclusive 2026-08-16 seven-day cutoff. Adapt owner boundaries for the resulting CUA, logging, Teams, Markdown, native, and test-harness contract changes while retaining versions blocked by upstream compatibility constraints.

* fix(ui): align markdown renderer env typing

* fix(deps): align postcss and mistral peer contracts

* fix(deps): repair refreshed dependency contracts

* fix(deps): retain tslog startup budget

* fix(ci): verify Android tools with SHA-256

* fix(ci): fence Android SDK cache version
2026-08-24 03:01:54 -07:00

1726 lines
86 KiB
YAML

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.19.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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
path: workflow
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Checkout target SHA
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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
# Parent reruns hide prior-attempt artifacts. This exact-key cache is immutable;
# a miss fails closed instead of reconstructing or redispatching child identities.
- name: Cache immutable release execution plan
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ${{ runner.temp }}/full-release-execution-plan
key: full-release-execution-plan-v1-${{ github.run_id }}
fail-on-cache-miss: ${{ github.run_attempt != 1 }}
- 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()
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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