fix: honor configured default agent in consults (#111749) #31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: OpenClaw Stable Main Closeout | |
| on: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: Stable OpenClaw tag to replay or repair, for example v2026.6.8 or v2026.6.8-2 | |
| required: false | |
| type: string | |
| rollback_drill_id: | |
| description: Opaque identifier for the current private rollback drill record | |
| required: false | |
| type: string | |
| rollback_drill_date: | |
| description: UTC date of the private rollback drill in YYYY-MM-DD form; must be within 90 days | |
| required: false | |
| type: string | |
| allow_failed_publish_recovery: | |
| description: Accept a failed Release Publish parent only after every stable platform asset was repaired and published | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| actions: read | |
| contents: write | |
| concurrency: | |
| group: openclaw-stable-main-closeout-${{ github.event_name == 'workflow_dispatch' && (inputs.tag || github.run_id) || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} | |
| jobs: | |
| resolve: | |
| name: Resolve stable release closeout inputs | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| outputs: | |
| full_release_validation_run_id: ${{ steps.inputs.outputs.full_release_validation_run_id }} | |
| full_release_validation_run_attempt: ${{ steps.inputs.outputs.full_release_validation_run_attempt }} | |
| release_publish_run_id: ${{ steps.inputs.outputs.release_publish_run_id }} | |
| rollback_drill_date: ${{ steps.inputs.outputs.rollback_drill_date }} | |
| rollback_drill_id: ${{ steps.inputs.outputs.rollback_drill_id }} | |
| evidence_tag: ${{ steps.inputs.outputs.evidence_tag }} | |
| fallback_correction: ${{ steps.inputs.outputs.fallback_correction }} | |
| main_ref: ${{ steps.inputs.outputs.main_ref }} | |
| repair_partial_closeout: ${{ steps.inputs.outputs.repair_partial_closeout }} | |
| should_closeout: ${{ steps.inputs.outputs.should_closeout }} | |
| tag: ${{ steps.inputs.outputs.tag }} | |
| steps: | |
| - name: Install GitHub API backoff helper | |
| run: | | |
| cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH' | |
| gh_with_retry() { | |
| local attempt output status lower_output | |
| for attempt in 1 2 3 4 5; do | |
| if output="$(gh "$@" 2>&1)"; then | |
| printf '%s\n' "$output" | |
| return 0 | |
| fi | |
| status=$? | |
| lower_output="${output,,}" | |
| if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then | |
| printf '%s\n' "$output" >&2 | |
| return "$status" | |
| fi | |
| echo "::warning::GitHub API throttled stable closeout on attempt ${attempt}; retrying after backoff." >&2 | |
| sleep $((attempt * attempt * 5)) | |
| done | |
| printf '%s\n' "$output" >&2 | |
| return "$status" | |
| } | |
| BASH | |
| - name: Checkout pushed main | |
| if: ${{ github.event_name == 'push' }} | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| ref: ${{ github.sha }} | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Resolve published stable release evidence | |
| id: inputs | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| GH_TOKEN: ${{ github.token }} | |
| MANUAL_TAG: ${{ inputs.tag }} | |
| ROLLBACK_DRILL_DATE: ${{ inputs.rollback_drill_date || vars.RELEASE_ROLLBACK_DRILL_DATE }} | |
| ROLLBACK_DRILL_ID: ${{ inputs.rollback_drill_id || vars.RELEASE_ROLLBACK_DRILL_ID }} | |
| TRIGGER_SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$EVENT_NAME" == "push" ]]; then | |
| sleep 45 | |
| fi | |
| . "$RUNNER_TEMP/github-api-backoff.sh" | |
| if [[ "$EVENT_NAME" == "push" ]]; then | |
| main_ref="$TRIGGER_SHA" | |
| tag="$(gh_with_retry release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --limit 100 \ | |
| --json tagName,isPrerelease,publishedAt \ | |
| --jq '[.[] | select(.isPrerelease | not) | select(.tagName | test("^v[0-9]{4}\\.[0-9]+\\.[0-9]+(-[0-9]+)?$"))] | sort_by(.publishedAt) | last | .tagName // empty')" | |
| if [[ -z "$tag" ]]; then | |
| echo "should_closeout=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| else | |
| tag="$MANUAL_TAG" | |
| fi | |
| if [[ ! "$tag" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then | |
| if [[ "$EVENT_NAME" == "push" ]]; then | |
| echo "should_closeout=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Stable main closeout accepts only a stable vYYYY.M.PATCH or vYYYY.M.PATCH-N tag, got $tag." >&2 | |
| exit 1 | |
| fi | |
| release_asset_version="${tag#v}" | |
| release_package_version="$release_asset_version" | |
| fallback_package_version="$release_asset_version" | |
| if [[ "$release_package_version" =~ ^(.+)-[0-9]+$ ]]; then | |
| fallback_package_version="${BASH_REMATCH[1]}" | |
| fi | |
| tag_package_content="$RUNNER_TEMP/tag-package-content.b64" | |
| tag_package_read=false | |
| for attempt in 1 2 3; do | |
| if gh_with_retry api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag" \ | |
| --jq '.content' > "$tag_package_content"; then | |
| tag_package_read=true | |
| break | |
| fi | |
| if [[ "$attempt" != "3" ]]; then | |
| sleep $((attempt * 5)) | |
| fi | |
| done | |
| if [[ "$tag_package_read" != "true" ]]; then | |
| echo "Stable closeout could not read package.json for $tag from GitHub API." >&2 | |
| exit 1 | |
| fi | |
| if ! tag_package_json="$(tr -d '\n' < "$tag_package_content" | base64 --decode)"; then | |
| echo "Stable closeout package.json content for $tag was not valid base64." >&2 | |
| exit 1 | |
| fi | |
| tag_package_version="$(jq -r '.version // empty' <<<"$tag_package_json")" | |
| fallback_correction=false | |
| evidence_source_tag="$tag" | |
| if [[ "$release_package_version" != "$fallback_package_version" && | |
| "$tag_package_version" == "$fallback_package_version" ]]; then | |
| fallback_correction=true | |
| evidence_source_tag="v$fallback_package_version" | |
| elif [[ "$tag_package_version" != "$release_package_version" ]]; then | |
| echo "Stable closeout requires $tag package.json to match $release_package_version, or the legacy fallback package version $fallback_package_version." >&2 | |
| exit 1 | |
| fi | |
| evidence_version="${evidence_source_tag#v}" | |
| evidence_asset="openclaw-${evidence_version}-postpublish-evidence.json" | |
| evidence_checksum_asset="${evidence_asset}.sha256" | |
| release_manifest_asset="openclaw-${evidence_version}-release-manifest.json" | |
| release_manifest_checksum_asset="${release_manifest_asset}.sha256" | |
| closeout_asset="openclaw-${release_asset_version}-stable-main-closeout.json" | |
| closeout_checksum_asset="${closeout_asset}.sha256" | |
| closeout_dir="$RUNNER_TEMP/release-closeout-evidence" | |
| mkdir -p "$closeout_dir" | |
| gh_with_retry release download "$tag" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern "$closeout_asset" --pattern "$closeout_checksum_asset" --dir "$closeout_dir" || true | |
| closeout_json_path="$closeout_dir/$closeout_asset" | |
| closeout_checksum_path="$closeout_dir/$closeout_checksum_asset" | |
| repair_partial_closeout=false | |
| existing_closeout_full_release_validation_run_id="" | |
| existing_closeout_full_release_validation_run_attempt="" | |
| existing_closeout_release_publish_run_id="" | |
| if [[ -f "$closeout_json_path" && -f "$closeout_checksum_path" ]]; then | |
| expected_closeout_digest="$(awk 'NF { print $1; exit }' "$closeout_checksum_path")" | |
| actual_closeout_digest="$(sha256sum "$closeout_json_path" | awk '{print $1}')" | |
| if [[ ! "$expected_closeout_digest" =~ ^[0-9a-f]{64}$ || | |
| "$expected_closeout_digest" != "$actual_closeout_digest" ]]; then | |
| echo "Stable closeout evidence for $tag has an invalid checksum; refusing to repair it." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [[ -f "$closeout_checksum_path" && ! -f "$closeout_json_path" ]]; then | |
| echo "Stable closeout evidence for $tag has a checksum without its manifest; refusing to repair it." >&2 | |
| exit 1 | |
| fi | |
| if [[ -f "$closeout_json_path" ]]; then | |
| existing_closeout_tag="$(jq -r '.releaseTag // empty' "$closeout_json_path")" | |
| existing_closeout_version="$(jq -r '.releaseVersion // empty' "$closeout_json_path")" | |
| existing_closeout_schema_version="$(jq -r '.version // empty' "$closeout_json_path")" | |
| existing_closeout_release_tag_sha="$(jq -r '.releaseTagSha // empty' "$closeout_json_path")" | |
| existing_closeout_main_ref="$(jq -r '.mainSha // empty' "$closeout_json_path")" | |
| existing_closeout_full_release_validation_run_id="$(jq -r '.fullReleaseValidationRunId // empty' "$closeout_json_path")" | |
| existing_closeout_full_release_validation_run_attempt="$(jq -r '.fullReleaseValidationRunAttempt // empty' "$closeout_json_path")" | |
| existing_closeout_release_publish_run_id="$(jq -r '.releasePublishRunId // empty' "$closeout_json_path")" | |
| existing_closeout_rollback_drill_id="$(jq -r '.rollbackDrill.id // empty' "$closeout_json_path")" | |
| existing_closeout_rollback_drill_date="$(jq -r '.rollbackDrill.date // empty' "$closeout_json_path")" | |
| # Schema v1 never shipped as a release asset and cannot bind an immutable run attempt. | |
| if [[ "$existing_closeout_schema_version" != "2" || | |
| "$existing_closeout_tag" != "$tag" || | |
| "$existing_closeout_version" != "$tag_package_version" || | |
| ! "$existing_closeout_release_tag_sha" =~ ^[0-9a-f]{40}$ || | |
| ! "$existing_closeout_main_ref" =~ ^[0-9a-f]{40}$ || | |
| -z "$existing_closeout_full_release_validation_run_id" || | |
| ! "$existing_closeout_full_release_validation_run_attempt" =~ ^[1-9][0-9]*$ || | |
| -z "$existing_closeout_release_publish_run_id" || | |
| -z "$existing_closeout_rollback_drill_id" || | |
| ! "$existing_closeout_rollback_drill_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then | |
| echo "Stable closeout manifest for $tag is incomplete; refusing to repair it." >&2 | |
| exit 1 | |
| fi | |
| main_ref="$existing_closeout_main_ref" | |
| ROLLBACK_DRILL_ID="$existing_closeout_rollback_drill_id" | |
| ROLLBACK_DRILL_DATE="$existing_closeout_rollback_drill_date" | |
| repair_partial_closeout=true | |
| elif [[ "$EVENT_NAME" == "push" ]]; then | |
| main_version="$(jq -r '.version // empty' package.json)" | |
| if [[ "$main_version" != "$release_package_version" && | |
| "$main_version" != "$fallback_package_version" ]]; then | |
| echo "should_closeout=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| else | |
| main_ref="main" | |
| fi | |
| evidence_dir="$RUNNER_TEMP/release-postpublish-evidence" | |
| mkdir -p "$evidence_dir" | |
| gh_with_retry release download "$evidence_source_tag" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern "$evidence_asset" \ | |
| --pattern "$evidence_checksum_asset" \ | |
| --pattern "$release_manifest_asset" \ | |
| --pattern "$release_manifest_checksum_asset" \ | |
| --dir "$evidence_dir" || true | |
| evidence_path="$evidence_dir/$evidence_asset" | |
| evidence_checksum_path="$evidence_dir/$evidence_checksum_asset" | |
| release_manifest_path="$evidence_dir/$release_manifest_asset" | |
| release_manifest_checksum_path="$evidence_dir/$release_manifest_checksum_asset" | |
| if [[ ! -f "$evidence_path" || ! -f "$evidence_checksum_path" ]]; then | |
| if [[ "$EVENT_NAME" == "push" ]]; then | |
| echo "Stable closeout skipped: $evidence_source_tag predates immutable postpublish evidence." >&2 | |
| echo "should_closeout=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Stable closeout is required for $tag, but immutable postpublish evidence from $evidence_source_tag is missing." >&2 | |
| exit 1 | |
| fi | |
| if [[ ! -f "$release_manifest_path" || ! -f "$release_manifest_checksum_path" ]]; then | |
| echo "Stable closeout is required for $tag, but immutable Full Release Validation evidence from $evidence_source_tag is missing." >&2 | |
| exit 1 | |
| fi | |
| if ! ( | |
| cd "$evidence_dir" | |
| sha256sum --strict --status -c "$evidence_checksum_asset" | |
| sha256sum --strict --status -c "$release_manifest_checksum_asset" | |
| ); then | |
| echo "Release evidence checksum failed for $tag." >&2 | |
| exit 1 | |
| fi | |
| evidence_release_tag="$(jq -r '.releaseTag // empty' "$evidence_path")" | |
| if ! full_release_validation_run_json="$(jq -ce ' | |
| [.workflowRuns[]? | select(.label == "Full Release Validation")] | | |
| if length == 1 and | |
| (.[0].id | type == "string" and test("^[1-9][0-9]*$")) and | |
| ((.[0].runAttempt == null) or | |
| (.[0].runAttempt | type == "string" and test("^[1-9][0-9]*$"))) | |
| then {id: .[0].id, runAttempt: (.[0].runAttempt // "")} | |
| else empty | |
| end | |
| ' "$evidence_path")"; then | |
| echo "Stable closeout is required for $tag, but postpublish evidence does not bind exactly one Full Release Validation run." >&2 | |
| exit 1 | |
| fi | |
| if ! release_manifest_run_json="$(jq -ce ' | |
| if .workflowName == "Full Release Validation" and | |
| (.runId | type == "string" and test("^[1-9][0-9]*$")) and | |
| (.runAttempt | type == "string" and test("^[1-9][0-9]*$")) | |
| then {id: .runId, runAttempt: .runAttempt} | |
| else empty | |
| end | |
| ' "$release_manifest_path")"; then | |
| echo "Stable closeout release manifest does not bind a Full Release Validation run attempt." >&2 | |
| exit 1 | |
| fi | |
| evidence_full_release_validation_run_id="$(jq -r '.id' <<<"$full_release_validation_run_json")" | |
| evidence_full_release_validation_run_attempt="$(jq -r '.runAttempt' <<<"$full_release_validation_run_json")" | |
| full_release_validation_run_id="$(jq -r '.id' <<<"$release_manifest_run_json")" | |
| full_release_validation_run_attempt="$(jq -r '.runAttempt' <<<"$release_manifest_run_json")" | |
| release_publish_run_id="$(jq -r '.releasePublishRunId // empty' "$evidence_path")" | |
| if [[ "$evidence_release_tag" != "$evidence_source_tag" || | |
| "$evidence_full_release_validation_run_id" != "$full_release_validation_run_id" || | |
| ( -n "$evidence_full_release_validation_run_attempt" && | |
| "$evidence_full_release_validation_run_attempt" != "$full_release_validation_run_attempt" ) || | |
| -z "$release_publish_run_id" ]]; then | |
| echo "Stable closeout is required for $tag, but postpublish evidence does not bind $evidence_source_tag to exactly one Full Release Validation run and its Publish run." >&2 | |
| exit 1 | |
| fi | |
| if [[ -n "$existing_closeout_full_release_validation_run_id" && | |
| ( "$existing_closeout_full_release_validation_run_id" != "$full_release_validation_run_id" || | |
| "$existing_closeout_full_release_validation_run_attempt" != "$full_release_validation_run_attempt" || | |
| "$existing_closeout_release_publish_run_id" != "$release_publish_run_id" ) ]]; then | |
| echo "Stable closeout manifest for $tag does not match immutable postpublish evidence; refusing to accept it." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$EVENT_NAME" == "push" && -f "$closeout_checksum_path" ]]; then | |
| echo "Stable closeout already complete for $tag." | |
| echo "should_closeout=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [[ -z "$ROLLBACK_DRILL_ID" || -z "$ROLLBACK_DRILL_DATE" ]]; then | |
| if [[ "$EVENT_NAME" == "push" ]]; then | |
| echo "::warning::Stable closeout skipped: rollback drill repository variables are missing; manual dispatch remains required to complete closeout." | |
| echo "should_closeout=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Stable closeout requires repository variables RELEASE_ROLLBACK_DRILL_ID and RELEASE_ROLLBACK_DRILL_DATE, or explicit manual overrides." >&2 | |
| exit 1 | |
| fi | |
| { | |
| echo "full_release_validation_run_id=$full_release_validation_run_id" | |
| echo "full_release_validation_run_attempt=$full_release_validation_run_attempt" | |
| echo "release_publish_run_id=$release_publish_run_id" | |
| echo "rollback_drill_date=$ROLLBACK_DRILL_DATE" | |
| echo "rollback_drill_id=$ROLLBACK_DRILL_ID" | |
| echo "evidence_tag=$evidence_source_tag" | |
| echo "fallback_correction=$fallback_correction" | |
| echo "main_ref=$main_ref" | |
| echo "repair_partial_closeout=$repair_partial_closeout" | |
| echo "should_closeout=true" | |
| echo "tag=$tag" | |
| } >> "$GITHUB_OUTPUT" | |
| verify: | |
| name: Verify stable main closeout | |
| needs: resolve | |
| if: ${{ needs.resolve.outputs.should_closeout == 'true' }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout resolved main state | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| ref: ${{ needs.resolve.outputs.main_ref }} | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Checkout shipped release tag | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| ref: refs/tags/${{ needs.resolve.outputs.tag }} | |
| path: release-tag | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Checkout fallback evidence tag | |
| if: ${{ needs.resolve.outputs.fallback_correction == 'true' }} | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| ref: refs/tags/${{ needs.resolve.outputs.evidence_tag }} | |
| path: evidence-tag | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Bind fallback correction to the published package source | |
| if: ${{ needs.resolve.outputs.fallback_correction == 'true' }} | |
| run: | | |
| set -euo pipefail | |
| correction_sha="$(git -C "$GITHUB_WORKSPACE/release-tag" rev-parse HEAD)" | |
| evidence_sha="$(git -C "$GITHUB_WORKSPACE/evidence-tag" rev-parse HEAD)" | |
| if [[ "$correction_sha" != "$evidence_sha" ]]; then | |
| echo "Fallback correction ${{ needs.resolve.outputs.tag }} must point to the same source commit as ${{ needs.resolve.outputs.evidence_tag }} to reuse immutable package evidence." >&2 | |
| exit 1 | |
| fi | |
| - name: Install GitHub API backoff helper | |
| run: | | |
| cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH' | |
| gh_with_retry() { | |
| local attempt output status lower_output | |
| for attempt in 1 2 3 4 5; do | |
| if output="$(gh "$@" 2>&1)"; then | |
| printf '%s\n' "$output" | |
| return 0 | |
| fi | |
| status=$? | |
| lower_output="${output,,}" | |
| if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then | |
| printf '%s\n' "$output" >&2 | |
| return "$status" | |
| fi | |
| echo "::warning::GitHub API throttled stable closeout on attempt ${attempt}; retrying after backoff." >&2 | |
| sleep $((attempt * attempt * 5)) | |
| done | |
| printf '%s\n' "$output" >&2 | |
| return "$status" | |
| } | |
| BASH | |
| - name: Verify release workflow evidence | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EVIDENCE_TAG: ${{ needs.resolve.outputs.evidence_tag }} | |
| FULL_RELEASE_VALIDATION_RUN_ID: ${{ needs.resolve.outputs.full_release_validation_run_id }} | |
| FULL_RELEASE_VALIDATION_RUN_ATTEMPT: ${{ needs.resolve.outputs.full_release_validation_run_attempt }} | |
| RELEASE_PUBLISH_RUN_ID: ${{ needs.resolve.outputs.release_publish_run_id }} | |
| ALLOW_FAILED_PUBLISH_RECOVERY: ${{ github.event_name == 'workflow_dispatch' && inputs.allow_failed_publish_recovery && 'true' || 'false' }} | |
| run: | | |
| set -euo pipefail | |
| . "$RUNNER_TEMP/github-api-backoff.sh" | |
| gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${FULL_RELEASE_VALIDATION_RUN_ID}/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}" \ | |
| > "$RUNNER_TEMP/full-release-validation-run.json" | |
| node --input-type=module - "$RUNNER_TEMP/full-release-validation-run.json" <<'NODE' | |
| import { readFileSync } from "node:fs"; | |
| const run = JSON.parse(readFileSync(process.argv[2], "utf8")); | |
| for (const [key, expected] of [ | |
| ["name", "Full Release Validation"], | |
| ["event", "workflow_dispatch"], | |
| ["status", "completed"], | |
| ["conclusion", "success"], | |
| ]) { | |
| if (run[key] !== expected) { | |
| throw new Error(`Full Release Validation must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); | |
| } | |
| } | |
| if (String(run.run_attempt ?? "") !== process.env.FULL_RELEASE_VALIDATION_RUN_ATTEMPT) { | |
| throw new Error(`Full Release Validation run attempt mismatch: expected ${process.env.FULL_RELEASE_VALIDATION_RUN_ATTEMPT}, got ${run.run_attempt ?? "<missing>"}.`); | |
| } | |
| NODE | |
| gh_with_retry run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" \ | |
| --json workflowName,event,status,conclusion,headSha \ | |
| > "$RUNNER_TEMP/release-publish-run.json" | |
| node --input-type=module - "$RUNNER_TEMP/release-publish-run.json" <<'NODE' | |
| import { readFileSync } from "node:fs"; | |
| const run = JSON.parse(readFileSync(process.argv[2], "utf8")); | |
| for (const [key, expected] of [ | |
| ["workflowName", "OpenClaw Release Publish"], | |
| ["event", "workflow_dispatch"], | |
| ["status", "completed"], | |
| ]) { | |
| if (run[key] !== expected) { | |
| throw new Error(`OpenClaw Release Publish must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); | |
| } | |
| } | |
| const recoveryRequested = process.env.ALLOW_FAILED_PUBLISH_RECOVERY === "true"; | |
| if (recoveryRequested && run.conclusion !== "failure") { | |
| throw new Error( | |
| `Failed-publish recovery requires conclusion=failure, got ${run.conclusion ?? "<missing>"}.`, | |
| ); | |
| } | |
| const failedRecovery = recoveryRequested && run.conclusion === "failure"; | |
| if (run.conclusion !== "success" && !failedRecovery) { | |
| throw new Error( | |
| `OpenClaw Release Publish must have conclusion=success, got ${run.conclusion ?? "<missing>"}.`, | |
| ); | |
| } | |
| if (failedRecovery) { | |
| console.log("Accepting failed Release Publish parent under explicit complete-platform recovery."); | |
| } | |
| NODE | |
| if [[ "$ALLOW_FAILED_PUBLISH_RECOVERY" == "true" ]]; then | |
| parent_log="$RUNNER_TEMP/release-publish-run.log" | |
| gh_with_retry run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --log \ | |
| > "$parent_log" | |
| mapfile -t windows_node_run_ids < <( | |
| sed -nE '/Dispatched windows-node-release\.yml/ { s#.*https://github\.com/openclaw/openclaw/actions/runs/([1-9][0-9]*).*#\1#p; }' "$parent_log" | | |
| LC_ALL=C sort -u | |
| ) | |
| if [[ "${#windows_node_run_ids[@]}" != "1" ]]; then | |
| echo "Failed-publish recovery requires exactly one Windows Node Release run dispatched by the parent; found ${#windows_node_run_ids[@]}." >&2 | |
| exit 1 | |
| fi | |
| windows_node_run_id="${windows_node_run_ids[0]}" | |
| windows_node_run_json="$RUNNER_TEMP/windows-node-release-run.json" | |
| gh_with_retry run view "$windows_node_run_id" --repo "$GITHUB_REPOSITORY" \ | |
| --json workflowName,event,status,conclusion,headSha,url,jobs \ | |
| > "$windows_node_run_json" | |
| parent_head_sha="$(jq -r '.headSha // empty' "$RUNNER_TEMP/release-publish-run.json")" | |
| PARENT_HEAD_SHA="$parent_head_sha" \ | |
| node --input-type=module -e ' | |
| import { readFileSync } from "node:fs"; | |
| const run = JSON.parse(readFileSync(process.argv[1], "utf8")); | |
| for (const [key, expected] of [ | |
| ["workflowName", "Windows Node Release"], | |
| ["event", "workflow_dispatch"], | |
| ["status", "completed"], | |
| ["conclusion", "success"], | |
| ["headSha", process.env.PARENT_HEAD_SHA], | |
| ]) { | |
| if (run[key] !== expected) { | |
| throw new Error(`Windows Node Release must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); | |
| } | |
| } | |
| const jobs = (run.jobs ?? []).filter( | |
| (job) => job.name === "Promote signed Windows installers" && job.conclusion === "success", | |
| ); | |
| if (jobs.length !== 1) { | |
| throw new Error("Windows Node Release must contain one successful signed-installer promotion job."); | |
| } | |
| for (const name of [ | |
| "Validate inputs", | |
| "Verify Authenticode signatures", | |
| "Upload to OpenClaw release", | |
| "Verify promoted release asset contract", | |
| ]) { | |
| const steps = (jobs[0].steps ?? []).filter( | |
| (step) => step.name === name && step.conclusion === "success", | |
| ); | |
| if (steps.length !== 1) { | |
| throw new Error(`Windows Node Release is missing successful step: ${name}.`); | |
| } | |
| } | |
| ' "$windows_node_run_json" | |
| windows_node_log="$RUNNER_TEMP/windows-node-release-run.log" | |
| gh_with_retry run view "$windows_node_run_id" --repo "$GITHUB_REPOSITORY" --log \ | |
| > "$windows_node_log" | |
| windows_node_installer_digests="$( | |
| node --input-type=module -e ' | |
| import { readFileSync } from "node:fs"; | |
| const log = readFileSync(process.argv[1], "utf8"); | |
| const marker = "EXPECTED_INSTALLER_DIGESTS:"; | |
| const names = [ | |
| "OpenClawCompanion-Setup-arm64.exe", | |
| "OpenClawCompanion-Setup-x64.exe", | |
| ]; | |
| const contracts = new Set(); | |
| for (const line of log.split(/\r?\n/u)) { | |
| const markerIndex = line.indexOf(marker); | |
| if (markerIndex === -1) continue; | |
| const candidate = line.slice(markerIndex + marker.length).trim(); | |
| let parsed; | |
| try { | |
| parsed = JSON.parse(candidate); | |
| } catch { | |
| continue; | |
| } | |
| const keys = Object.keys(parsed).toSorted((left, right) => left.localeCompare(right)); | |
| if ( | |
| JSON.stringify(keys) !== JSON.stringify(names) || | |
| !names.every((name) => /^sha256:[0-9a-f]{64}$/u.test(parsed[name] ?? "")) | |
| ) { | |
| continue; | |
| } | |
| contracts.add(JSON.stringify(Object.fromEntries(names.map((name) => [name, parsed[name]])))); | |
| } | |
| if (contracts.size !== 1) { | |
| throw new Error(`Windows Node Release logs must contain exactly one candidate-approved digest contract, got ${contracts.size}.`); | |
| } | |
| process.stdout.write([...contracts][0]); | |
| ' "$windows_node_log" | |
| )" | |
| { | |
| echo "WINDOWS_NODE_RELEASE_RUN_ID=$windows_node_run_id" | |
| echo "WINDOWS_NODE_INSTALLER_DIGESTS=$windows_node_installer_digests" | |
| } >> "$GITHUB_ENV" | |
| fi | |
| manifest_dir="$RUNNER_TEMP/full-release-validation-manifest" | |
| rm -rf "$manifest_dir" | |
| mkdir -p "$manifest_dir" | |
| evidence_version="${EVIDENCE_TAG#v}" | |
| manifest_asset="openclaw-${evidence_version}-release-manifest.json" | |
| manifest_checksum_asset="${manifest_asset}.sha256" | |
| gh_with_retry release download "$EVIDENCE_TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern "$manifest_asset" \ | |
| --pattern "$manifest_checksum_asset" \ | |
| --dir "$manifest_dir" | |
| ( | |
| cd "$manifest_dir" | |
| sha256sum --strict --status -c "$manifest_checksum_asset" | |
| ) || { | |
| echo "Full Release Validation manifest checksum failed for $EVIDENCE_TAG." >&2 | |
| exit 1 | |
| } | |
| tag_sha="$(git -C "$GITHUB_WORKSPACE/release-tag" rev-parse HEAD)" | |
| jq -e \ | |
| --arg tag_sha "$tag_sha" \ | |
| --arg run_id "$FULL_RELEASE_VALIDATION_RUN_ID" \ | |
| --arg run_attempt "$FULL_RELEASE_VALIDATION_RUN_ATTEMPT" ' | |
| .workflowName == "Full Release Validation" and | |
| .runId == $run_id and | |
| .runAttempt == $run_attempt and | |
| .targetSha == $tag_sha and | |
| .rerunGroup == "all" and | |
| .runReleaseSoak == "true" and | |
| .controls.performanceBlocking == true and | |
| .childRuns.productPerformance.conclusion == "success" | |
| ' "$manifest_dir/$manifest_asset" >/dev/null || { | |
| echo "Full Release Validation manifest does not contain the required stable release controls." >&2 | |
| exit 1 | |
| } | |
| - name: Verify stable state and write closeout manifest | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ needs.resolve.outputs.tag }} | |
| FULL_RELEASE_VALIDATION_RUN_ID: ${{ needs.resolve.outputs.full_release_validation_run_id }} | |
| FULL_RELEASE_VALIDATION_RUN_ATTEMPT: ${{ needs.resolve.outputs.full_release_validation_run_attempt }} | |
| RELEASE_PUBLISH_RUN_ID: ${{ needs.resolve.outputs.release_publish_run_id }} | |
| ROLLBACK_DRILL_ID: ${{ needs.resolve.outputs.rollback_drill_id }} | |
| ROLLBACK_DRILL_DATE: ${{ needs.resolve.outputs.rollback_drill_date }} | |
| REPAIR_PARTIAL_CLOSEOUT: ${{ needs.resolve.outputs.repair_partial_closeout }} | |
| ALLOW_FAILED_PUBLISH_RECOVERY: ${{ github.event_name == 'workflow_dispatch' && inputs.allow_failed_publish_recovery && 'true' || 'false' }} | |
| CLOSEOUT_DIR: ${{ runner.temp }}/openclaw-stable-main-closeout | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$CLOSEOUT_DIR" | |
| . "$RUNNER_TEMP/github-api-backoff.sh" | |
| gh_with_retry release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --json tagName,isDraft,isPrerelease,assets \ | |
| > "$CLOSEOUT_DIR/github-release.json" | |
| if [[ "$ALLOW_FAILED_PUBLISH_RECOVERY" == "true" ]]; then | |
| recovery_dir="$CLOSEOUT_DIR/platform-recovery" | |
| mkdir -p "$recovery_dir" | |
| gh_with_retry release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern OpenClaw-Android.apk \ | |
| --pattern OpenClaw-Android-SHA256SUMS.txt \ | |
| --pattern OpenClawCompanion-Setup-arm64.exe \ | |
| --pattern OpenClawCompanion-Setup-x64.exe \ | |
| --pattern OpenClawCompanion-SHA256SUMS.txt \ | |
| --dir "$recovery_dir" | |
| ( | |
| cd "$recovery_dir" | |
| verify_checksum_manifest() { | |
| local manifest="$1" actual expected normalized | |
| shift | |
| normalized="${manifest}.normalized" | |
| sed 's/\r$//' "$manifest" > "$normalized" | |
| expected="$(printf '%s\n' "$@" | LC_ALL=C sort)" | |
| actual="$(awk 'NF { name=$2; sub(/^\*/, "", name); print name }' "$normalized" | LC_ALL=C sort)" | |
| if [[ "$actual" != "$expected" ]]; then | |
| echo "$manifest must list exactly: $*" >&2 | |
| exit 1 | |
| fi | |
| sha256sum --strict --check "$normalized" | |
| rm -f "$normalized" | |
| } | |
| verify_checksum_manifest OpenClaw-Android-SHA256SUMS.txt \ | |
| OpenClaw-Android.apk | |
| verify_checksum_manifest OpenClawCompanion-SHA256SUMS.txt \ | |
| OpenClawCompanion-Setup-arm64.exe \ | |
| OpenClawCompanion-Setup-x64.exe | |
| ) | |
| gh_with_retry attestation verify "$recovery_dir/OpenClaw-Android.apk" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/android-release.yml" \ | |
| --source-ref "refs/tags/$RELEASE_TAG" \ | |
| --deny-self-hosted-runners | |
| fi | |
| node scripts/verify-stable-main-closeout.mjs \ | |
| --tag "$RELEASE_TAG" \ | |
| --main-dir "$GITHUB_WORKSPACE" \ | |
| --tag-dir "$GITHUB_WORKSPACE/release-tag" \ | |
| --release-json "$CLOSEOUT_DIR/github-release.json" \ | |
| --full-release-validation-run-id "$FULL_RELEASE_VALIDATION_RUN_ID" \ | |
| --full-release-validation-run-attempt "$FULL_RELEASE_VALIDATION_RUN_ATTEMPT" \ | |
| --release-publish-run-id "$RELEASE_PUBLISH_RUN_ID" \ | |
| --rollback-drill-id "$ROLLBACK_DRILL_ID" \ | |
| --rollback-drill-date "$ROLLBACK_DRILL_DATE" \ | |
| --allow-stale-rollback-drill "$REPAIR_PARTIAL_CLOSEOUT" \ | |
| --require-complete-platform-assets "$ALLOW_FAILED_PUBLISH_RECOVERY" \ | |
| --windows-node-release-run-id "${WINDOWS_NODE_RELEASE_RUN_ID:-}" \ | |
| --windows-node-installer-digests "${WINDOWS_NODE_INSTALLER_DIGESTS:-}" \ | |
| --output "$CLOSEOUT_DIR/stable-main-closeout.json" | |
| release_version="${RELEASE_TAG#v}" | |
| sha256sum "$CLOSEOUT_DIR/stable-main-closeout.json" | awk -v asset="openclaw-${release_version}-stable-main-closeout.json" \ | |
| '{print $1 " " asset}' \ | |
| > "$CLOSEOUT_DIR/stable-main-closeout.json.sha256" | |
| - name: Attach immutable closeout evidence | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ needs.resolve.outputs.tag }} | |
| CLOSEOUT_DIR: ${{ runner.temp }}/openclaw-stable-main-closeout | |
| run: | | |
| set -euo pipefail | |
| . "$RUNNER_TEMP/github-api-backoff.sh" | |
| release_version="${RELEASE_TAG#v}" | |
| attach_or_verify() { | |
| local source_path="$1" | |
| local asset_name="$2" | |
| local existing_dir="$CLOSEOUT_DIR/existing-${asset_name}" | |
| mkdir -p "$existing_dir" | |
| gh_with_retry release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --pattern "$asset_name" --dir "$existing_dir" || true | |
| if [[ -f "$existing_dir/$asset_name" ]]; then | |
| cmp --silent "$source_path" "$existing_dir/$asset_name" || { | |
| echo "Existing release asset $asset_name differs from closeout evidence." >&2 | |
| exit 1 | |
| } | |
| return | |
| fi | |
| # The #suffix is only a display label. Stage the desired basename so | |
| # GitHub stores the versioned asset name used by repair lookups. | |
| cp -- "$source_path" "$existing_dir/$asset_name" | |
| gh_with_retry release upload "$RELEASE_TAG" \ | |
| "$existing_dir/$asset_name#$asset_name" --repo "$GITHUB_REPOSITORY" | |
| } | |
| attach_or_verify \ | |
| "$CLOSEOUT_DIR/stable-main-closeout.json" \ | |
| "openclaw-${release_version}-stable-main-closeout.json" | |
| attach_or_verify \ | |
| "$CLOSEOUT_DIR/stable-main-closeout.json.sha256" \ | |
| "openclaw-${release_version}-stable-main-closeout.json.sha256" | |
| - name: Upload closeout workflow evidence | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: openclaw-stable-main-closeout-${{ needs.resolve.outputs.tag }} | |
| path: ${{ runner.temp }}/openclaw-stable-main-closeout | |
| if-no-files-found: error |