ci(qa): reuse Matrix live workflow for releases (#103604)

* ci(qa): reuse Matrix live workflow for releases

* docs(testing): restore Tailscale isolation flag

* fix(ci): make Matrix artifact names ref-safe

* test(ci): type Matrix workflow profiles

* style(testing): format Tailscale isolation guidance
This commit is contained in:
Dallin Romney
2026-07-15 01:48:59 -07:00
committed by GitHub
parent bef86c8b88
commit 6c4e76ea8b
10 changed files with 320 additions and 194 deletions
+17 -98
View File
@@ -1468,104 +1468,24 @@ jobs:
retention-days: 14
if-no-files-found: error
qa_live_matrix_release_checks:
name: Run QA Lab live Matrix lane
qa_live_release_checks:
name: Run QA Live Matrix profile
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_matrix_enabled == 'true'
continue-on-error: true
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
contents: read
pull-requests: read
environment: qa-live-shared
env:
OPENCLAW_BUILD_PRIVATE_QA: "1"
OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1"
steps:
- name: Checkout selected ref
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ needs.resolve_target.outputs.revision }}
fetch-depth: 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: ${{ env.NODE_VERSION }}
install-bun: "true"
- name: Build private QA runtime
env:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
- name: Run Matrix live lane
id: run_lane
shell: bash
env:
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS: "90000"
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
matrix_args=(
--repo-root . \
--provider-mode mock-openai \
--model mock-openai/gpt-5.6-luna \
--alt-model mock-openai/gpt-5.6-luna-alt \
--profile fast \
--fast
)
if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then
matrix_args+=(--fail-fast)
fi
for attempt in 1 2; do
attempt_output_dir="${output_dir}/attempt-${attempt}"
if pnpm openclaw qa matrix --output-dir "${attempt_output_dir}" "${matrix_args[@]}"; then
exit 0
fi
if [[ "${attempt}" == "2" ]]; then
exit 1
fi
echo "Matrix live lane failed on attempt ${attempt}; retrying once..." >&2
sleep 10
done
- name: Upload Matrix QA artifacts
id: upload_matrix_qa_artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: error
- name: Record advisory status
if: always()
shell: bash
env:
RELEASE_CHECK_JOB: qa_live_matrix_release_checks
RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }}
JOB_STATUS: ${{ job.status }}
RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_matrix_qa_artifacts.outcome }}
run: *record_release_check_advisory_status
- name: Upload advisory status
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-check-status-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }}
path: .artifacts/release-check-status/qa_live_matrix_release_checks-${{ github.run_id }}-${{ github.run_attempt }}.env
retention-days: 14
if-no-files-found: error
uses: ./.github/workflows/qa-live-transports-convex.yml
with:
ref: ${{ needs.resolve_target.outputs.revision }}
expected_sha: ${{ needs.resolve_target.outputs.revision }}
matrix_profile: release
matrix_provider_mode: mock-openai
matrix_primary_model: mock-openai/gpt-5.6-luna
matrix_alternate_model: mock-openai/gpt-5.6-luna-alt
matrix_attempts: 2
run_matrix: true
matrix_advisory: true
# The dispatched child owns Telegram evidence/status artifacts; this blocking job
# carries its exact conclusion into the parent summary without copying secrets or artifacts.
@@ -2022,7 +1942,7 @@ jobs:
- qa_lab_parity_report_release_checks
- qa_lab_runtime_parity_release_checks
- runtime_tool_coverage_release_checks
- qa_live_matrix_release_checks
- qa_live_release_checks
- qa_live_telegram_release_checks
- qa_live_discord_release_checks
- qa_live_whatsapp_release_checks
@@ -2057,7 +1977,7 @@ jobs:
QA_LAB_PARITY_REPORT_RELEASE_CHECKS_RESULT: ${{ needs.qa_lab_parity_report_release_checks.result }}
QA_LAB_RUNTIME_PARITY_RELEASE_CHECKS_RESULT: ${{ needs.qa_lab_runtime_parity_release_checks.result }}
RUNTIME_TOOL_COVERAGE_RELEASE_CHECKS_RESULT: ${{ needs.runtime_tool_coverage_release_checks.result }}
QA_LIVE_MATRIX_RELEASE_CHECKS_RESULT: ${{ needs.qa_live_matrix_release_checks.result }}
QA_LIVE_RELEASE_CHECKS_RESULT: ${{ needs.qa_live_release_checks.result == 'skipped' && 'skipped' || needs.qa_live_release_checks.outputs.matrix_status || 'failure' }}
QA_LIVE_TELEGRAM_RELEASE_CHECKS_RESULT: ${{ needs.qa_live_telegram_release_checks.result }}
QA_LIVE_TELEGRAM_SELECTED: ${{ contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_telegram_enabled == 'true' }}
QA_LIVE_DISCORD_RELEASE_CHECKS_RESULT: ${{ needs.qa_live_discord_release_checks.result }}
@@ -2083,7 +2003,7 @@ jobs:
"qa_lab_parity_report_release_checks=${QA_LAB_PARITY_REPORT_RELEASE_CHECKS_RESULT}"
"qa_lab_runtime_parity_release_checks=${QA_LAB_RUNTIME_PARITY_RELEASE_CHECKS_RESULT}"
"runtime_tool_coverage_release_checks=${RUNTIME_TOOL_COVERAGE_RELEASE_CHECKS_RESULT}"
"qa_live_matrix_release_checks=${QA_LIVE_MATRIX_RELEASE_CHECKS_RESULT}"
"qa_live_release_checks=${QA_LIVE_RELEASE_CHECKS_RESULT}"
"qa_live_telegram_release_checks=${QA_LIVE_TELEGRAM_RELEASE_CHECKS_RESULT}"
"qa_live_discord_release_checks=${QA_LIVE_DISCORD_RELEASE_CHECKS_RESULT}"
"qa_live_whatsapp_release_checks=${QA_LIVE_WHATSAPP_RELEASE_CHECKS_RESULT}"
@@ -2093,7 +2013,6 @@ jobs:
"qa_lab_parity_lane_release_checks=${QA_LAB_PARITY_LANE_RELEASE_CHECKS_RESULT}"
"qa_lab_parity_report_release_checks=${QA_LAB_PARITY_REPORT_RELEASE_CHECKS_RESULT}"
"qa_lab_runtime_parity_release_checks=${QA_LAB_RUNTIME_PARITY_RELEASE_CHECKS_RESULT}"
"qa_live_matrix_release_checks=${QA_LIVE_MATRIX_RELEASE_CHECKS_RESULT}"
"qa_live_discord_release_checks=${QA_LIVE_DISCORD_RELEASE_CHECKS_RESULT}"
"qa_live_whatsapp_release_checks=${QA_LIVE_WHATSAPP_RELEASE_CHECKS_RESULT}"
"qa_live_slack_release_checks=${QA_LIVE_SLACK_RELEASE_CHECKS_RESULT}"
@@ -2247,7 +2166,7 @@ jobs:
}
advisory_status_override_allowed() {
case "$1" in
qa_lab_parity_lane_release_checks|qa_lab_parity_report_release_checks|qa_lab_runtime_parity_release_checks|qa_live_matrix_release_checks|qa_live_discord_release_checks|qa_live_whatsapp_release_checks|qa_live_slack_release_checks)
qa_lab_parity_lane_release_checks|qa_lab_parity_report_release_checks|qa_lab_runtime_parity_release_checks|qa_live_discord_release_checks|qa_live_whatsapp_release_checks|qa_live_slack_release_checks)
return 0
;;
*)
+174 -40
View File
@@ -3,6 +3,80 @@ name: QA-Lab - All Lanes
on:
schedule:
- cron: "41 4 * * *"
workflow_call:
outputs:
matrix_status:
description: Matrix release profile status
value: ${{ jobs.run_live_matrix.outputs.status }}
inputs:
ref:
description: Ref, tag, or SHA to run
required: true
type: string
expected_sha:
description: Exact resolved SHA expected from the trusted caller
required: true
type: string
matrix_profile:
description: Matrix QA profile
required: false
default: release
type: string
matrix_provider_mode:
description: Matrix provider mode
required: false
default: mock-openai
type: string
matrix_primary_model:
description: Matrix primary model
required: false
default: mock-openai/gpt-5.6-luna
type: string
matrix_alternate_model:
description: Matrix alternate model
required: false
default: mock-openai/gpt-5.6-luna-alt
type: string
matrix_attempts:
description: Matrix lane attempts
required: false
default: 2
type: number
run_mock_parity:
description: Run the mock parity lane
required: false
default: false
type: boolean
run_matrix:
description: Run the Matrix live lane
required: false
default: false
type: boolean
run_telegram:
description: Run the Telegram live lane
required: false
default: false
type: boolean
run_discord:
description: Run the Discord live lane
required: false
default: false
type: boolean
run_whatsapp:
description: Run the WhatsApp live lane
required: false
default: false
type: boolean
run_slack:
description: Run the Slack live lane
required: false
default: false
type: boolean
matrix_advisory:
description: Report Matrix failure through the status output instead of failing the reusable job
required: false
default: false
type: boolean
workflow_dispatch:
inputs:
ref:
@@ -33,19 +107,16 @@ on:
type: choice
options:
- fast
- release
- all
- transport
- media
- e2ee-smoke
- e2ee-deep
- e2ee-cli
permissions:
contents: read
pull-requests: read
concurrency:
group: qa-lab-all-lanes-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
group: qa-lab-all-lanes-${{ github.event_name != 'schedule' && inputs.ref || github.sha }}
cancel-in-progress: false
env:
@@ -66,10 +137,12 @@ jobs:
- name: Require maintainer-level repository access
id: permission
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
with:
script: |
if (context.eventName === "schedule") {
core.info("Scheduled default-branch QA run; actor permission check is only required for manual dispatch.");
if (context.eventName === "schedule" || (process.env.EXPECTED_SHA ?? "") !== "") {
core.info("Trusted scheduled or reusable QA run; actor permission check is only required for manual dispatch.");
core.setOutput("authorized", "true");
return;
}
@@ -104,23 +177,37 @@ jobs:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
ref: ${{ github.event_name != 'schedule' && inputs.ref || github.sha }}
fetch-depth: 0
- name: Validate selected ref
id: validate
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
GH_TOKEN: ${{ github.token }}
INPUT_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
INPUT_REF: ${{ github.event_name != 'schedule' && inputs.ref || github.sha }}
shell: bash
run: |
set -euo pipefail
selected_revision="$(git rev-parse HEAD)"
trusted_reason=""
if [[ -n "${EXPECTED_SHA}" && "${selected_revision}" != "${EXPECTED_SHA}" ]]; then
echo "Ref '${INPUT_REF}' resolved to ${selected_revision}, expected ${EXPECTED_SHA}." >&2
exit 1
fi
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
if [[ -n "${EXPECTED_SHA}" ]]; then
git fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*'
git fetch --tags origin '+refs/tags/*:refs/tags/*'
if git tag --points-at "$selected_revision" | grep -Eq '^v'; then
trusted_reason="release-tag"
elif git for-each-ref --format='%(refname:short)' --contains "$selected_revision" refs/remotes/origin | grep -Eq '^origin/'; then
trusted_reason="repository-branch"
fi
elif git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
trusted_reason="main-ancestor"
elif git tag --points-at "$selected_revision" | grep -Eq '^v'; then
trusted_reason="release-tag"
@@ -159,6 +246,7 @@ jobs:
run_mock_parity:
name: Run QA Lab mock parity lane
needs: [validate_selected_ref]
if: inputs.expected_sha == '' || inputs.run_mock_parity
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 30
env:
@@ -322,10 +410,13 @@ jobs:
run_live_matrix:
name: Run Matrix live QA lane
needs: [authorize_actor, validate_selected_ref]
if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.matrix_profile == 'all') }}
if: (github.event_name != 'workflow_call' || inputs.run_matrix) && !(github.event_name == 'workflow_dispatch' && inputs.matrix_profile == 'all')
continue-on-error: ${{ github.event_name == 'workflow_call' && inputs.matrix_advisory }}
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
outputs:
status: ${{ steps.record_status.outputs.status }}
steps:
- name: Checkout selected ref
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
@@ -342,12 +433,13 @@ jobs:
- name: Validate required QA credential env
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MATRIX_PROVIDER_MODE: ${{ inputs.expected_sha != '' && inputs.matrix_provider_mode || github.event_name == 'workflow_dispatch' && 'mock-openai' || 'live-frontier' }}
OPENAI_API_KEY: ${{ (inputs.expected_sha != '' && inputs.matrix_provider_mode == 'live-frontier' || inputs.expected_sha == '' && github.event_name != 'workflow_dispatch') && secrets.OPENAI_API_KEY || '' }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
if [[ "${MATRIX_PROVIDER_MODE}" == "live-frontier" && -z "${OPENAI_API_KEY:-}" ]]; then
echo "Missing required OPENAI_API_KEY." >&2
exit 1
fi
@@ -361,40 +453,85 @@ jobs:
id: run_lane
shell: bash
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
INPUT_MATRIX_PROFILE: ${{ github.event_name == 'workflow_dispatch' && inputs.matrix_profile || 'fast' }}
OPENAI_API_KEY: ${{ (inputs.expected_sha != '' && inputs.matrix_provider_mode == 'live-frontier' || inputs.expected_sha == '' && github.event_name != 'workflow_dispatch') && secrets.OPENAI_API_KEY || '' }}
INPUT_MATRIX_PROFILE: ${{ inputs.expected_sha != '' && inputs.matrix_profile || github.event_name == 'workflow_dispatch' && inputs.matrix_profile || 'release' }}
MATRIX_ALTERNATE_MODEL: ${{ inputs.expected_sha != '' && inputs.matrix_alternate_model || github.event_name == 'workflow_dispatch' && 'mock-openai/gpt-5.6-luna-alt' || env.OPENCLAW_CI_OPENAI_FALLBACK_MODEL }}
MATRIX_ATTEMPTS: ${{ inputs.expected_sha != '' && inputs.matrix_attempts || 1 }}
MATRIX_PRIMARY_MODEL: ${{ inputs.expected_sha != '' && inputs.matrix_primary_model || github.event_name == 'workflow_dispatch' && 'mock-openai/gpt-5.6-luna' || env.OPENCLAW_CI_OPENAI_MODEL }}
MATRIX_PROVIDER_MODE: ${{ inputs.expected_sha != '' && inputs.matrix_provider_mode || github.event_name == 'workflow_dispatch' && 'mock-openai' || 'live-frontier' }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
if [[ ! "${MATRIX_ATTEMPTS}" =~ ^[1-9][0-9]*$ ]]; then
echo "matrix_attempts must be a positive integer; got: ${MATRIX_ATTEMPTS}" >&2
exit 1
fi
case "${MATRIX_PROVIDER_MODE}" in
mock-openai|live-frontier) ;;
*)
echo "matrix_provider_mode must be mock-openai or live-frontier; got: ${MATRIX_PROVIDER_MODE}" >&2
exit 1
;;
esac
output_dir=".artifacts/qa-e2e/matrix-live-${INPUT_MATRIX_PROFILE}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
matrix_args=(
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
--model "${OPENCLAW_CI_OPENAI_MODEL}" \
--alt-model "${OPENCLAW_CI_OPENAI_FALLBACK_MODEL}" \
--profile "${INPUT_MATRIX_PROFILE}" \
--fast
)
if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then
matrix_args+=(--fail-fast)
fi
pnpm openclaw qa matrix "${matrix_args[@]}"
for attempt in $(seq 1 "${MATRIX_ATTEMPTS}"); do
attempt_output_dir="${output_dir}/attempt-${attempt}"
if pnpm openclaw qa matrix \
--repo-root . \
--output-dir "${attempt_output_dir}" \
--provider-mode "${MATRIX_PROVIDER_MODE}" \
--model "${MATRIX_PRIMARY_MODEL}" \
--alt-model "${MATRIX_ALTERNATE_MODEL}" \
--profile "${INPUT_MATRIX_PROFILE}" \
--fast; then
exit 0
fi
if [[ "${attempt}" == "${MATRIX_ATTEMPTS}" ]]; then
exit 1
fi
echo "Matrix live lane failed on attempt ${attempt}; retrying..." >&2
sleep 10
done
- name: Upload Matrix QA artifacts
id: upload_artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: qa-live-matrix-${{ github.run_id }}-${{ github.run_attempt }}
name: ${{ inputs.expected_sha != '' && format('release-qa-live-matrix-{0}', inputs.expected_sha) || format('qa-live-matrix-{0}-{1}', github.run_id, github.run_attempt) }}
path: ${{ steps.run_lane.outputs.output_dir }}
retention-days: 14
if-no-files-found: error
- name: Record Matrix QA status
id: record_status
if: always()
shell: bash
env:
RUN_OUTCOME: ${{ steps.run_lane.outcome }}
UPLOAD_OUTCOME: ${{ steps.upload_artifacts.outcome }}
run: |
set -euo pipefail
status="success"
for outcome in "${RUN_OUTCOME:-}" "${UPLOAD_OUTCOME:-}"; do
case "${outcome}" in
failure) status="failure" ;;
cancelled)
if [[ "${status}" != "failure" ]]; then
status="cancelled"
fi
;;
success|skipped|"") ;;
*) status="failure" ;;
esac
done
echo "status=${status}" >> "$GITHUB_OUTPUT"
run_live_matrix_sharded:
name: Run Matrix live QA lane (${{ matrix.profile }})
needs: [authorize_actor, validate_selected_ref]
@@ -448,14 +585,13 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000"
run: |
set -euo pipefail
output_dir=".artifacts/qa-e2e/matrix-live-${{ matrix.profile }}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT"
matrix_args=(
pnpm openclaw qa matrix \
--repo-root . \
--output-dir "${output_dir}" \
--provider-mode live-frontier \
@@ -463,12 +599,6 @@ jobs:
--alt-model "${OPENCLAW_CI_OPENAI_FALLBACK_MODEL}" \
--profile "${{ matrix.profile }}" \
--fast
)
if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then
matrix_args+=(--fail-fast)
fi
pnpm openclaw qa matrix "${matrix_args[@]}"
- name: Upload Matrix QA shard artifacts
if: always()
@@ -482,6 +612,7 @@ jobs:
run_live_telegram:
name: Run Telegram live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
if: inputs.expected_sha == '' || inputs.run_telegram
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
@@ -578,6 +709,7 @@ jobs:
run_live_discord:
name: Run Discord live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
if: inputs.expected_sha == '' || inputs.run_discord
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared
@@ -653,6 +785,7 @@ jobs:
run_live_whatsapp:
name: Run WhatsApp live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
if: inputs.expected_sha == '' || inputs.run_whatsapp
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
concurrency:
@@ -731,6 +864,7 @@ jobs:
run_live_slack:
name: Run Slack live QA lane with Convex leases
needs: [authorize_actor, validate_selected_ref]
if: inputs.expected_sha == '' || inputs.run_slack
runs-on: blacksmith-16vcpu-ubuntu-2404
timeout-minutes: 60
environment: qa-live-shared