openclaw/.github/workflows/full-release-validation.yml

2619 lines
143 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: Source envelope {trustedWorkflow, validationPurpose, publicationSelection, optional laneInputs}; null identity permits existing direct-route inference, never intent inference
required: true
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
telegram_waiver:
description: Reviewed stable/full waiver; 2026.8.1/9.1 cover Telegram, 2026.9.5/9.7/9.8 cover Telegram and Matrix QA-live
required: false
default: ""
type: string
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 and permits same-parent recovery
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 cross-OS selection for all or cross-os runs, e.g. ubuntu,macos or windows/packaged-upgrade
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:
# Profiles own different coverage and verdicts, so their admission is independent.
# Stable/full force soak, so their explicit soak flag must not split the lock.
group: full-release-validation-${{ inputs.expected_sha || inputs.ref }}-${{ github.sha }}-${{ inputs.rerun_group }}-${{ inputs.release_profile == 'minimum' && 'beta' || inputs.release_profile }}-${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}
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.21.0"
RELEASE_ISOLATION_TOOLING_CONTRACT: "2"
FULL_RELEASE_DISPATCH_WITNESS_CONTRACT: "1"
FULL_RELEASE_SOURCE_ADMISSION_CONTRACT: "1"
FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT: "1"
FULL_RELEASE_EXECUTION_PLAN_RESTORE_CONTRACT: "1"
FULL_RELEASE_LANE_INPUTS_CONTRACT: "1"
CHILD_EVIDENCE_REUSE: ${{ inputs.reuse_evidence && inputs.rerun_group == 'all' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release-ci/')) }}
jobs:
resolve_target:
name: Resolve target ref
# Hosted admission delays compound along this serial release path.
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(github.repository == 'openclaw/openclaw' && vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04'))) || (github.repository == 'openclaw/openclaw' && vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 10
outputs:
sha: ${{ steps.resolve.outputs.sha }}
release_tag: ${{ steps.release_inputs.outputs.release_tag }}
npm_dist_tag: ${{ steps.release_inputs.outputs.npm_dist_tag }}
release_candidate_branch: ${{ steps.release_inputs.outputs.release_candidate_branch }}
trusted_workflow_json: ${{ steps.tooling_identity.outputs.json }}
extension_test_exclude_patterns_json: ${{ steps.publication_dispatch.outputs.extension_test_exclude_patterns_json }}
live_suite_filter: ${{ steps.filters.outputs.live_suite_filter }}
cross_os_suite_filter: ${{ steps.filters.outputs.cross_os_suite_filter }}
candidate_required: ${{ steps.candidate_request.outputs.required }}
target_version: ${{ steps.release_inputs.outputs.target_version }}
coverage_policy: ${{ steps.release_inputs.outputs.coverage_policy }}
ci_release_scope: ${{ steps.release_inputs.outputs.ci_release_scope }}
skip_package_telegram_e2e: ${{ steps.release_inputs.outputs.skip_package_telegram_e2e }}
plugin_candidate_required: ${{ steps.candidate_request.outputs.plugin_required }}
release_candidate_artifact_required: ${{ steps.candidate_request.outputs.release_artifact_required }}
candidate_request_json: ${{ steps.candidate_request.outputs.request_json }}
candidate_request_sha256: ${{ steps.candidate_request.outputs.request_sha256 }}
source_admission_json: ${{ steps.publication_admission.outputs.json }}
validation_purpose: ${{ steps.publication_request.outputs.validation_purpose }}
publication_selection_json: ${{ steps.publication_request.outputs.publication_selection_json }}
plugin_compatibility_required: ${{ steps.plugin_compatibility.outputs.required }}
steps:
- name: Setup supported Node runtime
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Retain root dispatch inputs
id: dispatch_witness
env:
DISPATCH_SERVER_URL: ${{ github.server_url }}
DISPATCH_WORKFLOW_REF: ${{ github.workflow_ref }}
run: |
node <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const { createHash } = require("node:crypto");
const env = process.env;
const descriptor = fs.openSync(env.GITHUB_EVENT_PATH, "r");
let event;
try {
const size = fs.fstatSync(descriptor).size;
if (size > 1024 * 1024) throw new Error("Event exceeds its byte limit");
const bytes = Buffer.alloc(size + 1);
const length = fs.readSync(descriptor, bytes, 0, bytes.length, 0);
if (length !== size) throw new Error("Event changed while reading");
event = JSON.parse(bytes.subarray(0, length).toString("utf8"));
} catch {
throw new Error("Invalid or oversized root dispatch event");
} finally {
fs.closeSync(descriptor);
}
const inputs = event?.inputs;
if (!inputs || typeof inputs !== "object" || Array.isArray(inputs) ||
Object.values(inputs).some((value) => !["string", "boolean", "number"].includes(typeof value) ||
(typeof value === "number" && !Number.isFinite(value)))) {
throw new Error("Invalid root dispatch inputs");
}
// The immutable workflow binds input types; event booleans use wire strings.
const wireInputs = Object.fromEntries(
Object.keys(inputs).sort().map((key) => [key, String(inputs[key])]),
);
const canonicalInputs = JSON.stringify(wireInputs);
if (Buffer.byteLength(canonicalInputs) > 128 * 1024) {
throw new Error("Root dispatch inputs exceed their byte limit");
}
const witness = {
kind: "openclaw.full-release-dispatch-inputs/v1",
serverUrl: env.DISPATCH_SERVER_URL,
repository: env.GITHUB_REPOSITORY,
workflowRef: env.DISPATCH_WORKFLOW_REF,
event: env.GITHUB_EVENT_NAME,
ref: env.GITHUB_REF,
sha: env.GITHUB_SHA,
runId: env.GITHUB_RUN_ID,
runAttempt: env.GITHUB_RUN_ATTEMPT,
inputsDigest: `sha256:${createHash("sha256").update(canonicalInputs).digest("hex")}`,
};
const bytes = JSON.stringify(witness) + "\n";
if (Buffer.byteLength(bytes) > 128 * 1024) {
throw new Error("Root dispatch witness exceeds its byte limit");
}
const directory = path.join(env.RUNNER_TEMP, "full-release-dispatch-inputs");
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
fs.writeFileSync(path.join(directory, "dispatch-inputs.json"), bytes, { mode: 0o600 });
NODE
- name: Upload root dispatch inputs
if: ${{ steps.dispatch_witness.outcome == 'success' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-release-dispatch-inputs-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/full-release-dispatch-inputs/dispatch-inputs.json
if-no-files-found: error
retention-days: 7
- 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: Setup admission Node.js
env:
REQUESTED_NODE_VERSION: "24.x"
run: |
set -euo pipefail
source workflow/.github/actions/setup-pnpm-store-cache/ensure-node.sh
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
- name: Decode publication dispatch envelope
id: publication_dispatch
env:
PUBLICATION_INPUTS_JSON: ${{ toJSON(inputs) }}
run: |
set -euo pipefail
node workflow/scripts/full-release-publication-contract.mjs --dispatch
- name: Resolve trusted workflow identity
id: tooling_identity
env:
GH_TOKEN: ${{ github.token }}
REQUESTED_IDENTITY_JSON: ${{ steps.publication_dispatch.outputs.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"
printf 'repo_live_suite_filter=%s\n' "$RELEASE_FILTER_REPO_LIVE_SUITE_FILTER"
printf 'qa_filter_seen=%s\n' "$RELEASE_FILTER_QA_FILTER_SEEN"
} >> "$GITHUB_OUTPUT"
- name: Build canonical release candidate request
id: candidate_request
env:
TARGET_SHA: ${{ steps.resolve.outputs.sha }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}
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')) }}
PACKAGE_PUBLISHED: ${{ inputs.release_package_spec != '' }}
RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }}
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
RERUN_GROUP: ${{ inputs.rerun_group }}
LIVE_SUITE_FILTER: ${{ steps.filters.outputs.live_suite_filter }}
run: |
set -euo pipefail
input="${RUNNER_TEMP}/full-release-candidate-request-input.json"
output="${RUNNER_TEMP}/full-release-candidate-request.json"
jq -cn \
--arg repository "$GITHUB_REPOSITORY" \
--arg targetSha "$TARGET_SHA" \
--arg toolingSha "$GITHUB_SHA" \
--arg releaseProfile "$RELEASE_PROFILE" \
--argjson releaseSoak "$RELEASE_SOAK" \
--arg upgradeSurvivorBaseline "openclaw@latest" \
--arg upgradeSurvivorBaselines "" \
--arg upgradeSurvivorScenarios "$UPGRADE_SURVIVOR_SCENARIOS" \
--argjson allowFrozenTargetScenarioOmissions "$ALLOW_FROZEN_TARGET_SCENARIO_OMISSIONS" \
--argjson allowUnreleasedChangelog "$ALLOW_UNRELEASED_CHANGELOG" \
--argjson packagePublished "$PACKAGE_PUBLISHED" \
--arg sharedImagePolicy "no-push-artifact" \
'$ARGS.named' > "$input"
result="$(
node workflow/scripts/full-release-candidate-contract.mjs request \
--input "$input" \
--output "$output"
)"
plugin_required=false
[[ "$RERUN_GROUP" =~ ^(all|plugin-prerelease)$ ]] && plugin_required=true
release_artifact_required=false
if [[ "$RERUN_GROUP" =~ ^(all|cross-os)$ &&
-z "${RELEASE_PACKAGE_SPEC// }" ]]; then
release_artifact_required=true
elif [[ "$RERUN_GROUP" =~ ^(all|package)$ &&
-z "${RELEASE_PACKAGE_SPEC// }" &&
-z "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
release_artifact_required=true
elif [[ "$RERUN_GROUP" == "live-e2e" &&
-z "${LIVE_SUITE_FILTER// }" &&
-z "${RELEASE_PACKAGE_SPEC// }" ]]; then
release_artifact_required=true
fi
required=false
if [[ "$plugin_required" == "true" || "$release_artifact_required" == "true" ]]; then
required=true
fi
{
printf 'required=%s\n' "$required"
printf 'plugin_required=%s\n' "$plugin_required"
printf 'release_artifact_required=%s\n' "$release_artifact_required"
printf 'request_json=%s\n' "$(jq -c . "$output")"
printf 'request_sha256=%s\n' \
"$(printf '%s\n' "$result" | jq -er '.requestSha256')"
} >> "$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: Detect target plugin compatibility gate
id: plugin_compatibility
env:
GH_TOKEN: ${{ github.token }}
TARGET_SHA: ${{ steps.resolve.outputs.sha }}
run: |
set -euo pipefail
if jq -e '.scripts["plugins:boundary-report:ci"] | type == "string"' target/package.json >/dev/null; then
echo 'required=true' >> "$GITHUB_OUTPUT"
exit 0
fi
gate_introduction=38ba27834dd3f98c19d5833e0598dfef3abb7587
relationship="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${gate_introduction}...${TARGET_SHA}" --jq .status)"
if [[ "$relationship" == "ahead" || "$relationship" == "identical" ]]; then
echo 'Current target is missing plugins:boundary-report:ci.' >&2
exit 1
fi
echo 'required=false' >> "$GITHUB_OUTPUT"
echo '::warning::Frozen target predates the plugin compatibility release gate; skipping target-owned compatibility readiness.'
- name: Validate release inputs
id: release_inputs
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}
SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }}
TELEGRAM_WAIVER: ${{ inputs.telegram_waiver }}
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 }}
RERUN_GROUP: ${{ inputs.rerun_group }}
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
CROSS_OS_SUITE_FILTER: ${{ steps.filters.outputs.cross_os_suite_filter }}
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)"
if [[ "$target_version" == *-alpha.* || "$TARGET_REF" == *-alpha.* || "$context_ref" == *-alpha.* || "$TARGET_REF" == *tideclaw/alpha/* || "$context_ref" == tideclaw/alpha/* || "${GITHUB_REF:-}" == *tideclaw/alpha/* ]]; then
echo "Alpha releases are retired; use a beta prerelease." >&2
exit 1
fi
if [[ -n "${TELEGRAM_WAIVER:-}" ]]; then
waived_channels="$(
TARGET_VERSION="$target_version" node --input-type=module <<'NODE'
import { releaseWaivedIntegrationChannels } from './workflow/scripts/full-release-validation-policy.mjs';
console.log(JSON.stringify(releaseWaivedIntegrationChannels({
telegramWaiver: process.env.TELEGRAM_WAIVER,
targetVersion: process.env.TARGET_VERSION,
releaseProfile: process.env.RELEASE_PROFILE,
rerunGroup: process.env.RERUN_GROUP,
liveSuiteFilter: process.env.LIVE_SUITE_FILTER,
releasePackageSpec: process.env.RELEASE_PACKAGE_SPEC,
packageAcceptancePackageSpec: process.env.PACKAGE_ACCEPTANCE_PACKAGE_SPEC,
npmTelegramPackageSpec: process.env.NPM_TELEGRAM_PACKAGE_SPEC,
})));
NODE
)"
printf 'waived_integration_channels=%s\n' "$waived_channels" >> "$GITHUB_OUTPUT"
fi
release_context="$(TARGET_VERSION="$target_version" node --input-type=module <<'NODE'
import { resolveReleaseContextIdentity } from './workflow/scripts/lib/release-context.mjs';
const env = process.env;
const identity = resolveReleaseContextIdentity(env.TARGET_CONTEXT_REF || env.TARGET_REF, env.TARGET_VERSION);
if (!identity && env.TARGET_CONTEXT_REF) throw new Error('target_context_ref must be a canonical OpenClaw release branch or tag.');
console.log(JSON.stringify(identity ?? { kind: '', releaseTag: `v${env.TARGET_VERSION}`, baseTag: null }));
NODE
)"
identity_kind="$(jq -r '.kind' <<< "$release_context")"
release_tag="$(jq -r '.releaseTag' <<< "$release_context")"
base_tag="$(jq -r '.baseTag // empty' <<< "$release_context")"
# The API uses this step's token and peels tags without persisting Git credentials.
resolve_release_ref() {
local encoded_ref
encoded_ref="$(jq -rn --arg value "$1" '$value | @uri')"
gh api "repos/${GITHUB_REPOSITORY}/commits/${encoded_ref}" --jq .sha
}
if [[ -n "$base_tag" ]]; then
base_sha="$(resolve_release_ref "refs/tags/${base_tag}")"
if [[ "$base_sha" != "$TARGET_SHA" ]]; then
echo "Correction source ${TARGET_SHA} must match base release tag ${base_tag}." >&2
exit 1
fi
fi
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
printf 'target_version=%s\nrelease_tag=%s\n' "$target_version" "$release_tag" >> "$GITHUB_OUTPUT"
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
if [[ "$identity_kind" == "release tag" ]]; then
remote_sha="$(resolve_release_ref "refs/tags/${context_ref}")"
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="$(resolve_release_ref "refs/heads/${context_ref}")"
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
TARGET_VERSION="$target_version" RELEASE_CONTEXT_KIND="$identity_kind" node --input-type=module <<'NODE'
import { appendFileSync } from 'node:fs';
import { normalizeReleaseCoveragePolicy } from './workflow/scripts/full-release-validation-policy.mjs';
import { classifyReleaseTrain, parseReleaseVersion } from './workflow/scripts/lib/release-version.mjs';
const env = process.env;
const version = parseReleaseVersion(env.TARGET_VERSION);
if (!version) throw new Error('Invalid release package version.');
const train = classifyReleaseTrain(version);
if (train === 'unsupported-extended-stable-correction') {
throw new Error('Extended-stable releases do not allow correction suffixes.');
}
if (train === 'alpha') throw new Error('Alpha releases are retired; use a beta prerelease.');
const distTag = train === 'stable' ? 'beta' : train;
const branch = train === 'extended-stable' ? `extended-stable/${version.year}.${version.month}.33` : '';
const beta = env.RELEASE_PROFILE === 'beta' && env.RUN_RELEASE_SOAK === 'false' && env.RERUN_GROUP === 'all';
const stable = env.RELEASE_PROFILE === 'stable' && env.RUN_RELEASE_SOAK === 'true' && env.RERUN_GROUP === 'all';
const releaseContext = ['release branch', 'release tag'].includes(env.RELEASE_CONTEXT_KIND);
// Bind qualification scope once; dispatch and sealed evidence consume the same fact.
const policy = releaseContext && beta && train === 'beta' ? 'npm-beta-v1'
: releaseContext && stable && train === 'stable' ? 'npm-stable-v1' : undefined;
normalizeReleaseCoveragePolicy({ coveragePolicy: policy, releaseProfile: env.RELEASE_PROFILE,
rerunGroup: env.RERUN_GROUP, runReleaseSoak: env.RUN_RELEASE_SOAK, targetVersion: version.version,
crossOsSuiteFilter: env.CROSS_OS_SUITE_FILTER });
const scope = policy === 'npm-beta-v1' ? 'npm-beta' : policy === 'npm-stable-v1' ? 'npm-stable' : 'full';
appendFileSync(env.GITHUB_OUTPUT, `npm_dist_tag=${distTag}\nrelease_candidate_branch=${branch}\ncoverage_policy=${policy ?? ''}\nci_release_scope=${scope}\nskip_package_telegram_e2e=${beta ? 'true' : env.SKIP_PACKAGE_TELEGRAM_E2E}\n`);
NODE
- name: Validate publication source request
id: publication_request
env:
PUBLICATION_INPUTS_JSON: ${{ toJSON(inputs) }}
PUBLICATION_TOOLING_JSON: ${{ steps.tooling_identity.outputs.json }}
PUBLICATION_TARGET_SHA: ${{ steps.resolve.outputs.sha }}
PUBLICATION_TARGET_CONTEXT: ${{ inputs.target_context_ref }}
PUBLICATION_COVERAGE_POLICY: ${{ steps.release_inputs.outputs.coverage_policy }}
PUBLICATION_LIVE_FILTER: ${{ steps.filters.outputs.live_suite_filter }}
PUBLICATION_CROSS_OS_FILTER: ${{ steps.filters.outputs.cross_os_suite_filter }}
PUBLICATION_SKIP_TELEGRAM: ${{ steps.release_inputs.outputs.skip_package_telegram_e2e }}
run: |
set -euo pipefail
umask 077
node workflow/scripts/full-release-publication-contract.mjs --request > "$RUNNER_TEMP/publication-source-request.json"
- name: Plan frozen source admission
id: frozen_selection
env:
ADMISSION_WORKFLOW: parent
ADMISSION_INPUTS: ${{ toJSON(inputs) }}
ADMISSION_SELECTED_ROOT: ${{ github.workspace }}/target
ADMISSION_SELECTED_SHA: ${{ steps.resolve.outputs.sha }}
ADMISSION_TOOLING_ROOT: ${{ github.workspace }}/workflow
ADMISSION_TOOLING_SHA: ${{ github.sha }}
ADMISSION_WORKFLOW_REF: ${{ github.workflow_ref }}
ADMISSION_REPO_LIVE_SUITE_FILTER: ${{ steps.filters.outputs.repo_live_suite_filter }}
ADMISSION_QA_FILTER_SEEN: ${{ steps.filters.outputs.qa_filter_seen }}
ADMISSION_COVERAGE_POLICY: ${{ steps.release_inputs.outputs.coverage_policy }}
ADMISSION_CANDIDATE_REQUEST_DIGEST: ${{ steps.candidate_request.outputs.request_sha256 }}
run: |
set -euo pipefail
umask 077
node "$ADMISSION_TOOLING_ROOT/scripts/preflight-frozen-target-contracts.mjs" --workflow-request > "$RUNNER_TEMP/frozen-admission-request.json"
node "$ADMISSION_TOOLING_ROOT/scripts/preflight-frozen-target-contracts.mjs" --plan "$RUNNER_TEMP/frozen-admission-request.json" > "$RUNNER_TEMP/frozen-admission-selection.json"
printf 'parser_required=%s\n' "$(jq -er '.parserRequired | tostring' "$RUNNER_TEMP/frozen-admission-selection.json")" >> "$GITHUB_OUTPUT"
- name: Setup trusted admission package manager
if: steps.frozen_selection.outputs.parser_required == 'true' || steps.publication_request.outputs.required == 'true'
uses: ./workflow/.github/actions/setup-pnpm-store-cache
with:
package-manager-file: workflow/package.json
lockfile-path: workflow/pnpm-lock.yaml
node-version: ${{ env.NODE_VERSION }}
cache-mode: off
- name: Provision trusted admission parser
if: steps.frozen_selection.outputs.parser_required == 'true' || steps.publication_request.outputs.required == 'true'
working-directory: workflow
env:
CI: "true"
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: Acquire selected contract objects
env:
ADMISSION_SELECTED_ROOT: ${{ github.workspace }}/target
ADMISSION_SELECTED_SHA: ${{ steps.resolve.outputs.sha }}
run: |
set -euo pipefail
if [[ "$(jq -r '.sourceHistory' "$RUNNER_TEMP/frozen-admission-selection.json")" == true ]]; then
if [[ "$(git -C "$ADMISSION_SELECTED_ROOT" rev-parse --is-shallow-repository)" == true ]]; then
git -C "$ADMISSION_SELECTED_ROOT" fetch --unshallow --filter=blob:none --no-tags origin "$ADMISSION_SELECTED_SHA"
fi
git -C "$ADMISSION_SELECTED_ROOT" fetch --filter=blob:none --no-tags origin '+refs/heads/extended-stable/*:refs/remotes/origin/extended-stable/*'
fi
node --input-type=module <<'NODE'
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
const paths = JSON.parse(readFileSync(`${process.env.RUNNER_TEMP}/frozen-admission-selection.json`, 'utf8')).sourcePaths;
const git = (...args) => execFileSync('git', ['-C', process.env.ADMISSION_SELECTED_ROOT, ...args], { timeout: 120000, maxBuffer: 64 * 1024 * 1024 });
if (!paths.length) process.exit(0);
const objects = git('ls-tree', '-r', '--format=%(objectname)', process.env.ADMISSION_SELECTED_SHA, '--', ...paths).toString().trim().split('\n').filter(Boolean);
if (objects.length > 4096) throw new Error('selected contract closure exceeds limit');
let bytes = 0;
for (const oid of new Set(objects)) {
const size = Number(git('cat-file', '-s', oid).toString());
if (size > 16 * 1024 * 1024 || (bytes += size) > 64 * 1024 * 1024) throw new Error('selected contract bytes exceed limit');
git('cat-file', 'blob', oid);
}
NODE
- name: Admit frozen source contracts
id: frozen_admission
env:
ADMISSION_TOOLING_ROOT: ${{ github.workspace }}/workflow
run: |
set -euo pipefail
env -i PATH="$PATH" LANG=C.UTF-8 \
node "$ADMISSION_TOOLING_ROOT/scripts/preflight-frozen-target-contracts.mjs" \
"$RUNNER_TEMP/frozen-admission-request.json" > "$RUNNER_TEMP/frozen-admission.json"
printf 'digest=%s\n' "$(jq -er '.digest' "$RUNNER_TEMP/frozen-admission.json")" >> "$GITHUB_OUTPUT"
- name: Upload frozen admission diagnostic
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: frozen-source-admission-parent-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/frozen-admission.json
if-no-files-found: error
retention-days: 7
- name: Acquire publication source metadata
if: github.run_attempt == 1 && steps.publication_request.outputs.required == 'true'
env:
PUBLICATION_TARGET_ROOT: ${{ github.workspace }}/target
PUBLICATION_TARGET_SHA: ${{ steps.resolve.outputs.sha }}
run: |
set -euo pipefail
node --input-type=module <<'NODE'
import { execFileSync } from 'node:child_process';
const root = process.env.PUBLICATION_TARGET_ROOT;
const sha = process.env.PUBLICATION_TARGET_SHA;
const git = (...args) => execFileSync('git', ['--no-replace-objects', '-C', root, ...args],
{ timeout: 120000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
const records = git('ls-tree', '-r', '-z', sha, '--', 'package.json', 'extensions', 'packages', 'apps/android/version.json')
.toString('latin1').split('\0').filter(record =>
/^(?:package\.json|apps\/android\/version\.json|extensions\/[^/]+\/(?:package\.json|README\.md)|packages\/[^/]+\/package\.json)$/.test(record.slice(record.indexOf('\t') + 1)));
if (records.length > 4096) throw new Error('publication metadata count exceeds limit');
let bytes = 0;
for (const record of records) {
const tab = record.indexOf('\t');
const header = record.slice(0, tab), path = record.slice(tab + 1);
const [mode, type, oid] = header.split(' ');
new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(path, 'latin1'));
if (!['100644', '100755'].includes(mode) || type !== 'blob') throw new Error('publication metadata must be regular committed files');
const size = Number(git('cat-file', '-s', oid).toString());
if (!Number.isSafeInteger(size) || size > 16 * 1024 * 1024 || (bytes += size) > 64 * 1024 * 1024) throw new Error('publication metadata byte limit exceeded');
git('cat-file', 'blob', oid);
}
NODE
- name: Restore immutable plan for publication admission
if: github.run_attempt != 1
continue-on-error: true
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: full-release-execution-plan
key: full-release-execution-plan-v2-${{ github.run_id }}
- name: Admit publication source
id: publication_admission
working-directory: workflow
env:
GH_TOKEN: ${{ github.token }}
PUBLICATION_TARGET_ROOT: ${{ github.workspace }}/target
PUBLICATION_REQUIRED: ${{ steps.publication_request.outputs.required }}
FULL_RELEASE_EXECUTION_PLAN_PATH: ${{ github.workspace }}/full-release-execution-plan/full-release-execution-plan.json
run: |
set -euo pipefail
umask 077
if [[ "$GITHUB_RUN_ATTEMPT" != 1 ]]; then
node scripts/full-release-validation-state.mjs restore-publication
elif [[ "$PUBLICATION_REQUIRED" == true ]]; then
node --import ./scripts/tsx.mjs scripts/full-release-publication-admission.mts \
"$RUNNER_TEMP/publication-source-request.json" \
--observations-out "$RUNNER_TEMP/publication-observations.json" > "$RUNNER_TEMP/publication-source-admission.json"
else
node scripts/full-release-publication-contract.mjs --not-applicable \
"$RUNNER_TEMP/publication-source-request.json" > "$RUNNER_TEMP/publication-source-admission.json"
fi
printf 'json=%s\n' "$(jq -ec . "$RUNNER_TEMP/publication-source-admission.json")" >> "$GITHUB_OUTPUT"
- name: Upload restored immutable release execution plan
if: github.run_attempt != 1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-release-execution-plan-${{ github.run_id }}
path: ${{ github.workspace }}/full-release-execution-plan/full-release-execution-plan.json
if-no-files-found: error
overwrite: false
- name: Upload publication source admission
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-release-source-admission-parent-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/publication-source-admission.json
if-no-files-found: error
retention-days: 7
- name: Upload immutable publication observations
id: publication_observations
if: github.run_attempt == 1 && steps.publication_request.outputs.required == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-release-publication-observations-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/publication-observations.json
if-no-files-found: error
- name: Finalize publication admission
working-directory: workflow
env:
GH_TOKEN: ${{ github.token }}
PUBLICATION_UPLOAD_OUTCOME: ${{ steps.publication_observations.outcome }}
PUBLICATION_ARTIFACT_ID: ${{ steps.publication_observations.outputs['artifact-id'] }}
PUBLICATION_ARTIFACT_DIGEST: ${{ steps.publication_observations.outputs['artifact-digest'] }}
PUBLICATION_CANDIDATE_REQUIRED: ${{ steps.candidate_request.outputs.required }}
CANDIDATE_REQUEST_JSON: ${{ steps.candidate_request.outputs.request_json }}
TARGET_REF: ${{ inputs.ref }}
TARGET_VERSION: ${{ steps.release_inputs.outputs.target_version }}
run: node scripts/full-release-validation-state.mjs finalize-publication
- name: Upload immutable publication admission
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-release-publication-admission-${{ github.run_id }}-1
path: ${{ runner.temp }}/publication-admission.json
if-no-files-found: error
- 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: ${{ steps.release_inputs.outputs.skip_package_telegram_e2e }}
COVERAGE_POLICY: ${{ steps.release_inputs.outputs.coverage_policy }}
CI_RELEASE_SCOPE: ${{ steps.release_inputs.outputs.ci_release_scope }}
TELEGRAM_WAIVER: ${{ inputs.telegram_waiver }}
WAIVED_INTEGRATION_CHANNELS: ${{ steps.release_inputs.outputs.waived_integration_channels }}
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 }}
EXTENSION_TEST_EXCLUDE_PATTERNS_JSON: ${{ steps.publication_dispatch.outputs.extension_test_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 "- CI release scope: \`${CI_RELEASE_SCOPE}\`"
if [[ "$COVERAGE_POLICY" == "npm-beta-v1" ]]; then
echo "- Native apps, product performance, and Telegram confidence: not run; deferred to postpublish confidence (\`${COVERAGE_POLICY}\`)"
fi
echo "- Fail fast: \`${FAIL_FAST}\`"
echo "- Package Acceptance Telegram E2E deferred: \`${SKIP_PACKAGE_TELEGRAM_E2E}\`"
if [[ -n "$TELEGRAM_WAIVER" ]]; then
echo "- Telegram integration checks: waived/not run (\`${TELEGRAM_WAIVER}\`); not a pass"
echo "- Owner-waived QA-live channels: $(jq -r 'join(", ")' <<< "$WAIVED_INTEGRATION_CHANNELS"); waived/not run, not a pass"
fi
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}\`"
echo "- Locale preflight: generated-locale drift is a warning and does not block dispatch. Strict \`control-ui-i18n\` and \`native-i18n\` jobs still run in normal CI; their failures fail validation."
else
echo "- Normal CI: skipped by rerun group"
fi
if [[ "$COVERAGE_POLICY" == "npm-beta-v1" ]]; then
echo "- Product performance: not run; deferred to postpublish confidence"
elif [[ "$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 [[ -n "$TELEGRAM_WAIVER" ]]; then
echo "- Source, Package Acceptance, and published-package Telegram E2E: waived/not run"
elif [[ "$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"
plugin_compatibility_readiness:
name: Enforce plugin compatibility release readiness
needs: [resolve_target]
if: needs.resolve_target.outputs.plugin_compatibility_required == 'true'
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(github.repository == 'openclaw/openclaw' && vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04'))) || (github.repository == 'openclaw/openclaw' && vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 10
steps:
- name: Checkout target source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.resolve_target.outputs.sha }}
fetch-depth: 1
filter: blob:none
persist-credentials: false
submodules: false
- name: Checkout trusted package-manager setup
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
path: .release-harness
sparse-checkout: .github/actions/setup-pnpm-store-cache
sparse-checkout-cone-mode: false
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Setup target package manager
uses: ./.release-harness/.github/actions/setup-pnpm-store-cache
with:
package-manager-file: package.json
lockfile-path: pnpm-lock.yaml
node-version: ${{ env.NODE_VERSION }}
cache-mode: restore
- name: Install target dependencies
env:
CI: "true"
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
- name: Enforce target compatibility readiness
run: pnpm plugins:boundary-report:ci
evidence_reuse:
name: Check for reusable validation evidence
needs: [resolve_target, plugin_compatibility_readiness]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && inputs.rerun_group == 'all' && inputs.reuse_evidence && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release-ci/')) }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(github.repository == 'openclaw/openclaw' && vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04'))) || (github.repository == 'openclaw/openclaw' && vars.OPENCLAW_CI_RUNNER_BACKEND == 'hybrid' && 'blacksmith-4vcpu-ubuntu-2404' || '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 }}
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
sparse-checkout: |
scripts
.github/actions/setup-pnpm-store-cache
sparse-checkout-cone-mode: false
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
sparse-checkout: |
package.json
apps/macos/Sources/OpenClaw/Resources/Info.plist
sparse-checkout-cone-mode: false
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Setup Node.js
env:
REQUESTED_NODE_VERSION: "24.x"
run: |
set -euo pipefail
source workflow/.github/actions/setup-pnpm-store-cache/ensure-node.sh
openclaw_ensure_node "$REQUESTED_NODE_VERSION"
- name: Download publication admission for reuse budgeting
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: full-release-publication-admission-${{ github.run_id }}-1
path: ${{ runner.temp }}/full-release-publication-admission
github-token: ${{ github.token }}
run-id: ${{ github.run_id }}
- name: Find reusable validation evidence
id: find
env:
GH_TOKEN: ${{ github.token }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
TARGET_VERSION: ${{ needs.resolve_target.outputs.target_version }}
VALIDATION_PURPOSE: ${{ needs.resolve_target.outputs.validation_purpose }}
PUBLICATION_SELECTION_JSON: ${{ needs.resolve_target.outputs.publication_selection_json }}
COVERAGE_POLICY: ${{ needs.resolve_target.outputs.coverage_policy }}
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: ${{ needs.resolve_target.outputs.skip_package_telegram_e2e }}
TELEGRAM_WAIVER: ${{ inputs.telegram_waiver }}
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 }}
EXTENSION_TEST_EXCLUDE_PATTERNS_JSON: ${{ needs.resolve_target.outputs.extension_test_exclude_patterns_json }}
TRUSTED_WORKFLOW_JSON: ${{ needs.resolve_target.outputs.trusted_workflow_json }}
CANDIDATE_REQUEST_JSON: ${{ needs.resolve_target.outputs.candidate_request_json }}
PUBLICATION_CANDIDATE_REQUIRED: ${{ needs.resolve_target.outputs.candidate_required }}
run: |
set -euo pipefail
if [[ "$GITHUB_RUN_ATTEMPT" != 1 ]]; then
node workflow/scripts/full-release-validation-state.mjs reuse-publication
exit 0
fi
# 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 validationPurpose "$VALIDATION_PURPOSE" \
--arg publicationSelectionJson "$PUBLICATION_SELECTION_JSON" \
--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 coveragePolicy "$COVERAGE_POLICY" \
--arg telegramWaiver "$TELEGRAM_WAIVER" \
--arg targetVersion "$TARGET_VERSION" \
--arg allowUnreleasedChangelog "$ALLOW_UNRELEASED_CHANGELOG" \
--arg pluginPrereleaseNodeExcludePatternsJson "$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON" \
--arg extensionTestExcludePatternsJson "$EXTENSION_TEST_EXCLUDE_PATTERNS_JSON" \
'{
validationPurpose: $validationPurpose,
publicationSelectionJson: $publicationSelectionJson,
provider: $provider,
mode: $mode,
targetContextRef: $targetContextRef,
targetVersion: $targetVersion,
liveSuiteFilter: $liveSuiteFilter,
crossOsSuiteFilter: $crossOsSuiteFilter,
releasePackageSpec: $releasePackageSpec,
packageAcceptancePackageSpec: $packageAcceptancePackageSpec,
codexPluginSpec: $codexPluginSpec,
npmTelegramPackageSpec: $npmTelegramPackageSpec,
npmTelegramProviderMode: $npmTelegramProviderMode,
npmTelegramScenario: $npmTelegramScenario,
skipPackageTelegramE2e: $skipPackageTelegramE2e,
allowUnreleasedChangelog: $allowUnreleasedChangelog,
pluginPrereleaseNodeExcludePatternsJson: $pluginPrereleaseNodeExcludePatternsJson,
extensionTestExcludePatternsJson: $extensionTestExcludePatternsJson
} + (if $coveragePolicy == "" then {} else {coveragePolicy: $coveragePolicy} end) + (if $telegramWaiver == "" then {} else {telegramWaiver: $telegramWaiver} end)')"
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 "$RUNNER_TEMP/reusable-evidence.outputs"
node workflow/scripts/full-release-validation-state.mjs reuse-publication
- 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"
normal_ci:
name: Run normal full CI
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && contains(fromJSON('["all","ci"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('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 }}
child_reuse: ${{ steps.dispatch.outputs.child_reuse }}
steps:
- &child_reuse_checkout
name: Checkout child reuse tooling
if: env.CHILD_EVIDENCE_REUSE == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
path: workflow
sparse-checkout: |
scripts
.github/workflows
persist-credentials: false
- &child_reuse_runtime
name: Setup child reuse runtime
if: env.CHILD_EVIDENCE_REUSE == 'true'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24.21.0"
package-manager-cache: false
- name: Dispatch CI
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_KIND: ci
CI_RELEASE_SCOPE: ${{ needs.resolve_target.outputs.ci_release_scope }}
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 60); 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
if jq -e '.status == "completed" and .conclusion == "startup_failure"' \
<<< "$candidate_run_json" >/dev/null; then
echo "::error::Refusing to adopt ${workflow} run ${candidate_run_id}: GitHub rejected the child before any jobs started (startup_failure)." >&2
jq '{id, workflow_id, path, display_title, head_branch, head_sha, event, status, conclusion, html_url}' \
<<< "$candidate_run_json" >&2
return 1
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
if [[ "${CHILD_EVIDENCE_REUSE:-false}" == true && "$CHILD_WORKFLOW_KIND" != artifact-* ]]; then
if node workflow/scripts/find-reusable-release-child.mjs "$workflow" "$@"; then
return 0
else
dispatch_status=$?
if [[ "$dispatch_status" != 3 ]]; then
echo "::warning::Child reuse lookup exited ${dispatch_status}; dispatching fresh work." >&2
fi
fi
fi
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"
echo "dispatch_id=${dispatch_id}" >> "$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}"
ci_release_scope="${CI_RELEASE_SCOPE:-full}"
include_android=true
if [[ "$ci_release_scope" == "npm-beta" || "$ci_release_scope" == "npm-stable" ]]; then include_android=false; fi
args=(-f target_ref="$TARGET_SHA" -f release_scope="$ci_release_scope" -f include_android="$include_android" -f dispatch_id="$dispatch_id")
context_ref="${TARGET_CONTEXT_REF:-$TARGET_REF}"
context_ref="${context_ref#refs/heads/}"
context_ref="${context_ref#refs/tags/}"
if [[ "$context_ref" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+((-(alpha|beta)\.[0-9]+)|(-[1-9][0-9]*))?$ ]]; then
args+=(-f historical_target_tag="$context_ref")
elif [[ "$context_ref" =~ ^(release/[0-9]{4}\.[0-9]+\.[0-9]+(-[1-9][0-9]*)?|extended-stable/[0-9]{4}\.[0-9]+\.33)$ ]]; then
args+=(-f target_context_ref="$context_ref")
fi
dispatch_child ci.yml "$dispatch_run_name" "${args[@]}"
;;
artifact-npm|artifact-candidate|artifact-docker)
stage="${CHILD_WORKFLOW_KIND#artifact-}"
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-artifacts-${stage}"
args=(-f stage="$stage" -f dispatch_id="$dispatch_id" -f source_sha="$TARGET_SHA" -f release_tag="$RELEASE_TAG" -f preflight_phase="$PREFLIGHT_PHASE")
if [[ "$stage" == "npm" ]]; then
args+=(-f npm_dist_tag="$NPM_DIST_TAG" -f release_candidate_branch="$RELEASE_CANDIDATE_BRANCH")
elif [[ "$stage" == "candidate" ]]; then
args+=(-f request_json="$CANDIDATE_REQUEST_JSON" -f prepared_npm_bundle_json="$PREPARED_NPM_BUNDLE_JSON")
fi
dispatch_child full-release-artifacts.yml "Full Release Artifacts ${dispatch_id}" "${args[@]}"
;;
plugin-prerelease)
case "$PHASE" in
independent|candidate) ;;
*)
echo "::error::Unsupported plugin prerelease phase ${PHASE}." >&2
exit 2
;;
esac
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 "- Phase: \`${PHASE}\`"
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-${PHASE}"
dispatch_run_name="Plugin Prerelease ${dispatch_id}"
args=(-f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true -f phase="$PHASE" -f dispatch_id="$dispatch_id" -f node_test_exclude_patterns_json="$PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON" -f extension_test_exclude_patterns_json="$EXTENSION_TEST_EXCLUDE_PATTERNS_JSON")
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 "${CANDIDATE_ARTIFACT_JSON// }" ]]; then
args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON")
fi
dispatch_child plugin-prerelease.yml "$dispatch_run_name" "${args[@]}"
;;
release-checks)
case "$PHASE" in
independent|candidate) ;;
*)
echo "::error::Unsupported release checks phase ${PHASE}." >&2
exit 2
;;
esac
{
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}\`"
echo "- Phase: \`${PHASE}\`"
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 phase="$PHASE"
-f allow_unreleased_changelog="$ALLOW_UNRELEASED_CHANGELOG"
-f skip_package_telegram_e2e="$SKIP_PACKAGE_TELEGRAM_E2E"
-f telegram_waiver="$TELEGRAM_WAIVER"
-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-${PHASE}"
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)"
echo "- Release impact: selected execution failures are blocking"
} >> "$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_independent:
name: Run plugin prerelease independent validation
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('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 }}
child_reuse: ${{ steps.dispatch.outputs.child_reuse }}
steps:
- *child_reuse_checkout
- *child_reuse_runtime
- name: Dispatch plugin prerelease independent phase
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_KIND: plugin-prerelease
PHASE: independent
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 }}
CANDIDATE_ARTIFACT_JSON: ""
PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }}
EXTENSION_TEST_EXCLUDE_PATTERNS_JSON: ${{ needs.resolve_target.outputs.extension_test_exclude_patterns_json }}
run: *full_release_child_dispatch
plugin_prerelease_candidate:
name: Run plugin prerelease candidate validation
needs: [resolve_target, evidence_reuse, candidate_acquisition]
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && needs.resolve_target.outputs.plugin_candidate_required == 'true' && needs.candidate_acquisition.result == 'success' && needs.candidate_acquisition.outputs.state == 'ready' && contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('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 }}
child_reuse: ${{ steps.dispatch.outputs.child_reuse }}
steps:
- *child_reuse_checkout
- *child_reuse_runtime
- name: Dispatch plugin prerelease candidate phase
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_KIND: plugin-prerelease
PHASE: candidate
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 }}
CANDIDATE_ARTIFACT_JSON: ${{ needs.candidate_acquisition.outputs.candidate_artifact_json }}
PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }}
EXTENSION_TEST_EXCLUDE_PATTERNS_JSON: ${{ needs.resolve_target.outputs.extension_test_exclude_patterns_json }}
run: *full_release_child_dispatch
release_checks_independent:
name: Run release checks independent validation
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && contains(fromJSON('["all","install-smoke","live-e2e","qa-parity","qa-live"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404'))) || (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || '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 }}
child_reuse: ${{ steps.dispatch.outputs.child_reuse }}
steps:
- *child_reuse_checkout
- *child_reuse_runtime
- name: Dispatch release checks independent phase
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_KIND: release-checks
PHASE: independent
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: ""
SKIP_PACKAGE_TELEGRAM_E2E: ${{ needs.resolve_target.outputs.skip_package_telegram_e2e }}
TELEGRAM_WAIVER: ${{ inputs.telegram_waiver }}
run: *full_release_child_dispatch
release_checks_candidate:
name: Run release checks candidate validation
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse, candidate_acquisition]
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && (needs.resolve_target.outputs.release_candidate_artifact_required != 'true' || (needs.candidate_acquisition.result == 'success' && needs.candidate_acquisition.outputs.state == 'ready')) && (contains(fromJSON('["all","cross-os","package"]'), inputs.rerun_group) || (inputs.rerun_group == 'live-e2e' && needs.resolve_target.outputs.live_suite_filter == '')) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404'))) || (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || '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 }}
child_reuse: ${{ steps.dispatch.outputs.child_reuse }}
steps:
- *child_reuse_checkout
- *child_reuse_runtime
- name: Dispatch release checks candidate phase
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_KIND: release-checks
PHASE: candidate
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.resolve_target.outputs.release_candidate_artifact_required == 'true' && needs.candidate_acquisition.outputs.candidate_artifact_json || '' }}
SKIP_PACKAGE_TELEGRAM_E2E: ${{ needs.resolve_target.outputs.skip_package_telegram_e2e }}
TELEGRAM_WAIVER: ${{ inputs.telegram_waiver }}
run: *full_release_child_dispatch
npm_telegram:
name: Run package Telegram E2E
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && inputs.telegram_waiver == '' && needs.resolve_target.outputs.coverage_policy != 'npm-beta-v1' && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group) && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '') && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('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 }}
child_reuse: ${{ steps.dispatch.outputs.child_reuse }}
steps:
- *child_reuse_checkout
- *child_reuse_runtime
- 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, plugin_compatibility_readiness, evidence_reuse]
if: ${{ always() && github.run_attempt == 1 && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && needs.resolve_target.outputs.coverage_policy != 'npm-beta-v1' && contains(fromJSON('["all","performance"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404'))) || (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || '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 }}
child_reuse: ${{ steps.dispatch.outputs.child_reuse }}
steps:
- *child_reuse_checkout
- *child_reuse_runtime
- 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
prepare_npm_package:
name: Prepare release npm artifacts
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && (inputs.rerun_group == 'all' || needs.resolve_target.outputs.candidate_required == 'true') }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('ubuntu-24.04') }}
timeout-minutes: 360
outputs:
run_id: ${{ steps.producer.outputs.run_id }}
run_attempt: ${{ steps.producer.outputs.run_attempt }}
dispatch_id: ${{ steps.producer.outputs.dispatch_id }}
prepared_bundle_json: ${{ steps.bundle.outputs.prepared_bundle_json }}
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_KIND: artifact-npm
ARTIFACT_STAGE: npm
ARTIFACT_DISPATCH_ID: full-release-validation-${{ github.run_id }}-1-artifacts-npm
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
RELEASE_TAG: ${{ needs.resolve_target.outputs.release_tag }}
NPM_DIST_TAG: ${{ needs.resolve_target.outputs.npm_dist_tag }}
RELEASE_CANDIDATE_BRANCH: ${{ needs.resolve_target.outputs.release_candidate_branch }}
PREFLIGHT_PHASE: ${{ inputs.rerun_group == 'all' && 'all' || 'prepare' }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
steps:
- name: Dispatch immutable npm artifact producer
id: dispatch
if: github.run_attempt == 1
run: *full_release_child_dispatch
- &artifact_tooling_checkout
name: Checkout artifact receipt tooling
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
sparse-checkout: scripts
persist-credentials: false
- name: Setup supported Node runtime
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- &artifact_producer_resolution
name: Recover original artifact producer
id: producer
env:
ARTIFACT_RUN_ID: ${{ steps.dispatch.outputs.run_id }}
ARTIFACT_RUN_ATTEMPT: ${{ steps.dispatch.outputs.run_attempt }}
run: node scripts/full-release-artifacts.mjs resolve
# Raw bytes unblock package consumers while source/SDK/dependency proof continues.
- name: Wait for publishable npm package
id: bundle
env:
ARTIFACT_RUN_ID: ${{ steps.producer.outputs.run_id }}
ARTIFACT_RUN_ATTEMPT: ${{ steps.producer.outputs.run_attempt }}
ARTIFACT_OUTPUT: raw
run: node scripts/full-release-artifacts.mjs wait
qualify_npm_package:
name: Qualify release npm artifacts
needs: [resolve_target, prepare_npm_package]
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.rerun_group == 'all' && needs.prepare_npm_package.result == 'success' }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('ubuntu-24.04') }}
timeout-minutes: 360
outputs:
qualified_preflight_bundle_json: ${{ steps.bundle.outputs.qualified_preflight_bundle_json }}
env:
GH_TOKEN: ${{ github.token }}
ARTIFACT_STAGE: npm
ARTIFACT_DISPATCH_ID: ${{ needs.prepare_npm_package.outputs.dispatch_id }}
ARTIFACT_RUN_ID: ${{ needs.prepare_npm_package.outputs.run_id }}
ARTIFACT_RUN_ATTEMPT: ${{ needs.prepare_npm_package.outputs.run_attempt }}
ARTIFACT_OUTPUT: receipt
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
RELEASE_TAG: ${{ needs.resolve_target.outputs.release_tag }}
PREFLIGHT_PHASE: all
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
steps:
- *artifact_tooling_checkout
- name: Setup supported Node runtime
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Wait for exact npm qualification
id: bundle
run: node scripts/full-release-artifacts.mjs wait
prepare_docker_release:
name: Prepare release Docker artifacts
needs: [resolve_target, plugin_compatibility_readiness, evidence_reuse]
if: ${{ always() && inputs.rerun_group == 'all' && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.plugin_compatibility_readiness.result) && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('ubuntu-24.04') }}
timeout-minutes: 360
outputs:
prepared_run_id: ${{ steps.bundle.outputs.prepared_run_id }}
prepared_run_attempt: ${{ steps.bundle.outputs.prepared_run_attempt }}
prepared_artifact_name: ${{ steps.bundle.outputs.prepared_artifact_name }}
prepared_manifest_sha256: ${{ steps.bundle.outputs.prepared_manifest_sha256 }}
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_KIND: artifact-docker
ARTIFACT_STAGE: docker
ARTIFACT_DISPATCH_ID: full-release-validation-${{ github.run_id }}-1-artifacts-docker
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
RELEASE_TAG: ${{ needs.resolve_target.outputs.release_tag }}
PREFLIGHT_PHASE: all
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
steps:
- name: Dispatch immutable Docker artifact producer
id: dispatch
if: github.run_attempt == 1
run: *full_release_child_dispatch
- *artifact_tooling_checkout
- name: Setup supported Node runtime
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- *artifact_producer_resolution
- name: Wait for exact Docker preparation
id: bundle
env:
ARTIFACT_RUN_ID: ${{ steps.producer.outputs.run_id }}
ARTIFACT_RUN_ATTEMPT: ${{ steps.producer.outputs.run_attempt }}
ARTIFACT_OUTPUT: receipt
run: node scripts/full-release-artifacts.mjs wait
candidate_acquisition:
name: Acquire full release candidate
needs: [resolve_target, evidence_reuse, prepare_npm_package]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["success","skipped"]'), needs.evidence_reuse.result) && needs.prepare_npm_package.result == 'success' && needs.evidence_reuse.outputs.reuse != 'true' && needs.resolve_target.outputs.candidate_required == 'true' }}
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('ubuntu-24.04') }}
timeout-minutes: 360
outputs:
state: ${{ steps.bundle.outputs.state }}
binding_json: ${{ steps.bundle.outputs.binding_json }}
candidate_artifact_json: ${{ steps.bundle.outputs.candidate_artifact_json }}
env:
GH_TOKEN: ${{ github.token }}
CHILD_WORKFLOW_KIND: artifact-candidate
ARTIFACT_STAGE: candidate
ARTIFACT_DISPATCH_ID: full-release-validation-${{ github.run_id }}-1-artifacts-candidate
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
RELEASE_TAG: ${{ needs.resolve_target.outputs.release_tag }}
PREFLIGHT_PHASE: all
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
CANDIDATE_REQUEST_JSON: ${{ needs.resolve_target.outputs.candidate_request_json }}
PREPARED_NPM_BUNDLE_JSON: ${{ needs.prepare_npm_package.outputs.prepared_bundle_json }}
steps:
- name: Dispatch immutable validation candidate producer
id: dispatch
if: github.run_attempt == 1
run: *full_release_child_dispatch
- *artifact_tooling_checkout
- name: Setup supported Node runtime
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- *artifact_producer_resolution
- name: Wait for exact validation candidate
id: bundle
env:
ARTIFACT_RUN_ID: ${{ steps.producer.outputs.run_id }}
ARTIFACT_RUN_ATTEMPT: ${{ steps.producer.outputs.run_attempt }}
ARTIFACT_OUTPUT: receipt
run: node scripts/full-release-artifacts.mjs wait
release_execution_plan:
name: Seal release execution plan
needs:
[
resolve_target,
evidence_reuse,
candidate_acquisition,
normal_ci,
plugin_prerelease_independent,
plugin_prerelease_candidate,
release_checks_independent,
release_checks_candidate,
npm_telegram,
performance,
]
if: always()
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404'))) || (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || '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
sparse-checkout-cone-mode: false
persist-credentials: false
# Reruns lose parent artifacts. The original upload's digest witness
# authenticates cached bytes before resolution republishes the same plan.
# Relative cache paths keep the version identical across runner classes.
- name: Cache immutable release execution plan
id: plan_cache
continue-on-error: true
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: full-release-execution-plan
key: full-release-execution-plan-v2-${{ github.run_id }}
- name: Restore immutable release execution plan artifact
if: ${{ always() && github.run_attempt != 1 && steps.plan_cache.outputs.cache-hit != 'true' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: full-release-execution-plan-${{ github.run_id }}
path: ${{ github.workspace }}/full-release-execution-plan
github-token: ${{ github.token }}
run-id: ${{ github.run_id }}
- name: Download immutable publication admission
if: github.run_attempt == 1 && needs.resolve_target.result == 'success'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: full-release-publication-admission-${{ github.run_id }}-1
path: ${{ runner.temp }}/full-release-publication-admission
github-token: ${{ github.token }}
run-id: ${{ github.run_id }}
- name: Setup supported Node runtime
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- name: Seal immutable release execution plan
id: plan
env:
GH_TOKEN: ${{ github.token }}
SOURCE_ADMISSION_JSON: ${{ needs.resolve_target.outputs.source_admission_json }}
TARGET_CONTEXT_REF: ${{ inputs.target_context_ref || inputs.ref }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RERUN_GROUP: ${{ inputs.rerun_group }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
TARGET_VERSION: ${{ needs.resolve_target.outputs.target_version }}
COVERAGE_POLICY: ${{ needs.resolve_target.outputs.coverage_policy }}
TELEGRAM_WAIVER: ${{ inputs.telegram_waiver }}
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 }}
NORMAL_CI_REUSE: ${{ needs.normal_ci.outputs.child_reuse || 'null' }}
PLUGIN_PRERELEASE_INDEPENDENT_RESULT: ${{ needs.plugin_prerelease_independent.result }}
PLUGIN_PRERELEASE_INDEPENDENT_RUN_ID: ${{ needs.plugin_prerelease_independent.outputs.run_id }}
PLUGIN_PRERELEASE_INDEPENDENT_RUN_ATTEMPT: ${{ needs.plugin_prerelease_independent.outputs.run_attempt }}
PLUGIN_PRERELEASE_INDEPENDENT_URL: ${{ needs.plugin_prerelease_independent.outputs.url }}
PLUGIN_PRERELEASE_INDEPENDENT_REUSE: ${{ needs.plugin_prerelease_independent.outputs.child_reuse || 'null' }}
PLUGIN_PRERELEASE_CANDIDATE_RESULT: ${{ needs.plugin_prerelease_candidate.result }}
PLUGIN_PRERELEASE_CANDIDATE_RUN_ID: ${{ needs.plugin_prerelease_candidate.outputs.run_id }}
PLUGIN_PRERELEASE_CANDIDATE_RUN_ATTEMPT: ${{ needs.plugin_prerelease_candidate.outputs.run_attempt }}
PLUGIN_PRERELEASE_CANDIDATE_URL: ${{ needs.plugin_prerelease_candidate.outputs.url }}
PLUGIN_PRERELEASE_CANDIDATE_REUSE: ${{ needs.plugin_prerelease_candidate.outputs.child_reuse || 'null' }}
RELEASE_CHECKS_INDEPENDENT_RESULT: ${{ needs.release_checks_independent.result }}
RELEASE_CHECKS_INDEPENDENT_RUN_ID: ${{ needs.release_checks_independent.outputs.run_id }}
RELEASE_CHECKS_INDEPENDENT_RUN_ATTEMPT: ${{ needs.release_checks_independent.outputs.run_attempt }}
RELEASE_CHECKS_INDEPENDENT_URL: ${{ needs.release_checks_independent.outputs.url }}
RELEASE_CHECKS_INDEPENDENT_REUSE: ${{ needs.release_checks_independent.outputs.child_reuse || 'null' }}
RELEASE_CHECKS_CANDIDATE_RESULT: ${{ needs.release_checks_candidate.result }}
RELEASE_CHECKS_CANDIDATE_RUN_ID: ${{ needs.release_checks_candidate.outputs.run_id }}
RELEASE_CHECKS_CANDIDATE_RUN_ATTEMPT: ${{ needs.release_checks_candidate.outputs.run_attempt }}
RELEASE_CHECKS_CANDIDATE_URL: ${{ needs.release_checks_candidate.outputs.url }}
RELEASE_CHECKS_CANDIDATE_REUSE: ${{ needs.release_checks_candidate.outputs.child_reuse || 'null' }}
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 }}
NPM_TELEGRAM_REUSE: ${{ needs.npm_telegram.outputs.child_reuse || 'null' }}
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 }}
PERFORMANCE_REUSE: ${{ needs.performance.outputs.child_reuse || 'null' }}
RESOLVE_TARGET_RESULT: ${{ needs.resolve_target.result }}
CANDIDATE_REQUIRED: ${{ needs.resolve_target.outputs.candidate_required }}
CANDIDATE_ACQUISITION_RESULT: ${{ needs.candidate_acquisition.result }}
CANDIDATE_EVIDENCE_JSON: ${{ needs.candidate_acquisition.outputs.binding_json }}
CANDIDATE_REQUEST_JSON: ${{ needs.resolve_target.outputs.candidate_request_json }}
FULL_RELEASE_RESTORE_PLAN: ${{ github.run_attempt != 1 }}
FULL_RELEASE_EXECUTION_PLAN_PATH: ${{ github.workspace }}/full-release-execution-plan/full-release-execution-plan.json
PUBLICATION_ADMISSION_PATH: ${{ runner.temp }}/full-release-publication-admission/publication-admission.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 sourceAdmissionContract "$FULL_RELEASE_SOURCE_ADMISSION_CONTRACT" \
--argjson sourceAdmission "${SOURCE_ADMISSION_JSON:-null}" \
--argjson childPhaseVersion 3 \
--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 coveragePolicy "$COVERAGE_POLICY" \
--arg telegramWaiver "$TELEGRAM_WAIVER" \
--arg targetVersion "$TARGET_VERSION" \
--arg liveSuiteFilter "$LIVE_SUITE_FILTER" \
--arg resolveTargetResult "$RESOLVE_TARGET_RESULT" \
--arg candidateRequired "$CANDIDATE_REQUIRED" \
--arg candidateAcquisitionResult "$CANDIDATE_ACQUISITION_RESULT" \
--argjson candidateEvidence "${CANDIDATE_EVIDENCE_JSON:-null}" \
--argjson candidateRequestInput "$CANDIDATE_REQUEST_JSON" \
--arg normalCiResult "$NORMAL_CI_RESULT" \
--arg normalCiRunId "$NORMAL_CI_RUN_ID" \
--arg normalCiRunAttempt "$NORMAL_CI_RUN_ATTEMPT" \
--arg normalCiUrl "$NORMAL_CI_URL" \
--arg pluginPrereleaseIndependentResult "$PLUGIN_PRERELEASE_INDEPENDENT_RESULT" \
--arg pluginPrereleaseIndependentRunId "$PLUGIN_PRERELEASE_INDEPENDENT_RUN_ID" \
--arg pluginPrereleaseIndependentRunAttempt "$PLUGIN_PRERELEASE_INDEPENDENT_RUN_ATTEMPT" \
--arg pluginPrereleaseIndependentUrl "$PLUGIN_PRERELEASE_INDEPENDENT_URL" \
--arg pluginPrereleaseCandidateResult "$PLUGIN_PRERELEASE_CANDIDATE_RESULT" \
--arg pluginPrereleaseCandidateRunId "$PLUGIN_PRERELEASE_CANDIDATE_RUN_ID" \
--arg pluginPrereleaseCandidateRunAttempt "$PLUGIN_PRERELEASE_CANDIDATE_RUN_ATTEMPT" \
--arg pluginPrereleaseCandidateUrl "$PLUGIN_PRERELEASE_CANDIDATE_URL" \
--arg releaseChecksIndependentResult "$RELEASE_CHECKS_INDEPENDENT_RESULT" \
--arg releaseChecksIndependentRunId "$RELEASE_CHECKS_INDEPENDENT_RUN_ID" \
--arg releaseChecksIndependentRunAttempt "$RELEASE_CHECKS_INDEPENDENT_RUN_ATTEMPT" \
--arg releaseChecksIndependentUrl "$RELEASE_CHECKS_INDEPENDENT_URL" \
--arg releaseChecksCandidateResult "$RELEASE_CHECKS_CANDIDATE_RESULT" \
--arg releaseChecksCandidateRunId "$RELEASE_CHECKS_CANDIDATE_RUN_ID" \
--arg releaseChecksCandidateRunAttempt "$RELEASE_CHECKS_CANDIDATE_RUN_ATTEMPT" \
--arg releaseChecksCandidateUrl "$RELEASE_CHECKS_CANDIDATE_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" \
--argjson normalCiReuse "$NORMAL_CI_REUSE" \
--argjson pluginPrereleaseIndependentReuse "$PLUGIN_PRERELEASE_INDEPENDENT_REUSE" \
--argjson pluginPrereleaseCandidateReuse "$PLUGIN_PRERELEASE_CANDIDATE_REUSE" \
--argjson releaseChecksIndependentReuse "$RELEASE_CHECKS_INDEPENDENT_REUSE" \
--argjson releaseChecksCandidateReuse "$RELEASE_CHECKS_CANDIDATE_REUSE" \
--argjson npmTelegramReuse "$NPM_TELEGRAM_REUSE" \
--argjson productPerformanceReuse "$PERFORMANCE_REUSE" \
'{
parentRunId: $parentRunId,
parentRunAttempt: $parentRunAttempt,
sourceAdmissionContract: $sourceAdmissionContract,
sourceAdmission: $sourceAdmission,
childPhaseVersion: $childPhaseVersion,
workflowRef: $workflowRef,
workflowSha: $workflowSha,
trustedWorkflow: $trustedWorkflow,
evidenceReuse: $evidenceReuse,
evidenceRunId: $evidenceRunId,
evidenceRootRunId: $evidenceRootRunId,
evidenceRunUrl: $evidenceRunUrl,
evidenceSha: $evidenceSha,
evidencePolicy: $evidencePolicy,
evidenceChangedPaths: $evidenceChangedPaths,
rerunGroup: $rerunGroup,
telegramWaiver: $telegramWaiver,
targetVersion: $targetVersion,
releasePackageSpec: $releasePackageSpec,
packageAcceptancePackageSpec: $packageAcceptancePackageSpec,
npmTelegramPackageSpec: $npmTelegramPackageSpec,
liveSuiteFilter: $liveSuiteFilter,
resolveTargetResult: $resolveTargetResult,
candidateRequired: $candidateRequired,
candidateAcquisitionResult: $candidateAcquisitionResult,
candidateEvidence: $candidateEvidence,
candidateRequestInput: $candidateRequestInput,
children: {
normalCi: {result: $normalCiResult, runId: $normalCiRunId, runAttempt: $normalCiRunAttempt, url: $normalCiUrl},
pluginPrereleaseIndependent: {result: $pluginPrereleaseIndependentResult, runId: $pluginPrereleaseIndependentRunId, runAttempt: $pluginPrereleaseIndependentRunAttempt, url: $pluginPrereleaseIndependentUrl},
pluginPrereleaseCandidate: {result: $pluginPrereleaseCandidateResult, runId: $pluginPrereleaseCandidateRunId, runAttempt: $pluginPrereleaseCandidateRunAttempt, url: $pluginPrereleaseCandidateUrl},
releaseChecksIndependent: {result: $releaseChecksIndependentResult, runId: $releaseChecksIndependentRunId, runAttempt: $releaseChecksIndependentRunAttempt, url: $releaseChecksIndependentUrl},
releaseChecksCandidate: {result: $releaseChecksCandidateResult, runId: $releaseChecksCandidateRunId, runAttempt: $releaseChecksCandidateRunAttempt, url: $releaseChecksCandidateUrl},
npmTelegram: {result: $npmTelegramResult, runId: $npmTelegramRunId, runAttempt: $npmTelegramRunAttempt, url: $npmTelegramUrl},
productPerformance: {result: $performanceResult, runId: $performanceRunId, runAttempt: $performanceRunAttempt, url: $performanceUrl}
}
} + (if $coveragePolicy == "" then {} else {coveragePolicy: $coveragePolicy} end)
+ ({normalCi: $normalCiReuse, pluginPrereleaseIndependent: $pluginPrereleaseIndependentReuse, pluginPrereleaseCandidate: $pluginPrereleaseCandidateReuse, releaseChecksIndependent: $releaseChecksIndependentReuse, releaseChecksCandidate: $releaseChecksCandidateReuse, npmTelegram: $npmTelegramReuse, productPerformance: $productPerformanceReuse} | with_entries(select(.value != null)) | if length == 0 then {} else {childReuse: .} end)'
)"
fi
node scripts/full-release-validation-state.mjs plan
- name: Upload immutable release execution plan
id: plan_upload
# The sealer can write a valid interrupted checkpoint and exit nonzero.
# Only its attempt-one write signal may publish the immutable alias.
if: ${{ always() && github.run_attempt == 1 && steps.plan.outputs.sha256 != '' && steps.plan.outputs.source_parent_attempt == '1' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-release-execution-plan-${{ github.run_id }}
path: ${{ github.workspace }}/full-release-execution-plan/full-release-execution-plan.json
if-no-files-found: error
overwrite: false
- name: Record immutable release execution plan digest
id: plan_witness
if: ${{ always() && github.run_attempt == 1 && steps.plan_upload.outcome == 'success' }}
env:
EXECUTION_PLAN_SHA256: ${{ steps.plan.outputs.sha256 }}
run: |
set -euo pipefail
[[ "$EXECUTION_PLAN_SHA256" =~ ^[a-f0-9]{64}$ ]]
printf 'FRV_EXECUTION_PLAN_SHA256=%s\n' "$EXECUTION_PLAN_SHA256"
# Explicit saving also retains a valid checkpoint from an interrupted seal.
- name: Save immutable release execution plan
if: ${{ always() && github.run_attempt == 1 && steps.plan_witness.outcome == 'success' }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: full-release-execution-plan
key: full-release-execution-plan-v2-${{ github.run_id }}
release_decision:
name: Release Decision
needs: [resolve_target, release_execution_plan]
if: always()
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404'))) || (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || '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
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: Setup supported Node runtime
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- 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: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON(vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404'))) || (vars.OPENCLAW_CI_RUNNER_BACKEND == 'github' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404') }}
timeout-minutes: 720
outputs:
state: ${{ steps.state.outputs.state }}
normal_ci_conclusion: ${{ steps.state.outputs.normalCi_conclusion }}
plugin_prerelease_independent_conclusion: ${{ steps.state.outputs.pluginPrereleaseIndependent_conclusion }}
plugin_prerelease_candidate_conclusion: ${{ steps.state.outputs.pluginPrereleaseCandidate_conclusion }}
release_checks_independent_conclusion: ${{ steps.state.outputs.releaseChecksIndependent_conclusion }}
release_checks_candidate_conclusion: ${{ steps.state.outputs.releaseChecksCandidate_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
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: Setup supported Node runtime
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- 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: Summarize locale validation
if: always() && steps.state.outputs.state != ''
env:
DIAGNOSTIC_DRAIN_PATH: ${{ runner.temp }}/full-release-diagnostics/full-release-diagnostic-manifest.json
run: |
node <<'NODE'
const fs = require("node:fs");
const diagnostics = JSON.parse(fs.readFileSync(process.env.DIAGNOSTIC_DRAIN_PATH, "utf8"));
const jobs = diagnostics.children.normalCi?.timing.jobs ?? [];
const lines = ["## Generated locale validation", "", "| Job | Result |", "| --- | --- |"];
const warnings = [];
for (const name of ["control-ui-i18n", "native-i18n"]) {
const result = jobs.find((job) => job.name === name)?.conclusion || "not recorded";
lines.push(`| ${name} | ${result} |`);
if (!["success", "skipped", "not recorded"].includes(result)) {
console.log(`::warning::${name}: ${result}. Inspect the strict locale job; generated-locale drift must not prevent FRV dispatch.`);
warnings.push("", "> [!WARNING]", `> ${name}: ${result}. Generated-locale drift does not block dispatch; the strict job still fails validation.`);
}
}
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, [...lines, ...warnings].join("\n") + "\n");
NODE
- 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,
prepare_npm_package,
qualify_npm_package,
prepare_docker_release,
candidate_acquisition,
normal_ci,
plugin_prerelease_independent,
plugin_prerelease_candidate,
release_checks_independent,
release_checks_candidate,
npm_telegram,
performance,
release_execution_plan,
release_decision,
diagnostic_drain,
]
if: always()
runs-on: ${{ vars.OPENCLAW_RELEASE_RUNNER_GROUP != '' && fromJSON(format('{{"group":{0},"labels":{1}}}', toJSON(vars.OPENCLAW_RELEASE_RUNNER_GROUP), toJSON('ubuntu-24.04'))) || ('ubuntu-24.04') }}
timeout-minutes: 10
steps:
- name: Require qualified publication artifacts
if: inputs.rerun_group == 'all'
env:
NPM_PREPARE_RESULT: ${{ needs.prepare_npm_package.result }}
NPM_QUALIFY_RESULT: ${{ needs.qualify_npm_package.result }}
DOCKER_PREPARE_RESULT: ${{ needs.prepare_docker_release.result }}
QUALIFIED_NPM_BUNDLE_JSON: ${{ needs.qualify_npm_package.outputs.qualified_preflight_bundle_json }}
PREPARED_DOCKER_MANIFEST_SHA256: ${{ needs.prepare_docker_release.outputs.prepared_manifest_sha256 }}
TARGET_VERSION: ${{ needs.resolve_target.outputs.target_version }}
run: |
set -euo pipefail
[[ "$NPM_PREPARE_RESULT" == "success" && "$NPM_QUALIFY_RESULT" == "success" &&
-n "$QUALIFIED_NPM_BUNDLE_JSON" ]] || {
echo "Release package qualification and Docker preparation must both succeed." >&2
exit 1
}
[[ "$DOCKER_PREPARE_RESULT" == "success" && "$PREPARED_DOCKER_MANIFEST_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo "Release Docker preparation must succeed." >&2
exit 1
}
- name: Checkout release state verifier
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
sparse-checkout: scripts
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: Setup supported Node runtime
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
package-manager-cache: false
- 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
# Exact IDs from the sealed plan prevent verification from drifting to a newer upload.
# Parent-owned candidate plans are rejected before same-parent continuation.
- name: Verify sealed release candidate
env:
GH_TOKEN: ${{ github.token }}
RELEASE_EXECUTION_PLAN_PATH: ${{ runner.temp }}/full-release-execution-plan/full-release-execution-plan.json
run: |
node scripts/full-release-candidate-reuse.mjs verify \
--plan "$RELEASE_EXECUTION_PLAN_PATH" \
--consumer-run-id "$GITHUB_RUN_ID" \
--consumer-run-attempt "$GITHUB_RUN_ATTEMPT"
- 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 == "releaseChecksIndependent" or .key == "releaseChecksCandidate") | .selected] | any' \
"$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:
GH_TOKEN: ${{ github.token }}
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: ${{ needs.resolve_target.outputs.skip_package_telegram_e2e }}
TELEGRAM_WAIVER: ${{ inputs.telegram_waiver }}
TARGET_VERSION: ${{ needs.resolve_target.outputs.target_version }}
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 }}
EXTENSION_TEST_EXCLUDE_PATTERNS_JSON: ${{ needs.resolve_target.outputs.extension_test_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
QUALIFIED_NPM_BUNDLE_JSON: ${{ needs.qualify_npm_package.outputs.qualified_preflight_bundle_json }}
PREPARED_DOCKER_RUN_ID: ${{ needs.prepare_docker_release.outputs.prepared_run_id }}
PREPARED_DOCKER_RUN_ATTEMPT: ${{ needs.prepare_docker_release.outputs.prepared_run_attempt }}
PREPARED_DOCKER_ARTIFACT_NAME: ${{ needs.prepare_docker_release.outputs.prepared_artifact_name }}
PREPARED_DOCKER_MANIFEST_SHA256: ${{ needs.prepare_docker_release.outputs.prepared_manifest_sha256 }}
run: node scripts/full-release-validation-state.mjs write-manifest
- 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_DECISION_PATH: ${{ runner.temp }}/full-release-decision/full-release-decision.json
DIAGNOSTIC_DRAIN_PATH: ${{ runner.temp }}/full-release-diagnostics/full-release-diagnostic-manifest.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