mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
ci: speed up release validation
This commit is contained in:
@@ -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.
|
all-plugin runtime time on release-only product coverage.
|
||||||
|
|
||||||
If a full run is already active on a newer `origin/main`, prefer watching that
|
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,
|
run over dispatching a duplicate. Do not cancel release, release-check, or child
|
||||||
cancel it and monitor the current run.
|
workflow runs unless Peter explicitly asks for cancellation.
|
||||||
|
|
||||||
The child-dispatch jobs record the child run ids. The final
|
The child-dispatch jobs record the child run ids. The final
|
||||||
`Verify full validation` job re-queries those child runs and is the canonical
|
`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`,
|
`release-checks`, `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`,
|
||||||
`qa-parity`, `qa-live`, and `npm-telegram`. Use the narrowest group that covers
|
`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
|
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
|
umbrella by habit: dispatch the matching `rerun_group` and rerun only the parent
|
||||||
runs for the same target/group, and rerun only the parent verifier/evidence step
|
verifier/evidence step after the child is green unless the release evidence is
|
||||||
after the child is green unless the release evidence is stale.
|
stale. For a single failed live/E2E shard, use
|
||||||
|
`-f rerun_group=live-e2e -f live_suite_filter=<suite_id>` so the Blacksmith
|
||||||
|
workflow only spends setup and queue time on that suite.
|
||||||
|
|
||||||
### Release Evidence
|
### Release Evidence
|
||||||
|
|
||||||
|
|||||||
102
.github/workflows/full-release-validation.yml
vendored
102
.github/workflows/full-release-validation.yml
vendored
@@ -53,6 +53,11 @@ on:
|
|||||||
- qa-parity
|
- qa-parity
|
||||||
- qa-live
|
- qa-live
|
||||||
- npm-telegram
|
- 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:
|
npm_telegram_package_spec:
|
||||||
description: Optional published package spec for the post-publish Telegram E2E lane
|
description: Optional published package spec for the post-publish Telegram E2E lane
|
||||||
required: false
|
required: false
|
||||||
@@ -83,7 +88,7 @@ permissions:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: full-release-validation-${{ inputs.ref }}-${{ inputs.rerun_group }}
|
group: full-release-validation-${{ inputs.ref }}-${{ inputs.rerun_group }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
@@ -123,6 +128,7 @@ jobs:
|
|||||||
NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }}
|
NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }}
|
||||||
EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }}
|
EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }}
|
||||||
RERUN_GROUP: ${{ inputs.rerun_group }}
|
RERUN_GROUP: ${{ inputs.rerun_group }}
|
||||||
|
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
|
||||||
run: |
|
run: |
|
||||||
{
|
{
|
||||||
echo "## Full release validation"
|
echo "## Full release validation"
|
||||||
@@ -131,6 +137,9 @@ jobs:
|
|||||||
echo "- Target SHA: \`${TARGET_SHA}\`"
|
echo "- Target SHA: \`${TARGET_SHA}\`"
|
||||||
echo "- Child workflow ref: \`${CHILD_WORKFLOW_REF}\`"
|
echo "- Child workflow ref: \`${CHILD_WORKFLOW_REF}\`"
|
||||||
echo "- Rerun group: \`${RERUN_GROUP}\`"
|
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
|
if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "ci" ]]; then
|
||||||
echo "- Normal CI: \`CI\` with \`target_ref=${TARGET_SHA}\`"
|
echo "- Normal CI: \`CI\` with \`target_ref=${TARGET_SHA}\`"
|
||||||
else
|
else
|
||||||
@@ -213,19 +222,6 @@ jobs:
|
|||||||
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||||
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
|
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
|
while true; do
|
||||||
status="$(gh run view "$run_id" --json status --jq '.status')"
|
status="$(gh run view "$run_id" --json status --jq '.status')"
|
||||||
if [[ "$status" == "completed" ]]; then
|
if [[ "$status" == "completed" ]]; then
|
||||||
@@ -252,23 +248,6 @@ jobs:
|
|||||||
echo "- Target SHA: \`${TARGET_SHA}\`"
|
echo "- Target SHA: \`${TARGET_SHA}\`"
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
} >> "$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
|
dispatch_and_wait ci.yml -f target_ref="$TARGET_SHA" -f include_android=true
|
||||||
|
|
||||||
plugin_prerelease:
|
plugin_prerelease:
|
||||||
@@ -328,19 +307,6 @@ jobs:
|
|||||||
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||||
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
|
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
|
while true; do
|
||||||
status="$(gh run view "$run_id" --json status --jq '.status')"
|
status="$(gh run view "$run_id" --json status --jq '.status')"
|
||||||
if [[ "$status" == "completed" ]]; then
|
if [[ "$status" == "completed" ]]; then
|
||||||
@@ -391,6 +357,7 @@ jobs:
|
|||||||
MODE: ${{ inputs.mode }}
|
MODE: ${{ inputs.mode }}
|
||||||
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
||||||
RERUN_GROUP: ${{ inputs.rerun_group }}
|
RERUN_GROUP: ${{ inputs.rerun_group }}
|
||||||
|
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -430,19 +397,6 @@ jobs:
|
|||||||
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||||
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
|
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
|
while true; do
|
||||||
status="$(gh run view "$run_id" --json status --jq '.status')"
|
status="$(gh run view "$run_id" --json status --jq '.status')"
|
||||||
if [[ "$status" == "completed" ]]; then
|
if [[ "$status" == "completed" ]]; then
|
||||||
@@ -471,6 +425,9 @@ jobs:
|
|||||||
echo "- Cross-OS mode: \`${MODE}\`"
|
echo "- Cross-OS mode: \`${MODE}\`"
|
||||||
echo "- Release profile: \`${RELEASE_PROFILE}\`"
|
echo "- Release profile: \`${RELEASE_PROFILE}\`"
|
||||||
echo "- Rerun group: \`${RERUN_GROUP}\`"
|
echo "- Rerun group: \`${RERUN_GROUP}\`"
|
||||||
|
if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then
|
||||||
|
echo "- Live suite filter: \`${LIVE_SUITE_FILTER}\`"
|
||||||
|
fi
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
child_rerun_group="$RERUN_GROUP"
|
child_rerun_group="$RERUN_GROUP"
|
||||||
@@ -478,13 +435,19 @@ jobs:
|
|||||||
child_rerun_group=all
|
child_rerun_group=all
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dispatch_and_wait openclaw-release-checks.yml \
|
args=(
|
||||||
-f ref="$TARGET_SHA" \
|
-f ref="$TARGET_SHA"
|
||||||
-f expected_sha="$TARGET_SHA" \
|
-f expected_sha="$TARGET_SHA"
|
||||||
-f provider="$PROVIDER" \
|
-f provider="$PROVIDER"
|
||||||
-f mode="$MODE" \
|
-f mode="$MODE"
|
||||||
-f release_profile="$RELEASE_PROFILE" \
|
-f release_profile="$RELEASE_PROFILE"
|
||||||
-f rerun_group="$child_rerun_group"
|
-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:
|
npm_telegram:
|
||||||
name: Run post-publish Telegram E2E
|
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 "Dispatched npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
||||||
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
|
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
|
while true; do
|
||||||
status="$(gh run view "$run_id" --json status --jq '.status')"
|
status="$(gh run view "$run_id" --json status --jq '.status')"
|
||||||
if [[ "$status" == "completed" ]]; then
|
if [[ "$status" == "completed" ]]; then
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
type: string
|
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:
|
release_test_profile:
|
||||||
description: Release coverage profile for live/Docker/provider breadth
|
description: Release coverage profile for live/Docker/provider breadth
|
||||||
required: false
|
required: false
|
||||||
@@ -133,6 +138,11 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
type: string
|
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:
|
release_test_profile:
|
||||||
description: Release coverage profile for live/Docker/provider breadth
|
description: Release coverage profile for live/Docker/provider breadth
|
||||||
required: false
|
required: false
|
||||||
@@ -296,7 +306,7 @@ jobs:
|
|||||||
|
|
||||||
validate_release_live_cache:
|
validate_release_live_cache:
|
||||||
needs: validate_selected_ref
|
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
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
env:
|
env:
|
||||||
@@ -335,7 +345,7 @@ jobs:
|
|||||||
|
|
||||||
validate_repo_e2e:
|
validate_repo_e2e:
|
||||||
needs: validate_selected_ref
|
needs: validate_selected_ref
|
||||||
if: inputs.include_repo_e2e
|
if: inputs.include_repo_e2e && inputs.live_suite_filter == ''
|
||||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
env:
|
env:
|
||||||
@@ -362,7 +372,7 @@ jobs:
|
|||||||
|
|
||||||
validate_special_e2e:
|
validate_special_e2e:
|
||||||
needs: validate_selected_ref
|
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
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -401,11 +411,15 @@ jobs:
|
|||||||
|
|
||||||
- name: Build dist for special E2E
|
- name: Build dist for special E2E
|
||||||
if: |
|
if: |
|
||||||
|
(
|
||||||
(inputs.include_repo_e2e && matrix.requires_repo_e2e) ||
|
(inputs.include_repo_e2e && matrix.requires_repo_e2e) ||
|
||||||
(inputs.include_live_suites && matrix.requires_live_suites)
|
(inputs.include_live_suites && matrix.requires_live_suites)
|
||||||
|
) &&
|
||||||
|
(inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id)
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Configure suite-specific env
|
- name: Configure suite-specific env
|
||||||
|
if: inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -417,6 +431,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Validate suite credentials
|
- name: Validate suite credentials
|
||||||
|
if: inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -431,8 +446,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Run ${{ matrix.label }}
|
- name: Run ${{ matrix.label }}
|
||||||
if: |
|
if: |
|
||||||
|
(
|
||||||
(inputs.include_repo_e2e && matrix.requires_repo_e2e) ||
|
(inputs.include_repo_e2e && matrix.requires_repo_e2e) ||
|
||||||
(inputs.include_live_suites && matrix.requires_live_suites)
|
(inputs.include_live_suites && matrix.requires_live_suites)
|
||||||
|
) &&
|
||||||
|
(inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id)
|
||||||
run: ${{ matrix.command }}
|
run: ${{ matrix.command }}
|
||||||
|
|
||||||
validate_docker_e2e:
|
validate_docker_e2e:
|
||||||
@@ -1278,7 +1296,7 @@ jobs:
|
|||||||
|
|
||||||
prepare_live_test_image:
|
prepare_live_test_image:
|
||||||
needs: validate_selected_ref
|
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
|
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
permissions:
|
permissions:
|
||||||
@@ -1351,7 +1369,7 @@ jobs:
|
|||||||
validate_live_models_docker:
|
validate_live_models_docker:
|
||||||
name: Docker live models (${{ matrix.provider_label }})
|
name: Docker live models (${{ matrix.provider_label }})
|
||||||
needs: [validate_selected_ref, prepare_live_test_image]
|
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
|
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||||
timeout-minutes: 75
|
timeout-minutes: 75
|
||||||
strategy:
|
strategy:
|
||||||
@@ -1501,7 +1519,7 @@ jobs:
|
|||||||
validate_live_models_docker_targeted:
|
validate_live_models_docker_targeted:
|
||||||
name: Docker live models (selected providers)
|
name: Docker live models (selected providers)
|
||||||
needs: [validate_selected_ref, prepare_live_test_image]
|
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
|
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||||
timeout-minutes: 75
|
timeout-minutes: 75
|
||||||
env:
|
env:
|
||||||
@@ -1674,7 +1692,7 @@ jobs:
|
|||||||
|
|
||||||
validate_live_provider_suites:
|
validate_live_provider_suites:
|
||||||
needs: validate_selected_ref
|
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
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -1782,6 +1800,7 @@ jobs:
|
|||||||
command: node .release-harness/scripts/test-live-shard.mjs native-live-extensions-moonshot
|
command: node .release-harness/scripts/test-live-shard.mjs native-live-extensions-moonshot
|
||||||
timeout_minutes: 60
|
timeout_minutes: 60
|
||||||
profile_env_only: false
|
profile_env_only: false
|
||||||
|
advisory: true
|
||||||
profiles: full
|
profiles: full
|
||||||
- suite_id: native-live-extensions-openai
|
- suite_id: native-live-extensions-openai
|
||||||
label: Native live OpenAI plugin
|
label: Native live OpenAI plugin
|
||||||
@@ -1852,14 +1871,14 @@ jobs:
|
|||||||
OPENCLAW_VITEST_MAX_WORKERS: "2"
|
OPENCLAW_VITEST_MAX_WORKERS: "2"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout selected ref
|
- 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
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Checkout trusted live shard harness
|
- 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
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
@@ -1867,7 +1886,7 @@ jobs:
|
|||||||
path: .release-harness
|
path: .release-harness
|
||||||
|
|
||||||
- name: Setup Node environment
|
- 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
|
uses: ./.github/actions/setup-node-env
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
@@ -1875,11 +1894,11 @@ jobs:
|
|||||||
install-bun: "true"
|
install-bun: "true"
|
||||||
|
|
||||||
- name: Hydrate live auth/profile inputs
|
- 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
|
run: bash scripts/ci-hydrate-live-auth.sh
|
||||||
|
|
||||||
- name: Configure suite-specific env
|
- 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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -1932,15 +1951,28 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Run ${{ matrix.label }}
|
- 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:
|
env:
|
||||||
OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}
|
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:
|
validate_live_docker_provider_suites:
|
||||||
name: Docker live suites (${{ matrix.label }})
|
name: Docker live suites (${{ matrix.label }})
|
||||||
needs: [validate_selected_ref, prepare_live_test_image]
|
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
|
runs-on: blacksmith-32vcpu-ubuntu-2404
|
||||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||||
strategy:
|
strategy:
|
||||||
@@ -2024,14 +2056,14 @@ jobs:
|
|||||||
OPENCLAW_VITEST_MAX_WORKERS: "2"
|
OPENCLAW_VITEST_MAX_WORKERS: "2"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout selected ref
|
- 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
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Checkout trusted live shard harness
|
- 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
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
@@ -2039,7 +2071,7 @@ jobs:
|
|||||||
path: .release-harness
|
path: .release-harness
|
||||||
|
|
||||||
- name: Setup Node environment
|
- 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
|
uses: ./.github/actions/setup-node-env
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
@@ -2047,11 +2079,11 @@ jobs:
|
|||||||
install-bun: "true"
|
install-bun: "true"
|
||||||
|
|
||||||
- name: Hydrate live auth/profile inputs
|
- 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
|
run: bash scripts/ci-hydrate-live-auth.sh
|
||||||
|
|
||||||
- name: Log in to GHCR
|
- 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
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -2059,7 +2091,7 @@ jobs:
|
|||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Configure suite-specific env
|
- 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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -2093,7 +2125,7 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Run ${{ matrix.label }}
|
- 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:
|
env:
|
||||||
OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}
|
OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}
|
||||||
run: bash .release-harness/scripts/ci-live-command-retry.sh
|
run: bash .release-harness/scripts/ci-live-command-retry.sh
|
||||||
@@ -2101,7 +2133,7 @@ jobs:
|
|||||||
validate_live_media_provider_suites:
|
validate_live_media_provider_suites:
|
||||||
name: Live media suites (${{ matrix.label }})
|
name: Live media suites (${{ matrix.label }})
|
||||||
needs: validate_selected_ref
|
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
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04
|
image: ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04
|
||||||
@@ -2194,14 +2226,14 @@ jobs:
|
|||||||
OPENCLAW_VITEST_MAX_WORKERS: "2"
|
OPENCLAW_VITEST_MAX_WORKERS: "2"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout selected ref
|
- 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
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
ref: ${{ needs.validate_selected_ref.outputs.selected_sha }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Checkout trusted live shard harness
|
- 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
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
@@ -2209,7 +2241,7 @@ jobs:
|
|||||||
path: .release-harness
|
path: .release-harness
|
||||||
|
|
||||||
- name: Verify preinstalled live media dependencies
|
- 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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -2217,7 +2249,7 @@ jobs:
|
|||||||
ffprobe -version | head -1
|
ffprobe -version | head -1
|
||||||
|
|
||||||
- name: Setup Node environment
|
- 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
|
uses: ./.github/actions/setup-node-env
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
@@ -2225,11 +2257,11 @@ jobs:
|
|||||||
install-bun: "true"
|
install-bun: "true"
|
||||||
|
|
||||||
- name: Hydrate live auth/profile inputs
|
- 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
|
run: bash scripts/ci-hydrate-live-auth.sh
|
||||||
|
|
||||||
- name: Configure suite-specific env
|
- 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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -2238,5 +2270,5 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run ${{ matrix.label }}
|
- 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 }}
|
run: ${{ matrix.command }}
|
||||||
|
|||||||
15
.github/workflows/openclaw-release-checks.yml
vendored
15
.github/workflows/openclaw-release-checks.yml
vendored
@@ -53,10 +53,15 @@ on:
|
|||||||
- qa
|
- qa
|
||||||
- qa-parity
|
- qa-parity
|
||||||
- qa-live
|
- 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:
|
concurrency:
|
||||||
group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }}
|
group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||||
@@ -77,6 +82,7 @@ jobs:
|
|||||||
mode: ${{ steps.inputs.outputs.mode }}
|
mode: ${{ steps.inputs.outputs.mode }}
|
||||||
release_profile: ${{ steps.inputs.outputs.release_profile }}
|
release_profile: ${{ steps.inputs.outputs.release_profile }}
|
||||||
rerun_group: ${{ steps.inputs.outputs.rerun_group }}
|
rerun_group: ${{ steps.inputs.outputs.rerun_group }}
|
||||||
|
live_suite_filter: ${{ steps.inputs.outputs.live_suite_filter }}
|
||||||
steps:
|
steps:
|
||||||
- name: Require main or release workflow ref for release checks
|
- name: Require main or release workflow ref for release checks
|
||||||
env:
|
env:
|
||||||
@@ -192,6 +198,7 @@ jobs:
|
|||||||
RELEASE_MODE_INPUT: ${{ inputs.mode }}
|
RELEASE_MODE_INPUT: ${{ inputs.mode }}
|
||||||
RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }}
|
RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }}
|
||||||
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
|
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
|
||||||
|
RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
{
|
{
|
||||||
@@ -200,6 +207,7 @@ jobs:
|
|||||||
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
|
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
|
||||||
printf 'release_profile=%s\n' "$RELEASE_PROFILE_INPUT"
|
printf 'release_profile=%s\n' "$RELEASE_PROFILE_INPUT"
|
||||||
printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT"
|
printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT"
|
||||||
|
printf 'live_suite_filter=%s\n' "$RELEASE_LIVE_SUITE_FILTER_INPUT"
|
||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Summarize validated ref
|
- name: Summarize validated ref
|
||||||
@@ -211,6 +219,7 @@ jobs:
|
|||||||
RELEASE_MODE: ${{ inputs.mode }}
|
RELEASE_MODE: ${{ inputs.mode }}
|
||||||
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
||||||
RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }}
|
RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }}
|
||||||
|
RELEASE_LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
|
||||||
run: |
|
run: |
|
||||||
{
|
{
|
||||||
echo "## Release checks"
|
echo "## Release checks"
|
||||||
@@ -222,6 +231,9 @@ jobs:
|
|||||||
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
|
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
|
||||||
echo "- Release profile: \`${RELEASE_PROFILE}\`"
|
echo "- Release profile: \`${RELEASE_PROFILE}\`"
|
||||||
echo "- Rerun group: \`${RELEASE_RERUN_GROUP}\`"
|
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."
|
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"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
@@ -342,6 +354,7 @@ jobs:
|
|||||||
include_openwebui: false
|
include_openwebui: false
|
||||||
include_live_suites: true
|
include_live_suites: true
|
||||||
release_test_profile: ${{ needs.resolve_target.outputs.release_profile }}
|
release_test_profile: ${{ needs.resolve_target.outputs.release_profile }}
|
||||||
|
live_suite_filter: ${{ needs.resolve_target.outputs.live_suite_filter }}
|
||||||
secrets: &live_e2e_release_secrets
|
secrets: &live_e2e_release_secrets
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
|
||||||
|
|||||||
85
scripts/qa-parity-report.ts
Normal file
85
scripts/qa-parity-report.ts
Normal file
@@ -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 <path> Candidate qa-suite-summary.json path
|
||||||
|
--baseline-summary <path> Baseline qa-suite-summary.json path
|
||||||
|
--candidate-label <label> Candidate display label
|
||||||
|
--baseline-label <label> Baseline display label
|
||||||
|
--repo-root <path> Repository root to target
|
||||||
|
--output-dir <path> Artifact directory for the parity report
|
||||||
|
-h, --help Display help
|
||||||
|
`);
|
||||||
|
process.exit(0);
|
||||||
|
case "--baseline-label":
|
||||||
|
opts.baselineLabel = takeValue(args, index, arg);
|
||||||
|
index += 1;
|
||||||
|
break;
|
||||||
|
case "--baseline-summary":
|
||||||
|
opts.baselineSummary = takeValue(args, index, arg);
|
||||||
|
index += 1;
|
||||||
|
break;
|
||||||
|
case "--candidate-label":
|
||||||
|
opts.candidateLabel = takeValue(args, index, arg);
|
||||||
|
index += 1;
|
||||||
|
break;
|
||||||
|
case "--candidate-summary":
|
||||||
|
opts.candidateSummary = takeValue(args, index, arg);
|
||||||
|
index += 1;
|
||||||
|
break;
|
||||||
|
case "--output-dir":
|
||||||
|
opts.outputDir = takeValue(args, index, arg);
|
||||||
|
index += 1;
|
||||||
|
break;
|
||||||
|
case "--repo-root":
|
||||||
|
opts.repoRoot = takeValue(args, index, arg);
|
||||||
|
index += 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown qa parity-report option: ${arg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = parseArgs(process.argv.slice(2));
|
||||||
|
if (!opts.candidateSummary) {
|
||||||
|
throw new Error("--candidate-summary is required.");
|
||||||
|
}
|
||||||
|
if (!opts.baselineSummary) {
|
||||||
|
throw new Error("--baseline-summary is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await runQaParityReportCommand({
|
||||||
|
baselineSummary: opts.baselineSummary,
|
||||||
|
candidateSummary: opts.candidateSummary,
|
||||||
|
...(opts.baselineLabel ? { baselineLabel: opts.baselineLabel } : {}),
|
||||||
|
...(opts.candidateLabel ? { candidateLabel: opts.candidateLabel } : {}),
|
||||||
|
...(opts.outputDir ? { outputDir: opts.outputDir } : {}),
|
||||||
|
...(opts.repoRoot ? { repoRoot: opts.repoRoot } : {}),
|
||||||
|
});
|
||||||
@@ -809,6 +809,34 @@ const shouldUseExistingDistForGatewayClient = (deps, buildRequirement) =>
|
|||||||
deps.env.OPENCLAW_FORCE_BUILD !== "1" &&
|
deps.env.OPENCLAW_FORCE_BUILD !== "1" &&
|
||||||
statMtime(deps.distEntry, deps.fs) != null;
|
statMtime(deps.distEntry, deps.fs) != null;
|
||||||
|
|
||||||
|
const isQaParityReportCommand = (args) => args[0] === "qa" && args[1] === "parity-report";
|
||||||
|
|
||||||
|
const shouldRunQaParityReportFromSource = (deps, buildRequirement) =>
|
||||||
|
buildRequirement.reason === "missing_private_qa_dist" &&
|
||||||
|
isQaParityReportCommand(deps.args) &&
|
||||||
|
deps.env.OPENCLAW_FORCE_BUILD !== "1" &&
|
||||||
|
statMtime(path.join(deps.cwd, "extensions", "qa-lab", "src", "cli.runtime.ts"), deps.fs) != null;
|
||||||
|
|
||||||
|
const runQaParityReportFromSource = async (deps) => {
|
||||||
|
const sourceEntrypoint = path.join(deps.cwd, "scripts", "qa-parity-report.ts");
|
||||||
|
const nodeProcess = deps.spawn(
|
||||||
|
deps.execPath,
|
||||||
|
["--import", "tsx", sourceEntrypoint, ...deps.args.slice(2)],
|
||||||
|
{
|
||||||
|
cwd: deps.cwd,
|
||||||
|
env: deps.env,
|
||||||
|
stdio: deps.outputTee ? ["inherit", "pipe", "pipe"] : "inherit",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pipeSpawnedOutput(nodeProcess, deps);
|
||||||
|
const res = await waitForSpawnedProcess(nodeProcess, deps);
|
||||||
|
const interruptedExitCode = getInterruptedSpawnExitCode(res);
|
||||||
|
if (interruptedExitCode !== null) {
|
||||||
|
return interruptedExitCode;
|
||||||
|
}
|
||||||
|
return res.exitCode ?? 1;
|
||||||
|
};
|
||||||
|
|
||||||
export async function runNodeMain(params = {}) {
|
export async function runNodeMain(params = {}) {
|
||||||
const deps = {
|
const deps = {
|
||||||
spawn: params.spawn ?? spawn,
|
spawn: params.spawn ?? spawn,
|
||||||
@@ -847,9 +875,15 @@ export async function runNodeMain(params = {}) {
|
|||||||
deps,
|
deps,
|
||||||
buildRequirement,
|
buildRequirement,
|
||||||
);
|
);
|
||||||
|
const useQaParityReportSource = shouldRunQaParityReportFromSource(deps, buildRequirement);
|
||||||
if (useExistingGatewayClientDist) {
|
if (useExistingGatewayClientDist) {
|
||||||
buildRequirement = { shouldBuild: false, reason: "gateway_client_existing_dist" };
|
buildRequirement = { shouldBuild: false, reason: "gateway_client_existing_dist" };
|
||||||
}
|
}
|
||||||
|
if (useQaParityReportSource) {
|
||||||
|
logRunner("Running QA parity report from source without rebuilding private QA dist.", deps);
|
||||||
|
exitCode = await runQaParityReportFromSource(deps);
|
||||||
|
return await closeRunNodeOutputTee(deps, exitCode);
|
||||||
|
}
|
||||||
if (!buildRequirement.shouldBuild) {
|
if (!buildRequirement.shouldBuild) {
|
||||||
if (!useExistingGatewayClientDist && !shouldSkipCleanWatchRuntimeSync(deps)) {
|
if (!useExistingGatewayClientDist && !shouldSkipCleanWatchRuntimeSync(deps)) {
|
||||||
const runtimePostBuildRequirement = resolveRuntimePostBuildRequirement(deps);
|
const runtimePostBuildRequirement = resolveRuntimePostBuildRequirement(deps);
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
["scripts/run-oxlint.mjs", ["test/scripts/run-oxlint.test.ts"]],
|
["scripts/run-oxlint.mjs", ["test/scripts/run-oxlint.test.ts"]],
|
||||||
|
["scripts/run-node.mjs", ["src/infra/run-node.test.ts"]],
|
||||||
["scripts/ci-run-timings.mjs", ["test/scripts/ci-run-timings.test.ts"]],
|
["scripts/ci-run-timings.mjs", ["test/scripts/ci-run-timings.test.ts"]],
|
||||||
["scripts/test-extension-batch.mjs", ["test/scripts/test-extension.test.ts"]],
|
["scripts/test-extension-batch.mjs", ["test/scripts/test-extension.test.ts"]],
|
||||||
["scripts/lib/extension-test-plan.mjs", ["test/scripts/test-extension.test.ts"]],
|
["scripts/lib/extension-test-plan.mjs", ["test/scripts/test-extension.test.ts"]],
|
||||||
|
|||||||
@@ -125,6 +125,26 @@ describe("loadModelCatalog", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reloads dynamic registry entries after clearing the cache", async () => {
|
||||||
|
const models = [{ id: "existing", name: "Existing", provider: "ollama" }];
|
||||||
|
mockPiDiscoveryModels(models);
|
||||||
|
|
||||||
|
const first = await loadModelCatalog({ config: {} as OpenClawConfig });
|
||||||
|
expect(first).toContainEqual({ id: "existing", name: "Existing", provider: "ollama" });
|
||||||
|
|
||||||
|
models.push({ id: "glm-5.1:cloud", name: "GLM 5.1 Cloud", provider: "ollama" });
|
||||||
|
resetModelCatalogCacheForTest();
|
||||||
|
mockPiDiscoveryModels(models);
|
||||||
|
|
||||||
|
const second = await loadModelCatalog({ config: {} as OpenClawConfig });
|
||||||
|
expect(second).toContainEqual({ id: "existing", name: "Existing", provider: "ollama" });
|
||||||
|
expect(second).toContainEqual({
|
||||||
|
id: "glm-5.1:cloud",
|
||||||
|
name: "GLM 5.1 Cloud",
|
||||||
|
provider: "ollama",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("returns partial results on discovery errors", async () => {
|
it("returns partial results on discovery errors", async () => {
|
||||||
setLoggerOverride({ level: "silent", consoleLevel: "warn" });
|
setLoggerOverride({ level: "silent", consoleLevel: "warn" });
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ function loadModelSuppression() {
|
|||||||
export function resetModelCatalogCache() {
|
export function resetModelCatalogCache() {
|
||||||
modelCatalogPromise = null;
|
modelCatalogPromise = null;
|
||||||
hasLoggedModelCatalogError = false;
|
hasLoggedModelCatalogError = false;
|
||||||
importPiSdk = defaultImportPiSdk;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetModelCatalogCacheForTest() {
|
export function resetModelCatalogCacheForTest() {
|
||||||
resetModelCatalogCache();
|
resetModelCatalogCache();
|
||||||
|
importPiSdk = defaultImportPiSdk;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test-only escape hatch: allow mocking the dynamic import to simulate transient failures.
|
// Test-only escape hatch: allow mocking the dynamic import to simulate transient failures.
|
||||||
|
|||||||
@@ -51,6 +51,23 @@ function sanitizeModelWarningValue(value: string): string {
|
|||||||
return sanitizeForLog(stripped.slice(0, controlBoundary));
|
return sanitizeForLog(stripped.slice(0, controlBoundary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeModelCatalogEntries(params: {
|
||||||
|
primary: readonly ModelCatalogEntry[];
|
||||||
|
secondary: readonly ModelCatalogEntry[];
|
||||||
|
}): ModelCatalogEntry[] {
|
||||||
|
const merged = [...params.primary];
|
||||||
|
const seen = new Set(merged.map((entry) => modelKey(entry.provider, entry.id)));
|
||||||
|
for (const entry of params.secondary) {
|
||||||
|
const key = modelKey(entry.provider, entry.id);
|
||||||
|
if (seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
merged.push(entry);
|
||||||
|
seen.add(key);
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
export function inferUniqueProviderFromConfiguredModels(params: {
|
export function inferUniqueProviderFromConfiguredModels(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
model: string;
|
model: string;
|
||||||
@@ -565,7 +582,11 @@ export function buildAllowedModelSetWithFallbacks(params: {
|
|||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
defaultProvider: params.defaultProvider,
|
defaultProvider: params.defaultProvider,
|
||||||
});
|
});
|
||||||
const catalog = params.catalog.map((entry) => applyModelCatalogMetadata({ entry, metadata }));
|
const configuredCatalog = buildConfiguredModelCatalog({ cfg: params.cfg });
|
||||||
|
const catalog = mergeModelCatalogEntries({
|
||||||
|
primary: params.catalog,
|
||||||
|
secondary: configuredCatalog,
|
||||||
|
}).map((entry) => applyModelCatalogMetadata({ entry, metadata }));
|
||||||
const rawAllowlist = (() => {
|
const rawAllowlist = (() => {
|
||||||
const modelMap = params.cfg.agents?.defaults?.models ?? {};
|
const modelMap = params.cfg.agents?.defaults?.models ?? {};
|
||||||
return Object.keys(modelMap);
|
return Object.keys(modelMap);
|
||||||
|
|||||||
@@ -662,6 +662,51 @@ describe("model-selection", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps configured provider models visible when the catalog is otherwise allow-any", () => {
|
||||||
|
const cfg: OpenClawConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "ollama/existing" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
ollama: {
|
||||||
|
baseUrl: "http://127.0.0.1:11434",
|
||||||
|
api: "ollama",
|
||||||
|
apiKey: "ollama-local",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "glm-5.1:cloud",
|
||||||
|
name: "GLM 5.1 Cloud",
|
||||||
|
contextWindow: 131_072,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as OpenClawConfig;
|
||||||
|
|
||||||
|
const result = buildAllowedModelSet({
|
||||||
|
cfg,
|
||||||
|
catalog: [{ provider: "ollama", id: "existing", name: "Existing" }],
|
||||||
|
defaultProvider: "ollama",
|
||||||
|
defaultModel: "existing",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.allowAny).toBe(true);
|
||||||
|
expect(result.allowedCatalog).toEqual([
|
||||||
|
{ provider: "ollama", id: "existing", name: "Existing" },
|
||||||
|
{
|
||||||
|
provider: "ollama",
|
||||||
|
id: "glm-5.1:cloud",
|
||||||
|
name: "GLM 5.1 Cloud",
|
||||||
|
contextWindow: 131_072,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(result.allowedKeys.has("ollama/glm-5.1:cloud")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("matches allowlisted catalog entries with normalized provider and model ids", () => {
|
it("matches allowlisted catalog entries with normalized provider and model ids", () => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
|
|||||||
@@ -998,6 +998,36 @@ describe("chat directive tag stripping for non-streaming final payloads", () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("chat.send broadcasts final replies for telegram-shaped session keys", async () => {
|
||||||
|
createTranscriptFixture("openclaw-chat-send-telegram-final-");
|
||||||
|
mockState.finalText = "telegram ok";
|
||||||
|
const respond = vi.fn();
|
||||||
|
const context = createChatContext();
|
||||||
|
const sessionKey = "agent:main:telegram:direct:123456";
|
||||||
|
|
||||||
|
const payload = await runNonStreamingChatSend({
|
||||||
|
context,
|
||||||
|
respond,
|
||||||
|
idempotencyKey: "idem-telegram-final",
|
||||||
|
sessionKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(payload).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
runId: "idem-telegram-final",
|
||||||
|
sessionKey,
|
||||||
|
state: "final",
|
||||||
|
message: expect.any(Object),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(extractFirstTextBlock(payload)).toBe("telegram ok");
|
||||||
|
expect(context.nodeSendToSession).toHaveBeenCalledWith(
|
||||||
|
sessionKey,
|
||||||
|
"chat",
|
||||||
|
expect.objectContaining({ sessionKey, state: "final" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("chat.send keeps explicit delivery routes for channel-scoped sessions", async () => {
|
it("chat.send keeps explicit delivery routes for channel-scoped sessions", async () => {
|
||||||
createTranscriptFixture("openclaw-chat-send-origin-routing-");
|
createTranscriptFixture("openclaw-chat-send-origin-routing-");
|
||||||
mockState.finalText = "ok";
|
mockState.finalText = "ok";
|
||||||
|
|||||||
@@ -2,13 +2,7 @@ import fs from "node:fs/promises";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import {
|
|
||||||
__setModelCatalogImportForTest,
|
|
||||||
resetModelCatalogCacheForTest,
|
|
||||||
} from "../agents/model-catalog.js";
|
|
||||||
import { buildModelsProviderData } from "../auto-reply/reply/commands-models.js";
|
|
||||||
import { resolveMainSessionKeyFromConfig } from "../config/sessions.js";
|
import { resolveMainSessionKeyFromConfig } from "../config/sessions.js";
|
||||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
||||||
import { drainSystemEvents } from "../infra/system-events.js";
|
import { drainSystemEvents } from "../infra/system-events.js";
|
||||||
import { withEnvAsync } from "../test-utils/env.js";
|
import { withEnvAsync } from "../test-utils/env.js";
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +19,7 @@ import {
|
|||||||
rpcReq,
|
rpcReq,
|
||||||
startServerWithClient,
|
startServerWithClient,
|
||||||
testState,
|
testState,
|
||||||
withGatewayServer,
|
withGatewayServer as withMinimalGatewayServer,
|
||||||
} from "./test-helpers.js";
|
} from "./test-helpers.js";
|
||||||
|
|
||||||
const hoisted = vi.hoisted(() => {
|
const hoisted = vi.hoisted(() => {
|
||||||
@@ -165,11 +159,13 @@ const hoisted = vi.hoisted(() => {
|
|||||||
reloaderStop,
|
reloaderStop,
|
||||||
getOnHotReload: () => onHotReload,
|
getOnHotReload: () => onHotReload,
|
||||||
getOnRestart: () => onRestart,
|
getOnRestart: () => onRestart,
|
||||||
|
resetReloadCallbacks: () => {
|
||||||
|
onHotReload = null;
|
||||||
|
onRestart = null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
type PiDiscoveryRuntimeModule = typeof import("../agents/pi-model-discovery-runtime.js");
|
|
||||||
|
|
||||||
vi.mock("../cron/service.js", () => ({
|
vi.mock("../cron/service.js", () => ({
|
||||||
CronService: hoisted.CronService,
|
CronService: hoisted.CronService,
|
||||||
}));
|
}));
|
||||||
@@ -310,6 +306,7 @@ describe("gateway hot reload", () => {
|
|||||||
hoisted.resetModelCatalogCache.mockReset();
|
hoisted.resetModelCatalogCache.mockReset();
|
||||||
hoisted.disposeAllSessionMcpRuntimes.mockReset();
|
hoisted.disposeAllSessionMcpRuntimes.mockReset();
|
||||||
hoisted.disposeAllSessionMcpRuntimes.mockResolvedValue(undefined);
|
hoisted.disposeAllSessionMcpRuntimes.mockResolvedValue(undefined);
|
||||||
|
hoisted.resetReloadCallbacks();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -430,10 +427,10 @@ describe("gateway hot reload", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function withNonMinimalGatewayServer(
|
async function withNonMinimalGatewayServer(
|
||||||
fn: Parameters<typeof withGatewayServer>[0],
|
fn: Parameters<typeof withMinimalGatewayServer>[0],
|
||||||
): ReturnType<typeof withGatewayServer> {
|
): ReturnType<typeof withMinimalGatewayServer> {
|
||||||
return await withEnvAsync({ OPENCLAW_TEST_MINIMAL_GATEWAY: undefined }, async () =>
|
return await withEnvAsync({ OPENCLAW_TEST_MINIMAL_GATEWAY: undefined }, async () =>
|
||||||
withGatewayServer(fn),
|
withMinimalGatewayServer(fn),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,7 +816,7 @@ describe("gateway hot reload", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("clears the model catalog cache on model-related hot reloads", async () => {
|
it("clears the model catalog cache on model-related hot reloads", async () => {
|
||||||
await withGatewayServer(async () => {
|
await withNonMinimalGatewayServer(async () => {
|
||||||
const onHotReload = hoisted.getOnHotReload();
|
const onHotReload = hoisted.getOnHotReload();
|
||||||
expect(onHotReload).toBeTypeOf("function");
|
expect(onHotReload).toBeTypeOf("function");
|
||||||
|
|
||||||
@@ -852,7 +849,7 @@ describe("gateway hot reload", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("disposes cached MCP runtimes on MCP config hot reloads", async () => {
|
it("disposes cached MCP runtimes on MCP config hot reloads", async () => {
|
||||||
await withGatewayServer(async () => {
|
await withNonMinimalGatewayServer(async () => {
|
||||||
const onHotReload = hoisted.getOnHotReload();
|
const onHotReload = hoisted.getOnHotReload();
|
||||||
expect(onHotReload).toBeTypeOf("function");
|
expect(onHotReload).toBeTypeOf("function");
|
||||||
|
|
||||||
@@ -882,108 +879,6 @@ describe("gateway hot reload", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("makes newly available catalog models visible in-process after hot reload", async () => {
|
|
||||||
type TestRegistryEntry = { provider: string; id: string; name: string };
|
|
||||||
let registryEntries: TestRegistryEntry[] = [
|
|
||||||
{ provider: "ollama", id: "existing", name: "Existing" },
|
|
||||||
];
|
|
||||||
__setModelCatalogImportForTest(
|
|
||||||
async () =>
|
|
||||||
({
|
|
||||||
discoverAuthStorage: () => ({}),
|
|
||||||
ModelRegistry: class {
|
|
||||||
getAll() {
|
|
||||||
return registryEntries;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}) as unknown as PiDiscoveryRuntimeModule,
|
|
||||||
);
|
|
||||||
resetModelCatalogCacheForTest();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await withGatewayServer(async () => {
|
|
||||||
const onHotReload = hoisted.getOnHotReload();
|
|
||||||
expect(onHotReload).toBeTypeOf("function");
|
|
||||||
|
|
||||||
const baseConfig: OpenClawConfig = {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: {
|
|
||||||
primary: "ollama/existing",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
providers: {
|
|
||||||
ollama: {
|
|
||||||
baseUrl: "http://127.0.0.1:11434",
|
|
||||||
api: "ollama",
|
|
||||||
apiKey: "ollama-local",
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "existing",
|
|
||||||
name: "Existing",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 131_072,
|
|
||||||
maxTokens: 4096,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const before = await buildModelsProviderData(baseConfig);
|
|
||||||
expect([...(before.byProvider.get("ollama") ?? new Set()).values()]).toEqual(["existing"]);
|
|
||||||
|
|
||||||
registryEntries = [
|
|
||||||
...registryEntries,
|
|
||||||
{ provider: "ollama", id: "glm-5.1:cloud", name: "GLM 5.1 Cloud" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const nextConfig = structuredClone(baseConfig);
|
|
||||||
await onHotReload?.(
|
|
||||||
{
|
|
||||||
changedPaths: ["models.providers.ollama.models"],
|
|
||||||
restartGateway: false,
|
|
||||||
restartReasons: [],
|
|
||||||
hotReasons: ["models.providers.ollama.models"],
|
|
||||||
reloadHooks: false,
|
|
||||||
restartGmailWatcher: false,
|
|
||||||
restartCron: false,
|
|
||||||
restartHeartbeat: false,
|
|
||||||
restartChannels: new Set(),
|
|
||||||
noopPaths: [],
|
|
||||||
},
|
|
||||||
nextConfig,
|
|
||||||
);
|
|
||||||
|
|
||||||
__setModelCatalogImportForTest(
|
|
||||||
async () =>
|
|
||||||
({
|
|
||||||
discoverAuthStorage: () => ({}),
|
|
||||||
ModelRegistry: class {
|
|
||||||
getAll() {
|
|
||||||
return registryEntries;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}) as unknown as PiDiscoveryRuntimeModule,
|
|
||||||
);
|
|
||||||
const after = await buildModelsProviderData(nextConfig);
|
|
||||||
expect([...(after.byProvider.get("ollama") ?? new Set()).values()]).toEqual([
|
|
||||||
"existing",
|
|
||||||
"glm-5.1:cloud",
|
|
||||||
]);
|
|
||||||
expect(hoisted.resetModelCatalogCache).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
__setModelCatalogImportForTest();
|
|
||||||
resetModelCatalogCacheForTest();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("serves secrets.reload immediately after startup without race failures", async () => {
|
it("serves secrets.reload immediately after startup without race failures", async () => {
|
||||||
await writeEnvRefConfig();
|
await writeEnvRefConfig();
|
||||||
process.env.OPENAI_API_KEY = "sk-startup"; // pragma: allowlist secret
|
process.env.OPENAI_API_KEY = "sk-startup"; // pragma: allowlist secret
|
||||||
|
|||||||
@@ -697,6 +697,56 @@ describe("run-node script", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs QA parity report from source without rebuilding private QA dist", async () => {
|
||||||
|
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
||||||
|
await setupTrackedProject(tmp, {
|
||||||
|
files: {
|
||||||
|
"extensions/qa-lab/src/cli.runtime.ts": "export {};\n",
|
||||||
|
},
|
||||||
|
buildPaths: [DIST_ENTRY, BUILD_STAMP],
|
||||||
|
});
|
||||||
|
|
||||||
|
const spawnCalls: string[][] = [];
|
||||||
|
const spawn = (cmd: string, args: string[]) => {
|
||||||
|
spawnCalls.push([cmd, ...args]);
|
||||||
|
return createExitedProcess(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const exitCode = await runNodeMain({
|
||||||
|
cwd: tmp,
|
||||||
|
args: [
|
||||||
|
"qa",
|
||||||
|
"parity-report",
|
||||||
|
"--candidate-summary",
|
||||||
|
".artifacts/qa-e2e/gpt54/qa-suite-summary.json",
|
||||||
|
"--baseline-summary",
|
||||||
|
".artifacts/qa-e2e/opus46/qa-suite-summary.json",
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
OPENCLAW_RUNNER_LOG: "0",
|
||||||
|
},
|
||||||
|
spawn,
|
||||||
|
execPath: process.execPath,
|
||||||
|
platform: process.platform,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(spawnCalls).toEqual([
|
||||||
|
[
|
||||||
|
process.execPath,
|
||||||
|
"--import",
|
||||||
|
"tsx",
|
||||||
|
path.join(tmp, "scripts", "qa-parity-report.ts"),
|
||||||
|
"--candidate-summary",
|
||||||
|
".artifacts/qa-e2e/gpt54/qa-suite-summary.json",
|
||||||
|
"--baseline-summary",
|
||||||
|
".artifacts/qa-e2e/opus46/qa-suite-summary.json",
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("skips runtime postbuild restaging when the runtime stamp is current", async () => {
|
it("skips runtime postbuild restaging when the runtime stamp is current", async () => {
|
||||||
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
|
||||||
await setupTrackedProject(tmp, {
|
await setupTrackedProject(tmp, {
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import { afterAll, describe, expect, it } from "vitest";
|
import { afterAll, describe, expect, it } from "vitest";
|
||||||
import { GatewayClient } from "../src/gateway/client.js";
|
import { GatewayClient } from "../src/gateway/client.js";
|
||||||
import { connectGatewayClient } from "../src/gateway/test-helpers.e2e.js";
|
|
||||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js";
|
|
||||||
import {
|
import {
|
||||||
type ChatEventPayload,
|
|
||||||
type GatewayInstance,
|
type GatewayInstance,
|
||||||
connectNode,
|
connectNode,
|
||||||
extractFirstTextBlock,
|
|
||||||
postJson,
|
postJson,
|
||||||
spawnGatewayInstance,
|
spawnGatewayInstance,
|
||||||
stopGatewayInstance,
|
stopGatewayInstance,
|
||||||
waitForChatFinalEvent,
|
|
||||||
waitForNodeStatus,
|
waitForNodeStatus,
|
||||||
} from "./helpers/gateway-e2e-harness.js";
|
} from "./helpers/gateway-e2e-harness.js";
|
||||||
|
|
||||||
@@ -20,15 +14,11 @@ const E2E_TIMEOUT_MS = 120_000;
|
|||||||
describe("gateway multi-instance e2e", () => {
|
describe("gateway multi-instance e2e", () => {
|
||||||
const instances: GatewayInstance[] = [];
|
const instances: GatewayInstance[] = [];
|
||||||
const nodeClients: GatewayClient[] = [];
|
const nodeClients: GatewayClient[] = [];
|
||||||
const chatClients: GatewayClient[] = [];
|
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
for (const client of nodeClients) {
|
for (const client of nodeClients) {
|
||||||
client.stop();
|
client.stop();
|
||||||
}
|
}
|
||||||
for (const client of chatClients) {
|
|
||||||
client.stop();
|
|
||||||
}
|
|
||||||
for (const inst of instances) {
|
for (const inst of instances) {
|
||||||
await stopGatewayInstance(inst);
|
await stopGatewayInstance(inst);
|
||||||
}
|
}
|
||||||
@@ -76,51 +66,4 @@ describe("gateway multi-instance e2e", () => {
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it(
|
|
||||||
"delivers final chat event for telegram-shaped session keys",
|
|
||||||
{ timeout: E2E_TIMEOUT_MS },
|
|
||||||
async () => {
|
|
||||||
const gw = await spawnGatewayInstance("chat-telegram-fixture");
|
|
||||||
instances.push(gw);
|
|
||||||
|
|
||||||
const chatEvents: ChatEventPayload[] = [];
|
|
||||||
const chatClient = await connectGatewayClient({
|
|
||||||
url: `ws://127.0.0.1:${gw.port}`,
|
|
||||||
token: gw.gatewayToken,
|
|
||||||
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
|
||||||
clientDisplayName: "chat-e2e-cli",
|
|
||||||
clientVersion: "1.0.0",
|
|
||||||
platform: "test",
|
|
||||||
mode: GATEWAY_CLIENT_MODES.CLI,
|
|
||||||
onEvent: (evt) => {
|
|
||||||
if (evt.event === "chat" && evt.payload && typeof evt.payload === "object") {
|
|
||||||
chatEvents.push(evt.payload as ChatEventPayload);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
chatClients.push(chatClient);
|
|
||||||
|
|
||||||
const sessionKey = "agent:main:telegram:direct:123456";
|
|
||||||
const idempotencyKey = `idem-${randomUUID()}`;
|
|
||||||
const sendRes = await chatClient.request("chat.send", {
|
|
||||||
sessionKey,
|
|
||||||
message: "/whoami",
|
|
||||||
idempotencyKey,
|
|
||||||
});
|
|
||||||
expect(sendRes.status).toBe("started");
|
|
||||||
const runId = sendRes.runId;
|
|
||||||
expect(typeof runId).toBe("string");
|
|
||||||
|
|
||||||
const finalEvent = await waitForChatFinalEvent({
|
|
||||||
events: chatEvents,
|
|
||||||
runId: String(runId),
|
|
||||||
sessionKey,
|
|
||||||
timeoutMs: 90_000,
|
|
||||||
});
|
|
||||||
const finalText = extractFirstTextBlock(finalEvent.message);
|
|
||||||
expect(typeof finalText).toBe("string");
|
|
||||||
expect(finalText?.length).toBeGreaterThan(0);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ describe("package artifact reuse", () => {
|
|||||||
"command: node .release-harness/scripts/test-live-shard.mjs native-live-src-agents",
|
"command: node .release-harness/scripts/test-live-shard.mjs native-live-src-agents",
|
||||||
);
|
);
|
||||||
expect(workflow).toContain("OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}");
|
expect(workflow).toContain("OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}");
|
||||||
|
expect(workflow).toContain("live_suite_filter:");
|
||||||
|
expect(workflow).toContain(
|
||||||
|
"inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id",
|
||||||
|
);
|
||||||
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.5");
|
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.5");
|
||||||
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key");
|
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key");
|
||||||
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1");
|
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1");
|
||||||
@@ -157,6 +161,9 @@ describe("package artifact reuse", () => {
|
|||||||
expect(workflow).toContain("suite_id: native-live-extensions-a-k");
|
expect(workflow).toContain("suite_id: native-live-extensions-a-k");
|
||||||
expect(workflow).toContain("suite_id: native-live-extensions-l-n");
|
expect(workflow).toContain("suite_id: native-live-extensions-l-n");
|
||||||
expect(workflow).toContain("suite_id: native-live-extensions-moonshot");
|
expect(workflow).toContain("suite_id: native-live-extensions-moonshot");
|
||||||
|
expect(workflow).toMatch(/suite_id: native-live-extensions-moonshot[\s\S]*?advisory: true/u);
|
||||||
|
expect(workflow).toContain("OPENCLAW_LIVE_SUITE_ADVISORY: ${{ matrix.advisory }}");
|
||||||
|
expect(workflow).toContain("Advisory live suite failed with exit code");
|
||||||
expect(workflow).toContain("suite_id: native-live-extensions-openai");
|
expect(workflow).toContain("suite_id: native-live-extensions-openai");
|
||||||
expect(workflow).toContain("suite_id: native-live-extensions-o-z-other");
|
expect(workflow).toContain("suite_id: native-live-extensions-o-z-other");
|
||||||
expect(workflow).toContain("validate_live_media_provider_suites:");
|
expect(workflow).toContain("validate_live_media_provider_suites:");
|
||||||
@@ -299,6 +306,10 @@ describe("package artifact reuse", () => {
|
|||||||
"OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}",
|
"OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}",
|
||||||
);
|
);
|
||||||
expect(workflow).toContain("rerun_group:");
|
expect(workflow).toContain("rerun_group:");
|
||||||
|
expect(workflow).toContain("live_suite_filter:");
|
||||||
|
expect(workflow).toContain(
|
||||||
|
"live_suite_filter: ${{ needs.resolve_target.outputs.live_suite_filter }}",
|
||||||
|
);
|
||||||
expect(workflow).toContain("- live-e2e");
|
expect(workflow).toContain("- live-e2e");
|
||||||
expect(workflow).toContain("- qa-live");
|
expect(workflow).toContain("- qa-live");
|
||||||
});
|
});
|
||||||
@@ -347,8 +358,11 @@ describe("package artifact reuse", () => {
|
|||||||
expect(workflow).toContain('-f harness_ref="$TARGET_SHA"');
|
expect(workflow).toContain('-f harness_ref="$TARGET_SHA"');
|
||||||
expect(workflow).toContain("child_rerun_group=all");
|
expect(workflow).toContain("child_rerun_group=all");
|
||||||
expect(workflow).toContain('-f rerun_group="$child_rerun_group"');
|
expect(workflow).toContain('-f rerun_group="$child_rerun_group"');
|
||||||
|
expect(workflow).toContain('args+=(-f live_suite_filter="$LIVE_SUITE_FILTER")');
|
||||||
|
expect(workflow).toContain("cancel-in-progress: false");
|
||||||
|
expect(workflow).not.toContain("gh run cancel");
|
||||||
|
expect(workflow).not.toContain("force-cancel");
|
||||||
expect(workflow).toContain("NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}");
|
expect(workflow).toContain("NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}");
|
||||||
expect(workflow.match(/trap - EXIT INT TERM/g)?.length ?? 0).toBeGreaterThanOrEqual(6);
|
|
||||||
expect(workflow).not.toContain("workflow_ref:");
|
expect(workflow).not.toContain("workflow_ref:");
|
||||||
expect(workflow).not.toContain("inputs.workflow_ref");
|
expect(workflow).not.toContain("inputs.workflow_ref");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user