Skip to content

Commit c4d8e0b

Browse files
committed
ci: harden release validation flow
1 parent a535978 commit c4d8e0b

8 files changed

Lines changed: 385 additions & 31 deletions

.github/workflows/openclaw-npm-release.yml

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ jobs:
191191
id: packed_tarball
192192
env:
193193
OPENCLAW_PREPACK_PREPARED: "1"
194-
RELEASE_TAG: ${{ inputs.tag }}
194+
RELEASE_REF: ${{ inputs.tag }}
195195
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
196196
DEPENDENCY_EVIDENCE_DIR: ${{ steps.dependency_evidence.outputs.dir }}
197197
run: |
@@ -259,6 +259,11 @@ jobs:
259259
fi
260260
RELEASE_SHA="$(git rev-parse HEAD)"
261261
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
262+
if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
263+
RELEASE_TAG="v${PACKAGE_VERSION}"
264+
else
265+
RELEASE_TAG="${RELEASE_REF}"
266+
fi
262267
TARBALL_NAME="$(basename "$PACK_PATH")"
263268
TARBALL_SHA256="$(sha256sum "$PACK_PATH" | awk '{print $1}')"
264269
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-npm-preflight"
@@ -290,6 +295,7 @@ jobs:
290295
);
291296
NODE
292297
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
298+
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
293299
294300
- name: Verify prepared npm tarball install
295301
env:
@@ -312,13 +318,29 @@ jobs:
312318
path: ${{ steps.dependency_evidence.outputs.dir }}
313319
if-no-files-found: error
314320

321+
- name: Upload dependency release evidence tag alias
322+
if: ${{ steps.packed_tarball.outputs.release_tag != inputs.tag }}
323+
uses: actions/upload-artifact@v7
324+
with:
325+
name: openclaw-release-dependency-evidence-${{ steps.packed_tarball.outputs.release_tag }}
326+
path: ${{ steps.dependency_evidence.outputs.dir }}
327+
if-no-files-found: error
328+
315329
- name: Upload prepared npm publish bundle
316330
uses: actions/upload-artifact@v7
317331
with:
318332
name: openclaw-npm-preflight-${{ inputs.tag }}
319333
path: ${{ steps.packed_tarball.outputs.dir }}
320334
if-no-files-found: error
321335

336+
- name: Upload prepared npm publish bundle tag alias
337+
if: ${{ steps.packed_tarball.outputs.release_tag != inputs.tag }}
338+
uses: actions/upload-artifact@v7
339+
with:
340+
name: openclaw-npm-preflight-${{ steps.packed_tarball.outputs.release_tag }}
341+
path: ${{ steps.packed_tarball.outputs.dir }}
342+
if-no-files-found: error
343+
322344
validate_publish_request:
323345
if: ${{ !inputs.preflight_only }}
324346
runs-on: ubuntu-latest
@@ -427,13 +449,45 @@ jobs:
427449
printf '%s' "$RUN_JSON" | node -e 'const fs = require("node:fs"); const run = JSON.parse(fs.readFileSync(0, "utf8")); const checks = [["workflowName", "Full Release Validation"], ["headBranch", process.env.EXPECTED_WORKFLOW_BRANCH], ["event", "workflow_dispatch"], ["status", "completed"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using full release validation run ${process.env.FULL_RELEASE_VALIDATION_RUN_ID}: ${run.url}`);'
428450
429451
- name: Download prepared npm tarball
430-
uses: actions/download-artifact@v8
431-
with:
432-
name: openclaw-npm-preflight-${{ inputs.tag }}
433-
path: preflight-tarball
434-
repository: ${{ github.repository }}
435-
run-id: ${{ inputs.preflight_run_id }}
436-
github-token: ${{ github.token }}
452+
env:
453+
GH_TOKEN: ${{ github.token }}
454+
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
455+
RELEASE_TAG: ${{ inputs.tag }}
456+
run: |
457+
set -euo pipefail
458+
459+
download_preflight_artifact() {
460+
local preferred_name fallback_name
461+
preferred_name="openclaw-npm-preflight-${RELEASE_TAG}"
462+
rm -rf preflight-tarball
463+
mkdir -p preflight-tarball
464+
if gh run download "${PREFLIGHT_RUN_ID}" \
465+
--repo "${GITHUB_REPOSITORY}" \
466+
--name "${preferred_name}" \
467+
--dir preflight-tarball; then
468+
echo "Downloaded ${preferred_name}."
469+
return 0
470+
fi
471+
472+
echo "::warning::${preferred_name} not found; checking run artifacts for a single compatible preflight artifact."
473+
mapfile -t matches < <(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/runs/${PREFLIGHT_RUN_ID}/artifacts" \
474+
--jq '.artifacts[] | select(.expired != true) | .name' |
475+
grep '^openclaw-npm-preflight-' || true)
476+
if [[ "${#matches[@]}" != "1" ]]; then
477+
echo "Expected ${preferred_name}, or exactly one openclaw-npm-preflight-* fallback artifact in run ${PREFLIGHT_RUN_ID}." >&2
478+
printf 'Available preflight candidates:\n' >&2
479+
printf -- '- %s\n' "${matches[@]:-<none>}" >&2
480+
exit 1
481+
fi
482+
fallback_name="${matches[0]}"
483+
gh run download "${PREFLIGHT_RUN_ID}" \
484+
--repo "${GITHUB_REPOSITORY}" \
485+
--name "${fallback_name}" \
486+
--dir preflight-tarball
487+
echo "Downloaded fallback preflight artifact ${fallback_name}."
488+
}
489+
490+
download_preflight_artifact
437491
438492
- name: Download full release validation manifest
439493
uses: actions/download-artifact@v8

.github/workflows/openclaw-release-publish.yml

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ jobs:
7676
timeout-minutes: 20
7777
outputs:
7878
sha: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
79+
preflight_artifact_name: ${{ steps.preflight_artifact.outputs.name }}
7980
steps:
8081
- name: Validate inputs
8182
env:
@@ -131,14 +132,43 @@ jobs:
131132
esac
132133
133134
- name: Download OpenClaw npm preflight manifest
135+
id: preflight_artifact
134136
if: ${{ inputs.publish_openclaw_npm }}
135-
uses: actions/download-artifact@v8
136-
with:
137-
name: openclaw-npm-preflight-${{ inputs.tag }}
138-
path: ${{ runner.temp }}/openclaw-npm-preflight-manifest
139-
repository: ${{ github.repository }}
140-
run-id: ${{ inputs.preflight_run_id }}
141-
github-token: ${{ github.token }}
137+
env:
138+
GH_TOKEN: ${{ github.token }}
139+
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
140+
RELEASE_TAG: ${{ inputs.tag }}
141+
run: |
142+
set -euo pipefail
143+
144+
preferred_name="openclaw-npm-preflight-${RELEASE_TAG}"
145+
preflight_dir="${RUNNER_TEMP}/openclaw-npm-preflight-manifest"
146+
rm -rf "${preflight_dir}"
147+
mkdir -p "${preflight_dir}"
148+
if gh run download "${PREFLIGHT_RUN_ID}" \
149+
--repo "${GITHUB_REPOSITORY}" \
150+
--name "${preferred_name}" \
151+
--dir "${preflight_dir}"; then
152+
echo "name=${preferred_name}" >> "$GITHUB_OUTPUT"
153+
exit 0
154+
fi
155+
156+
echo "::warning::${preferred_name} not found; checking run artifacts for a single compatible preflight artifact."
157+
mapfile -t matches < <(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/runs/${PREFLIGHT_RUN_ID}/artifacts" \
158+
--jq '.artifacts[] | select(.expired != true) | .name' |
159+
grep '^openclaw-npm-preflight-' || true)
160+
if [[ "${#matches[@]}" != "1" ]]; then
161+
echo "Expected ${preferred_name}, or exactly one openclaw-npm-preflight-* fallback artifact in run ${PREFLIGHT_RUN_ID}." >&2
162+
printf 'Available preflight candidates:\n' >&2
163+
printf -- '- %s\n' "${matches[@]:-<none>}" >&2
164+
exit 1
165+
fi
166+
fallback_name="${matches[0]}"
167+
gh run download "${PREFLIGHT_RUN_ID}" \
168+
--repo "${GITHUB_REPOSITORY}" \
169+
--name "${fallback_name}" \
170+
--dir "${preflight_dir}"
171+
echo "name=${fallback_name}" >> "$GITHUB_OUTPUT"
142172
143173
- name: Download full release validation manifest
144174
if: ${{ inputs.publish_openclaw_npm }}
@@ -306,6 +336,7 @@ jobs:
306336
PLUGINS: ${{ inputs.plugins }}
307337
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
308338
WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }}
339+
PREFLIGHT_ARTIFACT_NAME: ${{ needs.resolve_release_target.outputs.preflight_artifact_name }}
309340
run: |
310341
set -euo pipefail
311342
@@ -314,7 +345,10 @@ jobs:
314345
shift
315346
316347
local before_json dispatch_output run_id
317-
before_json="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
348+
before_json="$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
349+
-F event=workflow_dispatch \
350+
-F per_page=100 \
351+
--jq '[.workflow_runs[].id]')"
318352
319353
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
320354
printf '%s\n' "$dispatch_output" >&2
@@ -327,8 +361,10 @@ jobs:
327361
if [[ -z "$run_id" ]]; then
328362
for _ in $(seq 1 60); do
329363
run_id="$(
330-
BEFORE_IDS="$before_json" gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
331-
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
364+
BEFORE_IDS="$before_json" gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
365+
-F event=workflow_dispatch \
366+
-F per_page=50 \
367+
--jq '.workflow_runs | map({databaseId:.id, createdAt:.created_at}) | map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
332368
)"
333369
if [[ -n "$run_id" ]]; then
334370
break
@@ -349,6 +385,23 @@ jobs:
349385
printf '%s\n' "${run_id}"
350386
}
351387
388+
print_pending_deployments() {
389+
local workflow="$1"
390+
local run_id="$2"
391+
local pending_json
392+
393+
pending_json="$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments" 2>/dev/null || true)"
394+
if [[ -z "${pending_json}" ]] || ! printf '%s' "${pending_json}" | jq -e 'length > 0' >/dev/null 2>&1; then
395+
return 0
396+
fi
397+
398+
echo "${workflow} pending environment approval:"
399+
while IFS=$'\t' read -r env_id env_name can_approve; do
400+
echo "- env=${env_name} canApprove=${can_approve}"
401+
echo " approve: gh api -X POST repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments -F 'environment_ids[]=${env_id}' -f state=approved -f comment='Approve release gate'"
402+
done < <(printf '%s' "${pending_json}" | jq -r '.[] | [.environment.id, .environment.name, .current_user_can_approve] | @tsv')
403+
}
404+
352405
wait_for_run() {
353406
local workflow="$1"
354407
local run_id="$2"
@@ -366,6 +419,7 @@ jobs:
366419
state="${status}:${updated_at}"
367420
if [[ "$state" != "$last_state" ]]; then
368421
echo "${workflow} still ${status} (updated ${updated_at}): ${url}"
422+
print_pending_deployments "${workflow}" "${run_id}"
369423
last_state="$state"
370424
fi
371425
sleep 30
@@ -466,17 +520,18 @@ jobs:
466520
}
467521
468522
upload_dependency_evidence_release_asset() {
469-
local release_version download_dir asset_path asset_name
523+
local release_version download_dir asset_path asset_name artifact_name
470524
release_version="${RELEASE_TAG#v}"
471525
download_dir="${RUNNER_TEMP}/openclaw-release-dependency-evidence-asset"
472526
asset_name="openclaw-${release_version}-dependency-evidence.zip"
473527
asset_path="${RUNNER_TEMP}/${asset_name}"
528+
artifact_name="${PREFLIGHT_ARTIFACT_NAME:-openclaw-npm-preflight-${RELEASE_TAG}}"
474529
475530
rm -rf "${download_dir}" "${asset_path}"
476531
mkdir -p "${download_dir}"
477532
gh run download "${PREFLIGHT_RUN_ID}" \
478533
--repo "${GITHUB_REPOSITORY}" \
479-
--name "openclaw-npm-preflight-${RELEASE_TAG}" \
534+
--name "${artifact_name}" \
480535
--dir "${download_dir}"
481536
482537
if [[ ! -d "${download_dir}/dependency-evidence" ]]; then

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,6 +1553,7 @@
15531553
"qa:otel:smoke": "node --import tsx scripts/qa-otel-smoke.ts",
15541554
"qa:telegram-user:crabbox": "node --import tsx scripts/e2e/telegram-user-crabbox-proof.ts",
15551555
"release-metadata:check": "node scripts/check-release-metadata-only.mjs",
1556+
"release:beta": "node scripts/release-candidate-checklist.mjs",
15561557
"release:beta-smoke": "node --import tsx scripts/release-beta-smoke.ts",
15571558
"release:candidate": "node scripts/release-candidate-checklist.mjs",
15581559
"release:check": "pnpm release:generated:check && node --import tsx scripts/release-check.ts",

0 commit comments

Comments
 (0)