From 658240de747a73ad42775275014ded4d1a3255c6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 02:02:25 +0100 Subject: [PATCH] ci: add full release validation workflow --- .github/workflows/ci.yml | 43 ++- .github/workflows/full-release-validation.yml | 339 ++++++++++++++++++ .../openclaw-live-and-e2e-checks-reusable.yml | 25 +- .github/workflows/openclaw-release-checks.yml | 33 +- docs/ci.md | 15 +- docs/reference/RELEASING.md | 48 +-- 6 files changed, 432 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/full-release-validation.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89823c371cd..00117a2725b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,12 @@ name: CI on: workflow_dispatch: + inputs: + target_ref: + description: Optional branch, tag, or full commit SHA to validate instead of the workflow ref + required: false + default: "" + type: string push: branches: [main] paths-ignore: @@ -30,6 +36,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 20 outputs: + checkout_sha: ${{ steps.checkout_ref.outputs.sha }} docs_only: ${{ steps.manifest.outputs.docs_only }} docs_changed: ${{ steps.manifest.outputs.docs_changed }} run_node: ${{ steps.manifest.outputs.run_node }} @@ -66,11 +73,16 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + ref: ${{ inputs.target_ref || github.sha }} fetch-depth: 1 fetch-tags: false persist-credentials: false submodules: false + - name: Resolve checkout SHA + id: checkout_ref + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Ensure preflight base commit if: github.event_name != 'workflow_dispatch' uses: ./.github/actions/ensure-base-commit @@ -302,12 +314,14 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + ref: ${{ inputs.target_ref || github.sha }} fetch-depth: 1 fetch-tags: false persist-credentials: false submodules: false - name: Ensure security base commit + if: github.event_name != 'workflow_dispatch' uses: ./.github/actions/ensure-base-commit with: base-sha: ${{ github.event_name == 'push' && github.event.before || github.event.pull_request.base.sha }} @@ -391,6 +405,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + ref: ${{ inputs.target_ref || github.sha }} fetch-depth: 1 fetch-tags: false persist-credentials: false @@ -453,7 +468,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -525,7 +540,7 @@ jobs: path: | dist/ dist-runtime/ - key: ${{ runner.os }}-dist-build-${{ github.sha }} + key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_sha }} - name: Pack built runtime artifacts run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime @@ -654,7 +669,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -749,7 +764,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -852,7 +867,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -920,7 +935,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -1040,7 +1055,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -1120,7 +1135,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -1307,7 +1322,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -1439,7 +1454,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -1637,7 +1652,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -1700,6 +1715,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + ref: ${{ needs.preflight.outputs.checkout_sha }} persist-credentials: false submodules: false @@ -1742,6 +1758,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + ref: ${{ needs.preflight.outputs.checkout_sha }} persist-credentials: false submodules: false @@ -1846,6 +1863,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + ref: ${{ needs.preflight.outputs.checkout_sha }} persist-credentials: false submodules: false @@ -1886,6 +1904,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: + ref: ${{ needs.preflight.outputs.checkout_sha }} persist-credentials: false submodules: false @@ -1986,7 +2005,7 @@ jobs: shell: bash env: CHECKOUT_REPO: ${{ github.repository }} - CHECKOUT_SHA: ${{ github.sha }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_sha }} CHECKOUT_TOKEN: ${{ github.token }} run: | set -euo pipefail diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml new file mode 100644 index 00000000000..fe2f90c8dee --- /dev/null +++ b/.github/workflows/full-release-validation.yml @@ -0,0 +1,339 @@ +name: Full Release Validation + +on: + workflow_dispatch: + inputs: + ref: + description: Branch, tag, or full commit SHA to validate + required: true + default: main + type: string + workflow_ref: + description: Trusted workflow ref used to run child workflows + required: false + default: main + 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 + npm_telegram_package_spec: + description: Optional published package spec for the post-publish Telegram E2E lane + required: false + default: "" + type: string + npm_telegram_provider_mode: + description: Provider mode for the optional post-publish Telegram E2E lane + required: false + default: mock-openai + type: choice + options: + - mock-openai + - live-frontier + npm_telegram_scenario: + description: Optional comma-separated Telegram scenario ids for the post-publish lane + required: false + default: "" + type: string + +permissions: + actions: write + contents: read + +concurrency: + group: full-release-validation-${{ inputs.ref }} + cancel-in-progress: false + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + +jobs: + resolve_target: + name: Resolve target ref + runs-on: ubuntu-24.04 + timeout-minutes: 10 + outputs: + sha: ${{ steps.resolve.outputs.sha }} + steps: + - name: Checkout target ref + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: false + + - name: Resolve target SHA + id: resolve + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Summarize target + env: + TARGET_REF: ${{ inputs.ref }} + TARGET_SHA: ${{ steps.resolve.outputs.sha }} + WORKFLOW_REF: ${{ inputs.workflow_ref }} + NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} + run: | + { + echo "## Full release validation" + echo + echo "- Target ref: \`${TARGET_REF}\`" + echo "- Target SHA: \`${TARGET_SHA}\`" + echo "- Child workflow ref: \`${WORKFLOW_REF}\`" + echo "- Normal CI: \`CI\` with \`target_ref=${TARGET_REF}\`" + echo "- Release/live/Docker/QA: \`OpenClaw Release Checks\`" + if [[ -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then + echo "- Post-publish Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`" + else + echo "- Post-publish Telegram E2E: skipped because no published package spec was provided" + fi + } >> "$GITHUB_STEP_SUMMARY" + + normal_ci: + name: Run normal full CI + needs: [resolve_target] + runs-on: ubuntu-24.04 + timeout-minutes: 240 + steps: + - name: Dispatch and monitor CI + env: + GH_TOKEN: ${{ github.token }} + TARGET_REF: ${{ inputs.ref }} + TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} + WORKFLOW_REF: ${{ inputs.workflow_ref }} + run: | + set -euo pipefail + + dispatch_and_wait() { + local workflow="$1" + local workflow_ref="$2" + shift 2 + + local before_json run_id status conclusion url + before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')" + + gh workflow run "$workflow" --ref "$workflow_ref" "$@" + + for _ in $(seq 1 60); do + run_id="$( + BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \ + --jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty' + )" + if [[ -n "$run_id" ]]; then + break + fi + sleep 5 + done + + if [[ -z "${run_id:-}" ]]; then + echo "Could not find dispatched run for ${workflow}." >&2 + exit 1 + fi + + echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" + + while true; do + status="$(gh run view "$run_id" --json status --jq '.status')" + if [[ "$status" == "completed" ]]; then + break + fi + sleep 30 + done + + conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')" + url="$(gh run view "$run_id" --json url --jq '.url')" + echo "${workflow} finished with ${conclusion}: ${url}" + if [[ "$conclusion" != "success" ]]; then + gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' + exit 1 + fi + } + + { + echo "### Normal CI" + echo + echo "- Target ref: \`${TARGET_REF}\`" + echo "- Target SHA: \`${TARGET_SHA}\`" + } >> "$GITHUB_STEP_SUMMARY" + + dispatch_and_wait ci.yml "$WORKFLOW_REF" -f target_ref="$TARGET_REF" + + release_checks: + name: Run release/live/Docker/QA validation + needs: [resolve_target] + runs-on: ubuntu-24.04 + timeout-minutes: 720 + steps: + - name: Dispatch and monitor release checks + env: + GH_TOKEN: ${{ github.token }} + TARGET_REF: ${{ inputs.ref }} + TARGET_SHA: ${{ needs.resolve_target.outputs.sha }} + WORKFLOW_REF: ${{ inputs.workflow_ref }} + PROVIDER: ${{ inputs.provider }} + MODE: ${{ inputs.mode }} + run: | + set -euo pipefail + + dispatch_and_wait() { + local workflow="$1" + local workflow_ref="$2" + shift 2 + + local before_json run_id status conclusion url + before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')" + + gh workflow run "$workflow" --ref "$workflow_ref" "$@" + + for _ in $(seq 1 60); do + run_id="$( + BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \ + --jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty' + )" + if [[ -n "$run_id" ]]; then + break + fi + sleep 5 + done + + if [[ -z "${run_id:-}" ]]; then + echo "Could not find dispatched run for ${workflow}." >&2 + exit 1 + fi + + echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" + + while true; do + status="$(gh run view "$run_id" --json status --jq '.status')" + if [[ "$status" == "completed" ]]; then + break + fi + sleep 60 + done + + conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')" + url="$(gh run view "$run_id" --json url --jq '.url')" + echo "${workflow} finished with ${conclusion}: ${url}" + if [[ "$conclusion" != "success" ]]; then + gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' + exit 1 + fi + } + + { + 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}\`" + } >> "$GITHUB_STEP_SUMMARY" + + dispatch_and_wait openclaw-release-checks.yml "$WORKFLOW_REF" \ + -f ref="$TARGET_REF" \ + -f provider="$PROVIDER" \ + -f mode="$MODE" + + npm_telegram: + name: Run post-publish Telegram E2E + needs: [resolve_target] + if: inputs.npm_telegram_package_spec != '' + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - name: Dispatch and monitor npm Telegram E2E + env: + GH_TOKEN: ${{ github.token }} + WORKFLOW_REF: ${{ inputs.workflow_ref }} + PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} + PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }} + SCENARIO: ${{ inputs.npm_telegram_scenario }} + run: | + set -euo pipefail + + before_json="$(gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')" + + args=(-f package_spec="$PACKAGE_SPEC" -f provider_mode="$PROVIDER_MODE") + if [[ -n "${SCENARIO// }" ]]; then + args+=(-f scenario="$SCENARIO") + fi + + gh workflow run npm-telegram-beta-e2e.yml --ref "$WORKFLOW_REF" "${args[@]}" + + run_id="" + for _ in $(seq 1 60); do + run_id="$( + BEFORE_IDS="$before_json" gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 50 --json databaseId,createdAt \ + --jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty' + )" + if [[ -n "$run_id" ]]; then + break + fi + sleep 5 + done + + if [[ -z "$run_id" ]]; then + echo "Could not find dispatched run for npm-telegram-beta-e2e.yml." >&2 + exit 1 + fi + + echo "Dispatched npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" + + while true; do + status="$(gh run view "$run_id" --json status --jq '.status')" + if [[ "$status" == "completed" ]]; then + break + fi + sleep 60 + done + + conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')" + url="$(gh run view "$run_id" --json url --jq '.url')" + echo "npm-telegram-beta-e2e.yml finished with ${conclusion}: ${url}" + if [[ "$conclusion" != "success" ]]; then + gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' + exit 1 + fi + + summary: + name: Verify full validation + needs: [normal_ci, release_checks, npm_telegram] + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Verify child workflow results + env: + NORMAL_CI_RESULT: ${{ needs.normal_ci.result }} + RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }} + NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }} + run: | + set -euo pipefail + failed=0 + for item in \ + "normal_ci=${NORMAL_CI_RESULT}" \ + "release_checks=${RELEASE_CHECKS_RESULT}" \ + "npm_telegram=${NPM_TELEGRAM_RESULT}" + do + name="${item%%=*}" + result="${item#*=}" + if [[ "$result" != "success" && "$result" != "skipped" ]]; then + echo "::error::${name} ended with ${result}" + failed=1 + fi + done + exit "$failed" diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 08eaf802ad2..6259e23db8f 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -190,42 +190,29 @@ jobs: - name: Validate selected ref id: validate env: - GH_TOKEN: ${{ github.token }} INPUT_REF: ${{ inputs.ref }} - WORKFLOW_REF_NAME: ${{ github.ref_name }} shell: bash run: | set -euo pipefail selected_sha="$(git rev-parse HEAD)" trusted_reason="" - git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main - if [[ "${WORKFLOW_REF_NAME}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then - git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}" - fi + git fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*' + git fetch --tags origin '+refs/tags/*:refs/tags/*' if git merge-base --is-ancestor "$selected_sha" refs/remotes/origin/main; then trusted_reason="main-ancestor" - elif [[ "${WORKFLOW_REF_NAME}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && - [[ "$selected_sha" == "$(git rev-parse "refs/remotes/origin/${WORKFLOW_REF_NAME}")" ]]; then - trusted_reason="release-branch-head" elif git tag --points-at "$selected_sha" | grep -Eq '^v'; then trusted_reason="release-tag" + elif git for-each-ref --format='%(refname:short)' --contains "$selected_sha" refs/remotes/origin | grep -Eq '^origin/'; then + trusted_reason="repository-branch-history" else - pr_head_count="$( - gh api \ - -H "Accept: application/vnd.github+json" \ - "repos/${GITHUB_REPOSITORY}/commits/${selected_sha}/pulls" \ - --jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${selected_sha}"'")] | length' - )" - if [[ "$pr_head_count" != "0" ]]; then - trusted_reason="open-pr-head" - fi + trusted_reason="" fi if [[ -z "$trusted_reason" ]]; then echo "Ref '${INPUT_REF}' resolved to $selected_sha, which is not trusted for secret-bearing live/E2E checks." >&2 - echo "Allowed refs must be on main, match the current release branch head, point to a release tag, or match an open PR head in ${GITHUB_REPOSITORY}." >&2 + echo "Allowed refs must be reachable from an OpenClaw branch or release tag." >&2 exit 1 fi diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index 1a2509582fc..c5203a6552f 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: ref: - description: Existing release tag or current full 40-character workflow-branch commit SHA to validate (for example v2026.4.12 or 0123456789abcdef0123456789abcdef01234567) + description: Branch, tag, or full commit SHA to validate required: true type: string provider: @@ -63,8 +63,8 @@ jobs: RELEASE_REF: ${{ inputs.ref }} run: | set -euo pipefail - if [[ ! "${RELEASE_REF}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]] && [[ ! "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then - echo "Expected an existing release tag or current full 40-character workflow-branch commit SHA, got: ${RELEASE_REF}" >&2 + if [[ -z "${RELEASE_REF// }" ]] || [[ "${RELEASE_REF}" == -* ]]; then + echo "Expected a branch, tag, or full commit SHA; got: ${RELEASE_REF}" >&2 exit 1 fi @@ -78,24 +78,27 @@ jobs: id: ref run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - - name: Validate selected ref is on workflow branch + - name: Validate selected ref belongs to this repository env: RELEASE_REF: ${{ inputs.ref }} - WORKFLOW_REF_NAME: ${{ github.ref_name }} run: | set -euo pipefail - RELEASE_BRANCH_REF="refs/remotes/origin/${WORKFLOW_REF_NAME}" - git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}" - if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then - BRANCH_SHA="$(git rev-parse "${RELEASE_BRANCH_REF}")" - if [[ "$(git rev-parse HEAD)" != "${BRANCH_SHA}" ]]; then - echo "Commit SHA mode only supports the current ${WORKFLOW_REF_NAME} HEAD. Use a release tag for older commits." >&2 - exit 1 - fi - else - git merge-base --is-ancestor HEAD "${RELEASE_BRANCH_REF}" + SELECTED_SHA="$(git rev-parse HEAD)" + git fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*' + git fetch --tags origin '+refs/tags/*:refs/tags/*' + + if git tag --points-at "${SELECTED_SHA}" | grep -Eq '^v'; then + exit 0 fi + if git for-each-ref --format='%(refname:short)' --contains "${SELECTED_SHA}" refs/remotes/origin | grep -Eq '^origin/'; then + exit 0 + fi + + echo "Ref '${RELEASE_REF}' resolved to ${SELECTED_SHA}, but that commit is not reachable from an OpenClaw branch or release tag." >&2 + echo "Secret-bearing release checks only run repository-owned branch/tag history, not arbitrary unreferenced commits." >&2 + exit 1 + - name: Capture selected inputs id: inputs env: diff --git a/docs/ci.md b/docs/ci.md index 6e420b6481f..74497fb47dc 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -6,7 +6,14 @@ read_when: - You are debugging failing GitHub Actions checks --- -The CI runs on every push to `main` and every pull request. It uses smart scoping to skip expensive jobs when only unrelated areas changed. Manual `workflow_dispatch` runs intentionally bypass smart scoping and fan out the full CI graph for release candidates or broad validation. +The CI runs on every push to `main` and every pull request. It uses smart scoping to skip expensive jobs when only unrelated areas changed. Manual `workflow_dispatch` runs intentionally bypass smart scoping and fan out the full normal CI graph for release candidates or broad validation. + +`Full Release Validation` is the manual umbrella workflow for "run everything +before release." It accepts a branch, tag, or full commit SHA, dispatches the +manual `CI` workflow with that target, and dispatches `OpenClaw Release Checks` +for install smoke, Docker release-path suites, live/E2E, OpenWebUI, QA Lab +parity, Matrix, and Telegram lanes. It can also run the post-publish `NPM +Telegram Beta E2E` workflow when a published package spec is provided. QA Lab has dedicated CI lanes outside the main smart-scoped workflow. The `Parity gate` workflow runs on matching PR changes and manual dispatch; it @@ -84,10 +91,14 @@ scoped lane on: Linux Node shards, bundled-plugin shards, channel contracts, Node 22 compatibility, `check`, `check-additional`, build smoke, docs checks, Python skills, Windows, macOS, Android, and Control UI i18n. Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by -another push or PR run on the same ref. +another push or PR run on the same ref. The optional `target_ref` input lets a +trusted caller run that graph against a branch, tag, or full commit SHA while +using the workflow file from the selected dispatch ref. ```bash gh workflow run ci.yml --ref release/YYYY.M.D +gh workflow run ci.yml --ref main -f target_ref= +gh workflow run full-release-validation.yml --ref main -f ref= ``` ## Fail-fast order diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index dfec8dc506a..0a31abfc90d 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -49,8 +49,16 @@ OpenClaw has three public release lanes: - Run `pnpm build && pnpm ui:build` before `pnpm release:check` so the expected `dist/*` release artifacts and Control UI bundle exist for the pack validation step -- Run the manual `CI` workflow before release approval when you need full normal - CI coverage for the release candidate. Manual CI dispatches bypass changed +- Run the manual `Full Release Validation` workflow before release approval + when you need the whole release validation suite from one entrypoint. It + accepts a branch, tag, or full commit SHA, dispatches manual `CI`, and + dispatches `OpenClaw Release Checks` for install smoke, Docker release-path + suites, live/E2E, OpenWebUI, QA Lab parity, Matrix, and Telegram lanes. + Provide `npm_telegram_package_spec` only after a package has been published + and the post-publish Telegram E2E should run too. + Example: `gh workflow run full-release-validation.yml --ref main -f ref=release/YYYY.M.D` +- Run the manual `CI` workflow directly when you only need full normal CI + coverage for the release candidate. Manual CI dispatches bypass changed scoping and force the Linux Node shards, bundled-plugin shards, channel contracts, Node 22 compatibility, `check`, `check-additional`, build smoke, docs checks, Python skills, Windows, macOS, Android, and Control UI i18n @@ -74,13 +82,11 @@ OpenClaw has three public release lanes: - This split is intentional: keep the real npm release path short, deterministic, and artifact-focused, while slower live checks stay in their own lane so they do not stall or block publish -- Release checks must be dispatched from the `main` workflow ref or from a - `release/YYYY.M.D` workflow ref so the workflow logic and secrets stay - controlled -- That workflow accepts either an existing release tag or the current full - 40-character workflow-branch commit SHA -- In commit-SHA mode it only accepts the current workflow-branch HEAD; use a - release tag for older release commits +- Secret-bearing release checks should be dispatched through `Full Release +Validation` or from the `main`/release workflow ref so workflow logic and + secrets stay controlled +- `OpenClaw Release Checks` accepts a branch, tag, or full commit SHA as long + as the resolved commit is reachable from an OpenClaw branch or release tag - `OpenClaw NPM Release` validation-only preflight also accepts the current full 40-character workflow-branch commit SHA without requiring a pushed tag - That SHA path is validation-only and cannot be promoted into a real publish @@ -163,10 +169,9 @@ OpenClaw has three public release lanes: `OpenClaw Release Checks` accepts these operator-controlled inputs: -- `ref`: existing release tag or the current full 40-character `main` commit - SHA to validate when dispatched from `main`; from a release branch, use an - existing release tag or the current full 40-character release-branch commit - SHA +- `ref`: branch, tag, or full commit SHA to validate. Secret-bearing checks + require the resolved commit to be reachable from an OpenClaw branch or + release tag. Rules: @@ -174,9 +179,8 @@ Rules: - Beta prerelease tags may publish only to `beta` - For `OpenClaw NPM Release`, full commit SHA input is allowed only when `preflight_only=true` -- `OpenClaw Release Checks` is always validation-only and also accepts the - current workflow-branch commit SHA -- Release checks commit-SHA mode also requires the current workflow-branch HEAD +- `OpenClaw Release Checks` and `Full Release Validation` are always + validation-only - The real publish path must use the same `npm_dist_tag` used during preflight; the workflow verifies that metadata before publish continues @@ -189,13 +193,11 @@ When cutting a stable npm release: SHA for a validation-only dry run of the preflight workflow 2. Choose `npm_dist_tag=beta` for the normal beta-first flow, or `latest` only when you intentionally want a direct stable publish -3. Run the manual `CI` workflow on the release ref when you want full normal CI - coverage instead of smart-scoped merge coverage -4. Run `OpenClaw Release Checks` separately with the same tag or the - full current workflow-branch commit SHA when you want live prompt cache, - QA Lab parity, Matrix, and Telegram coverage - - This is separate on purpose so live coverage stays available without - recoupling long-running or flaky checks to the publish workflow +3. Run `Full Release Validation` on the release branch, release tag, or full + commit SHA when you want normal CI plus live prompt cache, Docker, QA Lab, + Matrix, and Telegram coverage from one manual workflow +4. If you intentionally only need the deterministic normal test graph, run the + manual `CI` workflow on the release ref instead 5. Save the successful `preflight_run_id` 6. Run `OpenClaw NPM Release` again with `preflight_only=false`, the same `tag`, the same `npm_dist_tag`, and the saved `preflight_run_id`