From 57e4994caf6d3f60c2721d411fb3486ebf793100 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 29 Apr 2026 19:53:29 +0100 Subject: [PATCH] ci: speed up release validation --- .agents/skills/openclaw-testing/SKILL.md | 12 +- .github/workflows/full-release-validation.yml | 102 ++++---------- .../openclaw-live-and-e2e-checks-reusable.yml | 100 +++++++++----- .github/workflows/openclaw-release-checks.yml | 15 ++- scripts/qa-parity-report.ts | 85 ++++++++++++ scripts/run-node.mjs | 34 +++++ scripts/test-projects.test-support.mjs | 1 + src/agents/model-catalog.test.ts | 20 +++ src/agents/model-catalog.ts | 2 +- src/agents/model-selection-shared.ts | 23 +++- src/agents/model-selection.test.ts | 45 +++++++ .../chat.directive-tags.test.ts | 30 +++++ src/gateway/server.reload.test.ts | 127 ++---------------- src/infra/run-node.test.ts | 50 +++++++ test/gateway.multi.e2e.test.ts | 57 -------- .../package-acceptance-workflow.test.ts | 16 ++- 16 files changed, 427 insertions(+), 292 deletions(-) create mode 100644 scripts/qa-parity-report.ts diff --git a/.agents/skills/openclaw-testing/SKILL.md b/.agents/skills/openclaw-testing/SKILL.md index 5f479af673d..b674037e202 100644 --- a/.agents/skills/openclaw-testing/SKILL.md +++ b/.agents/skills/openclaw-testing/SKILL.md @@ -160,8 +160,8 @@ PRs, main pushes, and ad hoc broad CI checks do not spend Docker/package time or all-plugin runtime time on release-only product coverage. If a full run is already active on a newer `origin/main`, prefer watching that -run over dispatching a duplicate. If you accidentally dispatch a stale duplicate, -cancel it and monitor the current run. +run over dispatching a duplicate. Do not cancel release, release-check, or child +workflow runs unless Peter explicitly asks for cancellation. The child-dispatch jobs record the child run ids. The final `Verify full validation` job re-queries those child runs and is the canonical @@ -174,9 +174,11 @@ Supported umbrella groups are `all`, `ci`, `plugin-prerelease`, `release-checks`, `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`, `qa-parity`, `qa-live`, and `npm-telegram`. Use the narrowest group that covers the failed box. After a targeted release-check fix, do not restart the full -umbrella by habit: dispatch the matching `rerun_group`, cancel older duplicate -runs for the same target/group, and rerun only the parent verifier/evidence step -after the child is green unless the release evidence is stale. +umbrella by habit: dispatch the matching `rerun_group` and rerun only the parent +verifier/evidence step after the child is green unless the release evidence is +stale. For a single failed live/E2E shard, use +`-f rerun_group=live-e2e -f live_suite_filter=` so the Blacksmith +workflow only spends setup and queue time on that suite. ### Release Evidence diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index 49b76948b5f..fe4ab872f7f 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -53,6 +53,11 @@ on: - qa-parity - qa-live - npm-telegram + live_suite_filter: + description: Optional exact live suite id for focused live/E2E reruns; blank runs all selected live suites + required: false + default: "" + type: string npm_telegram_package_spec: description: Optional published package spec for the post-publish Telegram E2E lane required: false @@ -83,7 +88,7 @@ permissions: concurrency: group: full-release-validation-${{ inputs.ref }}-${{ inputs.rerun_group }} - cancel-in-progress: true + cancel-in-progress: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" @@ -123,6 +128,7 @@ jobs: NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }} EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }} RERUN_GROUP: ${{ inputs.rerun_group }} + LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }} run: | { echo "## Full release validation" @@ -131,6 +137,9 @@ jobs: echo "- Target SHA: \`${TARGET_SHA}\`" echo "- Child workflow ref: \`${CHILD_WORKFLOW_REF}\`" echo "- Rerun group: \`${RERUN_GROUP}\`" + if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then + echo "- Live suite filter: \`${LIVE_SUITE_FILTER}\`" + fi if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "ci" ]]; then echo "- Normal CI: \`CI\` with \`target_ref=${TARGET_SHA}\`" else @@ -213,19 +222,6 @@ jobs: echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" - cleanup_child_run() { - local exit_code=$? - trap - EXIT INT TERM - local child_status - child_status="$(gh run view "$run_id" --json status --jq '.status' 2>/dev/null || true)" - if [[ "$child_status" != "completed" ]]; then - echo "Cancelling child ${workflow} run ${run_id} after parent exit (${exit_code})." - gh run cancel "$run_id" || gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/force-cancel" || true - fi - return "$exit_code" - } - trap cleanup_child_run EXIT INT TERM - while true; do status="$(gh run view "$run_id" --json status --jq '.status')" if [[ "$status" == "completed" ]]; then @@ -252,23 +248,6 @@ jobs: echo "- Target SHA: \`${TARGET_SHA}\`" } >> "$GITHUB_STEP_SUMMARY" - cancel_same_sha_push_ci() { - local run_ids run_id - run_ids="$( - gh run list --workflow ci.yml --limit 100 --json databaseId,event,headSha,status \ - --jq 'map(select(.event == "push" and .headSha == env.TARGET_SHA and (.status == "queued" or .status == "in_progress" or .status == "waiting" or .status == "pending"))) | .[].databaseId' - )" - if [[ -z "${run_ids// }" ]]; then - return 0 - fi - while IFS= read -r run_id; do - [[ -n "${run_id// }" ]] || continue - echo "Cancelling same-SHA push CI run ${run_id}; Full Release Validation dispatches the full manual CI child for ${TARGET_SHA}." - gh run cancel "$run_id" || gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/force-cancel" || true - done <<< "$run_ids" - } - - cancel_same_sha_push_ci dispatch_and_wait ci.yml -f target_ref="$TARGET_SHA" -f include_android=true plugin_prerelease: @@ -328,19 +307,6 @@ jobs: echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" - cleanup_child_run() { - local exit_code=$? - trap - EXIT INT TERM - local child_status - child_status="$(gh run view "$run_id" --json status --jq '.status' 2>/dev/null || true)" - if [[ "$child_status" != "completed" ]]; then - echo "Cancelling child ${workflow} run ${run_id} after parent exit (${exit_code})." - gh run cancel "$run_id" || gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/force-cancel" || true - fi - return "$exit_code" - } - trap cleanup_child_run EXIT INT TERM - while true; do status="$(gh run view "$run_id" --json status --jq '.status')" if [[ "$status" == "completed" ]]; then @@ -391,6 +357,7 @@ jobs: MODE: ${{ inputs.mode }} RELEASE_PROFILE: ${{ inputs.release_profile }} RERUN_GROUP: ${{ inputs.rerun_group }} + LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }} run: | set -euo pipefail @@ -430,19 +397,6 @@ jobs: echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" - cleanup_child_run() { - local exit_code=$? - trap - EXIT INT TERM - local child_status - child_status="$(gh run view "$run_id" --json status --jq '.status' 2>/dev/null || true)" - if [[ "$child_status" != "completed" ]]; then - echo "Cancelling child ${workflow} run ${run_id} after parent exit (${exit_code})." - gh run cancel "$run_id" || gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/force-cancel" || true - fi - return "$exit_code" - } - trap cleanup_child_run EXIT INT TERM - while true; do status="$(gh run view "$run_id" --json status --jq '.status')" if [[ "$status" == "completed" ]]; then @@ -471,6 +425,9 @@ jobs: echo "- Cross-OS mode: \`${MODE}\`" echo "- Release profile: \`${RELEASE_PROFILE}\`" echo "- Rerun group: \`${RERUN_GROUP}\`" + if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then + echo "- Live suite filter: \`${LIVE_SUITE_FILTER}\`" + fi } >> "$GITHUB_STEP_SUMMARY" child_rerun_group="$RERUN_GROUP" @@ -478,13 +435,19 @@ jobs: child_rerun_group=all fi - dispatch_and_wait openclaw-release-checks.yml \ - -f ref="$TARGET_SHA" \ - -f expected_sha="$TARGET_SHA" \ - -f provider="$PROVIDER" \ - -f mode="$MODE" \ - -f release_profile="$RELEASE_PROFILE" \ + args=( + -f ref="$TARGET_SHA" + -f expected_sha="$TARGET_SHA" + -f provider="$PROVIDER" + -f mode="$MODE" + -f release_profile="$RELEASE_PROFILE" -f rerun_group="$child_rerun_group" + ) + if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then + args+=(-f live_suite_filter="$LIVE_SUITE_FILTER") + fi + + dispatch_and_wait openclaw-release-checks.yml "${args[@]}" npm_telegram: name: Run post-publish Telegram E2E @@ -538,19 +501,6 @@ jobs: echo "Dispatched npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" echo "run_id=${run_id}" >> "$GITHUB_OUTPUT" - cleanup_child_run() { - local exit_code=$? - trap - EXIT INT TERM - local child_status - child_status="$(gh run view "$run_id" --json status --jq '.status' 2>/dev/null || true)" - if [[ "$child_status" != "completed" ]]; then - echo "Cancelling npm-telegram-beta-e2e.yml child run ${run_id} after parent exit (${exit_code})." - gh run cancel "$run_id" || gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/force-cancel" || true - fi - return "$exit_code" - } - trap cleanup_child_run EXIT INT TERM - while true; do status="$(gh run view "$run_id" --json status --jq '.status')" if [[ "$status" == "completed" ]]; then diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index bf85f14a580..dd6335c5f84 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -63,6 +63,11 @@ on: required: false default: "" type: string + live_suite_filter: + description: Optional exact live suite id to run for focused failed-shard recovery; blank runs all selected suites + required: false + default: "" + type: string release_test_profile: description: Release coverage profile for live/Docker/provider breadth required: false @@ -133,6 +138,11 @@ on: required: false default: "" type: string + live_suite_filter: + description: Optional exact live suite id to run for focused failed-shard recovery; blank runs all selected suites + required: false + default: "" + type: string release_test_profile: description: Release coverage profile for live/Docker/provider breadth required: false @@ -296,7 +306,7 @@ jobs: validate_release_live_cache: needs: validate_selected_ref - if: inputs.include_live_suites && !inputs.live_models_only + if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'live-cache') runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 60 env: @@ -335,7 +345,7 @@ jobs: validate_repo_e2e: needs: validate_selected_ref - if: inputs.include_repo_e2e + if: inputs.include_repo_e2e && inputs.live_suite_filter == '' runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 90 env: @@ -362,7 +372,7 @@ jobs: validate_special_e2e: needs: validate_selected_ref - if: inputs.include_repo_e2e || (inputs.include_live_suites && !inputs.live_models_only) + if: (inputs.include_repo_e2e || (inputs.include_live_suites && !inputs.live_models_only)) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e' || inputs.live_suite_filter == 'openai-ws-stream-live-e2e') runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: ${{ matrix.timeout_minutes }} strategy: @@ -401,11 +411,15 @@ jobs: - name: Build dist for special E2E if: | - (inputs.include_repo_e2e && matrix.requires_repo_e2e) || - (inputs.include_live_suites && matrix.requires_live_suites) + ( + (inputs.include_repo_e2e && matrix.requires_repo_e2e) || + (inputs.include_live_suites && matrix.requires_live_suites) + ) && + (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) run: pnpm build - name: Configure suite-specific env + if: inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id shell: bash run: | set -euo pipefail @@ -417,6 +431,7 @@ jobs: esac - name: Validate suite credentials + if: inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id shell: bash run: | set -euo pipefail @@ -431,8 +446,11 @@ jobs: - name: Run ${{ matrix.label }} if: | - (inputs.include_repo_e2e && matrix.requires_repo_e2e) || - (inputs.include_live_suites && matrix.requires_live_suites) + ( + (inputs.include_repo_e2e && matrix.requires_repo_e2e) || + (inputs.include_live_suites && matrix.requires_live_suites) + ) && + (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) run: ${{ matrix.command }} validate_docker_e2e: @@ -1278,7 +1296,7 @@ jobs: prepare_live_test_image: needs: validate_selected_ref - if: inputs.include_live_suites + if: inputs.include_live_suites && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-') || startsWith(inputs.live_suite_filter, 'docker-live-models')) runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: 60 permissions: @@ -1351,7 +1369,7 @@ jobs: validate_live_models_docker: name: Docker live models (${{ matrix.provider_label }}) needs: [validate_selected_ref, prepare_live_test_image] - if: inputs.include_live_suites && inputs.live_model_providers == '' + if: inputs.include_live_suites && inputs.live_model_providers == '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models') runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: 75 strategy: @@ -1501,7 +1519,7 @@ jobs: validate_live_models_docker_targeted: name: Docker live models (selected providers) needs: [validate_selected_ref, prepare_live_test_image] - if: inputs.include_live_suites && inputs.live_model_providers != '' + if: inputs.include_live_suites && inputs.live_model_providers != '' && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'docker-live-models') runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: 75 env: @@ -1674,7 +1692,7 @@ jobs: validate_live_provider_suites: needs: validate_selected_ref - if: inputs.include_live_suites && !inputs.live_models_only + if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || (startsWith(inputs.live_suite_filter, 'native-live-') && !startsWith(inputs.live_suite_filter, 'native-live-extensions-media') && inputs.live_suite_filter != 'native-live-extensions-a-k')) runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: ${{ matrix.timeout_minutes }} strategy: @@ -1782,6 +1800,7 @@ jobs: command: node .release-harness/scripts/test-live-shard.mjs native-live-extensions-moonshot timeout_minutes: 60 profile_env_only: false + advisory: true profiles: full - suite_id: native-live-extensions-openai label: Native live OpenAI plugin @@ -1852,14 +1871,14 @@ jobs: OPENCLAW_VITEST_MAX_WORKERS: "2" steps: - name: Checkout selected ref - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: actions/checkout@v6 with: ref: ${{ needs.validate_selected_ref.outputs.selected_sha }} fetch-depth: 1 - name: Checkout trusted live shard harness - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: actions/checkout@v6 with: ref: ${{ github.sha }} @@ -1867,7 +1886,7 @@ jobs: path: .release-harness - name: Setup Node environment - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} @@ -1875,11 +1894,11 @@ jobs: install-bun: "true" - name: Hydrate live auth/profile inputs - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) run: bash scripts/ci-hydrate-live-auth.sh - name: Configure suite-specific env - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) shell: bash run: | set -euo pipefail @@ -1932,15 +1951,28 @@ jobs: esac - name: Run ${{ matrix.label }} - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) env: OPENCLAW_LIVE_COMMAND: ${{ matrix.command }} - run: bash .release-harness/scripts/ci-live-command-retry.sh + OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }} + run: | + set +e + bash .release-harness/scripts/ci-live-command-retry.sh + status=$? + set -e + if [[ "$status" -eq 0 ]]; then + exit 0 + fi + if [[ "${OPENCLAW_LIVE_SUITE_ADVISORY:-}" == "true" ]]; then + echo "::warning::Advisory live suite failed with exit code ${status}: ${{ matrix.suite_id }}" + exit 0 + fi + exit "$status" validate_live_docker_provider_suites: name: Docker live suites (${{ matrix.label }}) needs: [validate_selected_ref, prepare_live_test_image] - if: inputs.include_live_suites && !inputs.live_models_only + if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'live-')) runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: ${{ matrix.timeout_minutes }} strategy: @@ -2024,14 +2056,14 @@ jobs: OPENCLAW_VITEST_MAX_WORKERS: "2" steps: - name: Checkout selected ref - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: actions/checkout@v6 with: ref: ${{ needs.validate_selected_ref.outputs.selected_sha }} fetch-depth: 1 - name: Checkout trusted live shard harness - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: actions/checkout@v6 with: ref: ${{ github.sha }} @@ -2039,7 +2071,7 @@ jobs: path: .release-harness - name: Setup Node environment - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} @@ -2047,11 +2079,11 @@ jobs: install-bun: "true" - name: Hydrate live auth/profile inputs - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) run: bash scripts/ci-hydrate-live-auth.sh - name: Log in to GHCR - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ghcr.io @@ -2059,7 +2091,7 @@ jobs: password: ${{ github.token }} - name: Configure suite-specific env - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) shell: bash run: | set -euo pipefail @@ -2093,7 +2125,7 @@ jobs: esac - name: Run ${{ matrix.label }} - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) env: OPENCLAW_LIVE_COMMAND: ${{ matrix.command }} run: bash .release-harness/scripts/ci-live-command-retry.sh @@ -2101,7 +2133,7 @@ jobs: validate_live_media_provider_suites: name: Live media suites (${{ matrix.label }}) needs: validate_selected_ref - if: inputs.include_live_suites && !inputs.live_models_only + if: inputs.include_live_suites && !inputs.live_models_only && (inputs.live_suite_filter == '' || startsWith(inputs.live_suite_filter, 'native-live-extensions-media') || inputs.live_suite_filter == 'native-live-extensions-a-k') runs-on: blacksmith-8vcpu-ubuntu-2404 container: image: ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04 @@ -2194,14 +2226,14 @@ jobs: OPENCLAW_VITEST_MAX_WORKERS: "2" steps: - name: Checkout selected ref - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: actions/checkout@v6 with: ref: ${{ needs.validate_selected_ref.outputs.selected_sha }} fetch-depth: 1 - name: Checkout trusted live shard harness - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: actions/checkout@v6 with: ref: ${{ github.sha }} @@ -2209,7 +2241,7 @@ jobs: path: .release-harness - name: Verify preinstalled live media dependencies - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) shell: bash run: | set -euo pipefail @@ -2217,7 +2249,7 @@ jobs: ffprobe -version | head -1 - name: Setup Node environment - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} @@ -2225,11 +2257,11 @@ jobs: install-bun: "true" - name: Hydrate live auth/profile inputs - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) run: bash scripts/ci-hydrate-live-auth.sh - name: Configure suite-specific env - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) shell: bash run: | set -euo pipefail @@ -2238,5 +2270,5 @@ jobs: fi - name: Run ${{ matrix.label }} - if: contains(matrix.profiles, inputs.release_test_profile) + if: contains(matrix.profiles, inputs.release_test_profile) && (inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) run: ${{ matrix.command }} diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index 1896c0e301e..87c27cdd6af 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -53,10 +53,15 @@ on: - qa - qa-parity - qa-live + live_suite_filter: + description: Optional exact live suite id for focused live/E2E reruns; blank runs all selected live suites + required: false + default: "" + type: string concurrency: group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }} - cancel-in-progress: true + cancel-in-progress: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" @@ -77,6 +82,7 @@ jobs: mode: ${{ steps.inputs.outputs.mode }} release_profile: ${{ steps.inputs.outputs.release_profile }} rerun_group: ${{ steps.inputs.outputs.rerun_group }} + live_suite_filter: ${{ steps.inputs.outputs.live_suite_filter }} steps: - name: Require main or release workflow ref for release checks env: @@ -192,6 +198,7 @@ jobs: RELEASE_MODE_INPUT: ${{ inputs.mode }} RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }} RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }} + RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }} run: | set -euo pipefail { @@ -200,6 +207,7 @@ jobs: printf 'mode=%s\n' "$RELEASE_MODE_INPUT" printf 'release_profile=%s\n' "$RELEASE_PROFILE_INPUT" printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT" + printf 'live_suite_filter=%s\n' "$RELEASE_LIVE_SUITE_FILTER_INPUT" } >> "$GITHUB_OUTPUT" - name: Summarize validated ref @@ -211,6 +219,7 @@ jobs: RELEASE_MODE: ${{ inputs.mode }} RELEASE_PROFILE: ${{ inputs.release_profile }} RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }} + RELEASE_LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }} run: | { echo "## Release checks" @@ -222,6 +231,9 @@ jobs: echo "- Cross-OS mode: \`${RELEASE_MODE}\`" echo "- Release profile: \`${RELEASE_PROFILE}\`" echo "- Rerun group: \`${RELEASE_RERUN_GROUP}\`" + if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then + echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`" + fi echo "- This run will execute cross-OS release validation, install smoke, QA Lab parity, Matrix, and Telegram lanes, and the non-Parallels Docker/live/openwebui coverage from the CI migration plan." } >> "$GITHUB_STEP_SUMMARY" @@ -342,6 +354,7 @@ jobs: include_openwebui: false include_live_suites: true release_test_profile: ${{ needs.resolve_target.outputs.release_profile }} + live_suite_filter: ${{ needs.resolve_target.outputs.live_suite_filter }} secrets: &live_e2e_release_secrets OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} diff --git a/scripts/qa-parity-report.ts b/scripts/qa-parity-report.ts new file mode 100644 index 00000000000..bd84f6380da --- /dev/null +++ b/scripts/qa-parity-report.ts @@ -0,0 +1,85 @@ +import { runQaParityReportCommand } from "../extensions/qa-lab/src/cli.runtime.ts"; + +type Options = { + baselineLabel?: string; + baselineSummary?: string; + candidateLabel?: string; + candidateSummary?: string; + outputDir?: string; + repoRoot?: string; +}; + +function takeValue(args: string[], index: number, flag: string): string { + const value = args[index + 1]; + if (!value || value.startsWith("-")) { + throw new Error(`${flag} requires a value.`); + } + return value; +} + +function parseArgs(args: string[]): Options { + const opts: Options = {}; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + switch (arg) { + case "--help": + case "-h": + process.stdout.write(`Usage: openclaw qa parity-report [options] + +Options: + --candidate-summary Candidate qa-suite-summary.json path + --baseline-summary Baseline qa-suite-summary.json path + --candidate-label