fix(cli): reject non-positive list limits #4360
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| "on": | |
| pull_request: | |
| merge_group: | |
| types: [checks_requested] | |
| push: | |
| branches: [main] | |
| schedule: | |
| - cron: "23 18 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| actions: read | |
| contents: read | |
| pull-requests: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CI_OPTIMIZATION_MODE: ${{ vars.CI_OPTIMIZATION_MODE || 'enforce' }} | |
| OPENSQUILLA_TESTING: "true" | |
| jobs: | |
| queue-attestation: | |
| name: Verify reusable PR CI evidence | |
| if: ${{ github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| reusable: ${{ steps.verify.outputs.reusable || 'false' }} | |
| reason: ${{ steps.verify.outputs.reason || steps.mode.outputs.reason }} | |
| reason_code: ${{ steps.verify.outputs.reason_code || steps.mode.outputs.reason_code }} | |
| source_run_id: ${{ steps.verify.outputs.source_run_id }} | |
| candidate_count: ${{ steps.verify.outputs.candidate_count || '0' }} | |
| artifact_name: ${{ steps.verify.outputs.artifact_name }} | |
| queue_base_sha: ${{ steps.verify.outputs.queue_base_sha }} | |
| queue_head_sha: ${{ steps.verify.outputs.queue_head_sha }} | |
| queue_tree_sha: ${{ steps.verify.outputs.queue_tree_sha }} | |
| source_root_issued_at: ${{ steps.verify.outputs.source_root_issued_at }} | |
| source_lineage: ${{ steps.verify.outputs.source_lineage }} | |
| source_successful_suites: ${{ steps.verify.outputs.source_successful_suites }} | |
| source_planner_digest: ${{ steps.verify.outputs.source_planner_digest }} | |
| source_suite_execution_digests: ${{ steps.verify.outputs.source_suite_execution_digests }} | |
| combined_smoke_suites: ${{ steps.verify.outputs.combined_smoke_suites || '[]' }} | |
| steps: | |
| - name: Validate optimization mode | |
| id: mode | |
| shell: bash | |
| run: | | |
| case "${CI_OPTIMIZATION_MODE}" in | |
| legacy | shadow | enforce) ;; | |
| *) | |
| echo "ERROR: CI_OPTIMIZATION_MODE must be legacy, shadow, or enforce." >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| if [[ "${CI_OPTIMIZATION_MODE}" == "legacy" ]]; then | |
| echo "reason_code=legacy_mode" >> "${GITHUB_OUTPUT}" | |
| echo "reason=legacy mode always runs queue CI" >> "${GITHUB_OUTPUT}" | |
| fi | |
| - name: Check out merge-group commit | |
| if: ${{ github.event_name == 'merge_group' && env.CI_OPTIMIZATION_MODE != 'legacy' }} | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.merge_group.head_sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify matching trusted PR CI attestation | |
| id: verify | |
| if: ${{ github.event_name == 'merge_group' && env.CI_OPTIMIZATION_MODE != 'legacy' }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: >- | |
| python3 .github/scripts/ci_attestation.py verify-queue | |
| --repository "${GITHUB_REPOSITORY}" | |
| --event-path "${GITHUB_EVENT_PATH}" | |
| --run-id "${GITHUB_RUN_ID}" | |
| --api-url "${GITHUB_API_URL}" | |
| --github-output "${GITHUB_OUTPUT}" | |
| - name: Summarize queue decision | |
| if: ${{ github.event_name == 'merge_group' }} | |
| env: | |
| REUSABLE: ${{ steps.verify.outputs.reusable || 'false' }} | |
| REASON: ${{ steps.verify.outputs.reason || steps.mode.outputs.reason }} | |
| REASON_CODE: ${{ steps.verify.outputs.reason_code || steps.mode.outputs.reason_code }} | |
| SOURCE_RUN_ID: ${{ steps.verify.outputs.source_run_id }} | |
| CANDIDATE_COUNT: ${{ steps.verify.outputs.candidate_count || '0' }} | |
| ARTIFACT_NAME: ${{ steps.verify.outputs.artifact_name }} | |
| QUEUE_BASE_SHA: ${{ steps.verify.outputs.queue_base_sha }} | |
| QUEUE_HEAD_SHA: ${{ steps.verify.outputs.queue_head_sha }} | |
| QUEUE_TREE_SHA: ${{ steps.verify.outputs.queue_tree_sha }} | |
| COMBINED_SMOKE_SUITES: ${{ steps.verify.outputs.combined_smoke_suites || '[]' }} | |
| shell: bash | |
| run: | | |
| { | |
| echo "## Merge queue CI evidence" | |
| echo | |
| echo "- Mode: \`${CI_OPTIMIZATION_MODE}\`" | |
| echo "- Reusable: \`${REUSABLE}\`" | |
| echo "- Reason: \`${REASON_CODE}\` - ${REASON}" | |
| echo "- Candidates: \`${CANDIDATE_COUNT}\`" | |
| echo "- Artifact: \`${ARTIFACT_NAME}\`" | |
| echo "- Queue base/head/tree: \`${QUEUE_BASE_SHA}\` / \`${QUEUE_HEAD_SHA}\` / \`${QUEUE_TREE_SHA}\`" | |
| echo "- Combined-tree domain smokes: \`${COMBINED_SMOKE_SUITES}\`" | |
| if [[ "${REASON_CODE}" == "tree_mismatch" ]]; then | |
| echo "- Fast-path note: the PR evidence does not match this queue tree, so this entry runs the full fail-closed matrix. If main advanced, refresh the PR against the latest main and wait for a new green PR CI run before requeueing. Reuse is always decided against the future entry's exact base and tree." | |
| fi | |
| if [[ -n "${SOURCE_RUN_ID}" ]]; then | |
| echo "- Source run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${SOURCE_RUN_ID}" | |
| fi | |
| if [[ "${CI_OPTIMIZATION_MODE}" == "enforce" && "${REUSABLE}" == "true" ]]; then | |
| echo "- Decision: \`attestation_reuse\`" | |
| fi | |
| if [[ "${CI_OPTIMIZATION_MODE}" == "shadow" && "${REUSABLE}" == "true" ]]; then | |
| echo "- Shadow decision: evidence matched, but the full fail-closed queue matrix still runs." | |
| fi | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| plan-ci: | |
| name: Plan CI suites | |
| needs: queue-attestation | |
| if: >- | |
| ${{ always() | |
| && (github.event_name != 'merge_group' | |
| || (vars.CI_OPTIMIZATION_MODE || 'enforce') != 'enforce' | |
| || needs.queue-attestation.result != 'success' | |
| || needs.queue-attestation.outputs.reusable != 'true') | |
| && (github.event_name != 'push' | |
| || (vars.CI_OPTIMIZATION_MODE || 'enforce') != 'enforce') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| required_suites: ${{ steps.plan.outputs.required_suites }} | |
| desktop_matrix: ${{ steps.plan.outputs.desktop_matrix }} | |
| python_matrix: ${{ steps.plan.outputs.python_matrix }} | |
| platform_matrix: ${{ steps.plan.outputs.platform_matrix }} | |
| python_targets: ${{ steps.plan.outputs.python_targets }} | |
| full_fallback: ${{ steps.plan.outputs.full_fallback }} | |
| reason_codes: ${{ steps.plan.outputs.reason_codes }} | |
| plan_digest: ${{ steps.plan.outputs.plan_digest }} | |
| suite_execution_digests: ${{ steps.plan.outputs.suite_execution_digests }} | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: List changed files | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| changed_files="${RUNNER_TEMP}/changed-files.txt" | |
| case "${CI_OPTIMIZATION_MODE}" in | |
| legacy | shadow | enforce) ;; | |
| *) | |
| echo "ERROR: CI_OPTIMIZATION_MODE must be legacy, shadow, or enforce." >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| case "${{ github.event_name }}" in | |
| pull_request) | |
| if [[ "${CI_OPTIMIZATION_MODE}" == "legacy" ]]; then | |
| printf '.ci/run-all\n' > "${changed_files}" | |
| else | |
| base_sha="${{ github.event.pull_request.base.sha }}" | |
| head_sha="${{ github.event.pull_request.head.sha }}" | |
| if git diff --merge-base --no-renames --name-only \ | |
| "${base_sha}" "${head_sha}" -- > "${changed_files}"; then | |
| echo "Planning PR-owned changes from the merge base." | |
| else | |
| echo "Unable to derive the PR-owned change set; running the full fail-closed matrix." >&2 | |
| printf '.ci/run-all\n' > "${changed_files}" | |
| fi | |
| fi | |
| ;; | |
| merge_group) | |
| # Reaching the planner means the queue fast-path proof was | |
| # unavailable or rejected. Do not turn an evidence failure into | |
| # a weaker targeted queue run: every such path is fail-closed. | |
| echo "Queue evidence was unavailable; running the full fail-closed matrix." >&2 | |
| printf '.ci/run-all\n' > "${changed_files}" | |
| ;; | |
| push) | |
| before="${{ github.event.before }}" | |
| after="${{ github.event.after }}" | |
| if [[ -n "${before}" && "${before}" != "0000000000000000000000000000000000000000" ]]; then | |
| git diff --no-renames --name-only "${before}" "${after}" > "${changed_files}" | |
| else | |
| printf '.ci/run-all\n' > "${changed_files}" | |
| fi | |
| ;; | |
| *) | |
| printf '.ci/run-all\n' > "${changed_files}" | |
| ;; | |
| esac | |
| echo "Changed files:" | |
| sed 's/^/ /' "${changed_files}" | |
| printf 'CHANGED_FILES=%s\n' "${changed_files}" >> "${GITHUB_ENV}" | |
| - name: Build canonical suite plan | |
| id: plan | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| plan_json="$(python3 .github/scripts/plan_ci.py "${CHANGED_FILES}" --repo .)" | |
| python3 - "${plan_json}" "${GITHUB_OUTPUT}" <<'PY' | |
| import json | |
| import sys | |
| plan = json.loads(sys.argv[1]) | |
| output = sys.argv[2] | |
| with open(output, "a", encoding="utf-8") as handle: | |
| for key in ( | |
| "required_suites", | |
| "desktop_matrix", | |
| "python_matrix", | |
| "platform_matrix", | |
| "python_targets", | |
| "full_fallback", | |
| "reason_codes", | |
| "plan_digest", | |
| "suite_execution_digests", | |
| ): | |
| value = plan[key] | |
| if key == "python_targets": | |
| value = ",".join(value) | |
| elif isinstance(value, bool): | |
| value = str(value).lower() | |
| elif isinstance(value, (dict, list)): | |
| value = json.dumps(value, sort_keys=True, separators=(",", ":")) | |
| handle.write(f"{key}={value}\n") | |
| PY | |
| - name: Summarize canonical CI plan | |
| env: | |
| REQUIRED_SUITES: ${{ steps.plan.outputs.required_suites }} | |
| DESKTOP_MATRIX: ${{ steps.plan.outputs.desktop_matrix }} | |
| PYTHON_MATRIX: ${{ steps.plan.outputs.python_matrix }} | |
| PLATFORM_MATRIX: ${{ steps.plan.outputs.platform_matrix }} | |
| FULL_FALLBACK: ${{ steps.plan.outputs.full_fallback }} | |
| REASON_CODES: ${{ steps.plan.outputs.reason_codes }} | |
| shell: bash | |
| run: | | |
| python3 - <<'PY' >> "${GITHUB_STEP_SUMMARY}" | |
| import json | |
| import os | |
| required_suites = json.loads(os.environ["REQUIRED_SUITES"]) | |
| desktop_matrix = json.loads(os.environ["DESKTOP_MATRIX"]) | |
| python_matrix = json.loads(os.environ["PYTHON_MATRIX"]) | |
| platform_matrix = json.loads(os.environ["PLATFORM_MATRIX"]) | |
| reason_codes = json.loads(os.environ["REASON_CODES"]) | |
| decision = "full_fallback" if os.environ["FULL_FALLBACK"] == "true" else "targeted" | |
| print("## Canonical CI plan") | |
| print() | |
| print(f"- Decision: `{decision}`") | |
| print(f"- Reason codes: `{json.dumps(reason_codes, separators=(',', ':'))}`") | |
| print(f"- Required suites: `{json.dumps(required_suites, separators=(',', ':'))}`") | |
| print(f"- Python matrix: `{json.dumps(python_matrix, separators=(',', ':'))}`") | |
| print(f"- Desktop matrix: `{json.dumps(desktop_matrix, separators=(',', ':'))}`") | |
| print(f"- Suite cells: `{len(platform_matrix)}`") | |
| PY | |
| - name: Summarize merge-group fallback decision | |
| if: ${{ github.event_name == 'merge_group' }} | |
| shell: bash | |
| run: | | |
| { | |
| echo "## Merge queue fallback" | |
| echo | |
| echo "- Decision: \`full_fail_closed\`" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| workflow-lint: | |
| name: Validate GitHub Actions workflows | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'workflow-lint') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Cache actionlint Go build inputs | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/go/pkg/mod | |
| ~/.cache/go-build | |
| key: actionlint-${{ runner.os }}-${{ runner.arch }}-v1.7.12 | |
| - name: Run actionlint | |
| run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color=false | |
| frontend-artifact: | |
| name: Frontend artifact producer | |
| needs: plan-ci | |
| # This job owns the generated package input. Source-only Python tests stay | |
| # parallel; only packaging/browser/desktop consumers wait for this artifact. | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-artifact') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: opensquilla-webui/.node-version | |
| cache: 'npm' | |
| cache-dependency-path: opensquilla-webui/package-lock.json | |
| - name: Install frontend dependencies | |
| working-directory: opensquilla-webui | |
| run: npm ci | |
| - name: Verify generated dist is not tracked | |
| shell: bash | |
| run: | | |
| tracked="$(git ls-files 'opensquilla-webui/dist/**' 'src/opensquilla/gateway/static/dist/**')" | |
| if [[ -n "${tracked}" ]]; then | |
| echo "ERROR: generated Web UI dist must not be committed:" >&2 | |
| printf '%s\n' "${tracked}" >&2 | |
| exit 1 | |
| fi | |
| - name: Build verified frontend artifact | |
| working-directory: opensquilla-webui | |
| shell: bash | |
| run: | | |
| # Vite copies public/ verbatim. Prove the canonical normalization | |
| # strips harmless Finder metadata before the manifest is written. | |
| printf 'CI-only Finder metadata\n' > public/.DS_Store | |
| npm run build:artifact | |
| if [[ -e dist/.DS_Store || -e ../src/opensquilla/gateway/static/dist/.DS_Store ]]; then | |
| echo "ERROR: Finder metadata survived WebUI artifact normalization" >&2 | |
| exit 1 | |
| fi | |
| npm run verify:release-dist | |
| - name: Verify staged backend copy | |
| working-directory: opensquilla-webui | |
| run: node scripts/stage-dist.mjs --check | |
| - name: Upload verified frontend artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| # Stable across rerun attempts: "re-run failed jobs" does not rerun | |
| # an already-successful producer, so consumers must reuse its name. | |
| name: opensquilla-webui-dist | |
| path: opensquilla-webui/dist/ | |
| if-no-files-found: error | |
| # GitHub permits reruns for 30 days; keep the producer artifact for | |
| # the full window so a failed-only consumer rerun can still reuse it. | |
| retention-days: 31 | |
| overwrite: true | |
| frontend-check: | |
| name: Frontend tests and package validation | |
| needs: [plan-ci, frontend-artifact] | |
| if: >- | |
| ${{ always() | |
| && needs.plan-ci.result == 'success' | |
| && needs.frontend-artifact.result == 'success' | |
| && (contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') | |
| || contains(fromJSON(needs.plan-ci.outputs.required_suites), 'wheel-webui-roundtrip')) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Download verified frontend artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: opensquilla-webui-dist | |
| path: opensquilla-webui/dist/ | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: opensquilla-webui/.node-version | |
| cache: npm | |
| cache-dependency-path: opensquilla-webui/package-lock.json | |
| - name: Install frontend dependencies | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| working-directory: opensquilla-webui | |
| run: npm ci | |
| - name: Stage verified frontend artifact for Python consumers | |
| working-directory: opensquilla-webui | |
| run: node scripts/stage-dist.mjs | |
| - name: Set up Python for Contract generation | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv for Contract generation | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install pinned Contract generators | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| run: uv sync --only-group dev --frozen | |
| - name: Verify deterministic Contract generation | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| shell: bash | |
| run: | | |
| uv run --no-sync python scripts/contracts/generate_gateway_contracts.py --check | |
| uv run --no-sync python scripts/contracts/generate_gateway_contracts.py --verify-determinism | |
| - name: Run real Gateway Contract toolchain integration | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| env: | |
| OPENSQUILLA_RUN_CONTRACT_TOOLCHAIN_INTEGRATION: "1" | |
| PYTHONPATH: "${{ github.workspace }}:${{ github.workspace }}/src" | |
| run: >- | |
| uv run --no-sync pytest -q | |
| --confcutdir=tests/contracts | |
| tests/contracts | |
| - name: Write Linux Gateway Contract hash manifest | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| run: >- | |
| uv run --no-sync python scripts/contracts/generate_gateway_contracts.py | |
| --hash-manifest "${RUNNER_TEMP}/gateway-contract-hashes.json" | |
| - name: Upload Linux Gateway Contract hash manifest | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: gateway-contract-hashes-linux | |
| path: ${{ runner.temp }}/gateway-contract-hashes.json | |
| if-no-files-found: error | |
| retention-days: 7 | |
| overwrite: true | |
| - name: Run frontend type checks | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| working-directory: opensquilla-webui | |
| run: npm run typecheck | |
| - name: Run frontend unit tests | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| working-directory: opensquilla-webui | |
| run: npm run test:unit | |
| - name: Set up Python | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'wheel-webui-roundtrip') }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'wheel-webui-roundtrip') }} | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Verify sdist-to-wheel frontend artifact round trip | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'wheel-webui-roundtrip') }} | |
| shell: bash | |
| run: | | |
| package_dir="${RUNNER_TEMP}/webui-package" | |
| wheel_dir="${RUNNER_TEMP}/webui-wheel" | |
| mkdir -p "${package_dir}" "${wheel_dir}" | |
| junk="opensquilla-webui/src/.DS_Store" | |
| trap 'rm -f "${junk}"' EXIT | |
| printf 'CI-only Finder metadata\n' > "${junk}" | |
| uv build --sdist --out-dir "${package_dir}" | |
| sdists=("${package_dir}"/opensquilla-*.tar.gz) | |
| if [[ "${#sdists[@]}" -ne 1 || ! -f "${sdists[0]}" ]]; then | |
| echo "ERROR: expected exactly one OpenSquilla sdist" >&2 | |
| exit 1 | |
| fi | |
| tar_listing="${package_dir}/sdist-files.txt" | |
| tar -tzf "${sdists[0]}" > "${tar_listing}" | |
| if grep -qE '(^|/)\.DS_Store$' "${tar_listing}"; then | |
| echo "ERROR: ignored Finder metadata leaked into the sdist" >&2 | |
| exit 1 | |
| fi | |
| uv build --wheel --out-dir "${wheel_dir}" "${sdists[0]}" | |
| wheels=("${wheel_dir}"/opensquilla-*.whl) | |
| if [[ "${#wheels[@]}" -ne 1 || ! -f "${wheels[0]}" ]]; then | |
| echo "ERROR: expected exactly one OpenSquilla wheel" >&2 | |
| exit 1 | |
| fi | |
| python scripts/verify_webui_artifact.py \ | |
| --forbid-personal-bgm \ | |
| --dist src/opensquilla/gateway/static/dist \ | |
| --wheel "${wheels[0]}" | |
| gateway-contract-windows: | |
| name: Gateway Contract determinism (Windows) | |
| needs: [plan-ci, frontend-check] | |
| if: >- | |
| ${{ always() | |
| && needs.plan-ci.result == 'success' | |
| && needs.frontend-check.result == 'success' | |
| && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'frontend-validation') }} | |
| runs-on: windows-latest | |
| # Real-toolchain generation scales with the complete Contract inventory. | |
| # Keep the full Windows check, determinism replay, tests, and cross-OS hash | |
| # comparison within one bounded job instead of weakening the gate. | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: opensquilla-webui/.node-version | |
| cache: npm | |
| cache-dependency-path: opensquilla-webui/package-lock.json | |
| - name: Install frontend dependencies | |
| working-directory: opensquilla-webui | |
| run: npm ci | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install pinned Contract generators | |
| run: uv sync --only-group dev --frozen | |
| - name: Verify Windows Contract generation and real toolchain | |
| env: | |
| OPENSQUILLA_RUN_CONTRACT_TOOLCHAIN_INTEGRATION: "1" | |
| PYTHONPATH: "${{ github.workspace }};${{ github.workspace }}/src" | |
| run: | | |
| uv run --no-sync python scripts/contracts/generate_gateway_contracts.py --check | |
| uv run --no-sync python scripts/contracts/generate_gateway_contracts.py --verify-determinism | |
| uv run --no-sync pytest -q --confcutdir=tests/contracts tests/contracts | |
| - name: Download Linux Gateway Contract hash manifest | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: gateway-contract-hashes-linux | |
| path: ${{ runner.temp }}/gateway-contract-hashes-linux | |
| - name: Compare Linux and Windows Contract hashes | |
| shell: pwsh | |
| run: | | |
| $windowsManifest = Join-Path $env:RUNNER_TEMP "gateway-contract-hashes-windows.json" | |
| $linuxManifest = Join-Path $env:RUNNER_TEMP "gateway-contract-hashes-linux/gateway-contract-hashes.json" | |
| uv run --no-sync python scripts/contracts/generate_gateway_contracts.py --hash-manifest $windowsManifest | |
| uv run --no-sync python scripts/contracts/generate_gateway_contracts.py --compare-hash-manifests $linuxManifest $windowsManifest | |
| webui-chat-recovery: | |
| name: WebUI chat recovery | |
| needs: [plan-ci, frontend-artifact] | |
| if: ${{ always() && needs.plan-ci.result == 'success' && needs.frontend-artifact.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'webui-chat-recovery') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| env: | |
| OPENSQUILLA_WEBUI_BASE_URL: http://127.0.0.1:18791 | |
| OPENSQUILLA_PLAYWRIGHT_MANAGE_WEBUI: gateway | |
| PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Download verified frontend artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: opensquilla-webui-dist | |
| path: opensquilla-webui/dist/ | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: opensquilla-webui/.node-version | |
| cache: 'npm' | |
| cache-dependency-path: opensquilla-webui/package-lock.json | |
| - name: Install Gateway dependencies | |
| run: uv sync --frozen | |
| - name: Install frontend dependencies | |
| working-directory: opensquilla-webui | |
| run: npm ci | |
| - name: Stage verified frontend artifact for Gateway | |
| working-directory: opensquilla-webui | |
| run: node scripts/stage-dist.mjs | |
| - name: Resolve Playwright browser revision | |
| id: playwright-browser | |
| shell: bash | |
| run: | | |
| revision="$(node -e "const b=require('./opensquilla-webui/node_modules/playwright-core/browsers.json').browsers.find((item)=>item.name==='chromium'); if(!b)process.exit(2); process.stdout.write(b.revision)")" | |
| printf 'revision=%s\n' "${revision}" >> "${GITHUB_OUTPUT}" | |
| - name: Restore Playwright browser | |
| id: playwright-cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} | |
| key: playwright-chromium-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright-browser.outputs.revision }} | |
| - name: Install Chromium | |
| working-directory: opensquilla-webui | |
| run: npx playwright install --with-deps chromium | |
| - name: Seed Playwright browser cache from nightly main | |
| if: ${{ github.event_name == 'schedule' && steps.playwright-cache.outputs.cache-hit != 'true' }} | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} | |
| key: playwright-chromium-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright-browser.outputs.revision }} | |
| - name: Run production-dist chat and Goal recovery browser contracts | |
| working-directory: opensquilla-webui | |
| run: >- | |
| npm run test:e2e -- | |
| assistant-activity.spec.ts | |
| composer-paste.spec.ts | |
| history-hydration.spec.ts | |
| session-created-card.spec.ts | |
| session-switch-transport.spec.ts | |
| goal-mode.spec.ts | |
| queue-steer.spec.ts | |
| share.spec.ts | |
| --project=chromium | |
| --workers=2 | |
| - name: Upload chat recovery traces | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: webui-chat-recovery-attempt-${{ github.run_attempt }} | |
| path: opensquilla-webui/test-results/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| tui-check: | |
| name: OpenTUI package tests | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'tui') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: opensquilla-webui/.node-version | |
| - name: Set up Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version-file: src/opensquilla/cli/tui/opentui/package/.bun-version | |
| - name: Install OpenTUI host dependencies | |
| working-directory: src/opensquilla/cli/tui/opentui/package | |
| shell: bash | |
| run: bun install --frozen-lockfile | |
| - name: Run OpenTUI Node tests | |
| working-directory: src/opensquilla/cli/tui/opentui/package | |
| run: npm run test | |
| - name: Run OpenTUI Bun tests | |
| working-directory: src/opensquilla/cli/tui/opentui/package | |
| shell: bash | |
| run: | | |
| set +e | |
| for attempt in 1 2; do | |
| bun run test:bun | |
| status=$? | |
| if [ "$status" -eq 0 ]; then | |
| exit 0 | |
| fi | |
| if [ "$status" -ne 132 ] || [ "$attempt" -eq 2 ]; then | |
| exit "$status" | |
| fi | |
| echo "::warning::Bun exited with SIGILL (132); retrying once." | |
| done | |
| desktop-check: | |
| name: Desktop Electron unit tests | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'desktop-static') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| env: | |
| # The pure-logic unit tests run against the compiled dist and never launch | |
| # Electron, so skip the heavy Electron/Playwright binary downloads. | |
| ELECTRON_SKIP_BINARY_DOWNLOAD: '1' | |
| PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22.12.0' | |
| cache: 'npm' | |
| cache-dependency-path: desktop/electron/package-lock.json | |
| - name: Install desktop dependencies | |
| working-directory: desktop/electron | |
| run: npm ci | |
| - name: Build desktop TypeScript | |
| working-directory: desktop/electron | |
| run: npm run build | |
| - name: Run desktop unit tests | |
| working-directory: desktop/electron | |
| run: | | |
| node scripts/test-installer-progress-contract.mjs | |
| node scripts/test-secret-storage-policy.mjs | |
| node scripts/test-update-resolver.mjs | |
| node scripts/test-update-check-scheduler.mjs | |
| node scripts/test-desktop-locale.mjs | |
| node scripts/test-desktop-zoom-shortcuts.mjs --contracts-only | |
| node scripts/test-desktop-profile-substrate.mjs | |
| node scripts/test-desktop-profile-consolidation.mjs | |
| node scripts/test-desktop-gateway-ownership.mjs | |
| node scripts/test-desktop-gateway-lifecycle.mjs | |
| node scripts/test-desktop-window-lifecycle.mjs | |
| node scripts/test-bundled-runtimes.mjs | |
| node scripts/test-desktop-cleanup-contract.mjs | |
| node scripts/test-desktop-renderer-log.mjs | |
| node scripts/test-artifact-preview-lease-broker.mjs | |
| node scripts/test-native-workbench-surface.mjs | |
| node scripts/test-onboarding-flow-coordinator.mjs | |
| node scripts/test-onboarding-save-telemetry.mjs | |
| node scripts/test-ci-case-telemetry.mjs | |
| node ../../.github/scripts/verify-sandbox-package.mjs --source | |
| ubuntu-quality: | |
| name: Lint, test, and build (ubuntu-latest, 3.12) | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && (contains(fromJSON(needs.plan-ci.outputs.required_suites), 'python-targeted') || contains(fromJSON(needs.plan-ci.outputs.required_suites), 'python-full')) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| env: | |
| UV_PYTHON: "3.12" | |
| PYTHONPATH: ${{ github.workspace }} | |
| OPENSQUILLA_TURN_CALL_LOG: "0" | |
| OPENSQUILLA_DOCKERIGNORE_E2E: "1" | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| lfs: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'python-full') }} | |
| - name: Configure runtime directories | |
| shell: bash | |
| run: | | |
| printf 'OPENSQUILLA_STATE_DIR=%s/opensquilla-state\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" | |
| printf 'OPENSQUILLA_LOG_DIR=%s/opensquilla-logs\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| shell: bash | |
| run: uv sync --extra dev --extra recommended --extra mcp --frozen | |
| - name: Lint | |
| shell: bash | |
| run: uv run ruff check src tests | |
| - name: Type check | |
| shell: bash | |
| run: uv run mypy src/opensquilla --show-error-codes | |
| - name: Test targeted PR suite | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'python-targeted') && !contains(fromJSON(needs.plan-ci.outputs.required_suites), 'python-full') }} | |
| shell: bash | |
| env: | |
| TARGETED_PYTEST_TARGETS: ${{ needs.plan-ci.outputs.python_targets }} | |
| run: | | |
| set -euo pipefail | |
| markers="not llm and not live_search and not live_skill_hub and not live_channel and not webui_browser and not tui_real_terminal and not local_golden and not agent_context_boundary and not llm_router_acc" | |
| pytest_targets=( | |
| tests/unit | |
| ) | |
| if [[ -n "${TARGETED_PYTEST_TARGETS}" ]]; then | |
| IFS=',' read -r -a affected_targets <<< "${TARGETED_PYTEST_TARGETS}" | |
| for target in "${affected_targets[@]}"; do | |
| if [[ "${target}" == *'*'* || "${target}" == *'?'* || "${target}" == *'['* ]]; then | |
| mapfile -t matched_targets < <(compgen -G "${target}" || true) | |
| if [[ "${#matched_targets[@]}" -eq 0 ]]; then | |
| echo "ERROR: planner pattern matched no pytest targets: ${target}" >&2 | |
| exit 2 | |
| fi | |
| pytest_targets+=("${matched_targets[@]}") | |
| elif [[ ! -e "${target}" ]]; then | |
| echo "ERROR: planner selected a missing pytest target: ${target}" >&2 | |
| exit 2 | |
| else | |
| pytest_targets+=("${target}") | |
| fi | |
| done | |
| fi | |
| uv run pytest \ | |
| --ignore=tests/test_ci/test_router_artifact_manifest.py \ | |
| "${pytest_targets[@]}" \ | |
| -q -m "${markers}" --durations=50 | |
| - name: Test Docker build-context exclusions in full CI | |
| if: ${{ contains(fromJSON(needs.plan-ci.outputs.required_suites), 'python-full') }} | |
| shell: bash | |
| run: uv run pytest tests/test_ci/test_dockerignore_context.py -q | |
| - name: Verify metric counter names unchanged | |
| shell: bash | |
| run: | | |
| set -e | |
| for name in opensquilla_queue_depth in_flight_turns_total turn_cancellations_total queue_full_errors_total; do | |
| count=$(grep -c "$name" src/opensquilla/gateway/task_runtime.py || echo 0) | |
| if [ "$count" -lt 1 ]; then | |
| echo "ERROR: metric name '$name' missing from task_runtime.py" | |
| exit 1 | |
| fi | |
| done | |
| ubuntu-full: | |
| name: Full offline tests (ubuntu-latest, ${{ matrix.shard }}) | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'python-full') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: ${{ fromJSON(needs.plan-ci.outputs.python_matrix).ubuntu }} | |
| env: | |
| UV_PYTHON: "3.12" | |
| PYTHONPATH: ${{ github.workspace }} | |
| OPENSQUILLA_TURN_CALL_LOG: "0" | |
| steps: | |
| - name: Prepare Ubuntu full report | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| report_dir="${RUNNER_TEMP}/ci-reports/ubuntu-${{ matrix.shard }}" | |
| mkdir -p "${report_dir}" | |
| printf 'CI_REPORT_DIR=%s\n' "${report_dir}" >> "${GITHUB_ENV}" | |
| printf 'pytest_status=not_started\n' > "${report_dir}/first-failure.txt" | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| lfs: true | |
| - name: Configure runtime directories | |
| shell: bash | |
| run: | | |
| printf 'OPENSQUILLA_STATE_DIR=%s/opensquilla-state\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" | |
| printf 'OPENSQUILLA_LOG_DIR=%s/opensquilla-logs\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| shell: bash | |
| run: uv sync --extra dev --extra recommended --extra mcp --frozen | |
| - name: Test Ubuntu full shard | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| markers="not llm and not live_search and not live_skill_hub and not live_channel and not webui_browser and not tui_real_terminal and not local_golden and not agent_context_boundary and not llm_router_acc" | |
| worker_args=() | |
| if [[ "${{ matrix.shard }}" == "gateway-sqlite" ]]; then | |
| # Gateway tests start subprocesses and touch shared SQLite/logging | |
| # state. Match the bounded Windows shard fan-out instead of | |
| # letting the Ubuntu default start four competing workers. | |
| worker_args+=(--workers=2) | |
| fi | |
| maxfail_args=() | |
| if [[ "${{ github.event_name }}" == "pull_request" || | |
| "${{ github.event_name }}" == "merge_group" ]]; then | |
| maxfail_args+=(--maxfail=3) | |
| fi | |
| uv run python .github/scripts/windows_test_shards.py run \ | |
| "${{ matrix.shard }}" \ | |
| --junit "${CI_REPORT_DIR}/junit.xml" \ | |
| --summary "${CI_REPORT_DIR}/first-failure.txt" \ | |
| "${worker_args[@]}" \ | |
| -- \ | |
| -q \ | |
| -m "${markers}" \ | |
| --durations=50 \ | |
| "${maxfail_args[@]}" \ | |
| 2>&1 | tee "${CI_REPORT_DIR}/pytest.log" | |
| - name: Upload Ubuntu full shard report | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ubuntu-full-${{ matrix.shard }}-attempt-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/ci-reports/ubuntu-${{ matrix.shard }} | |
| if-no-files-found: error | |
| retention-days: 14 | |
| windows-full: | |
| name: Windows high-risk (${{ matrix.shard }}) | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'windows-high-risk') }} | |
| runs-on: windows-latest | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: ${{ fromJSON(needs.plan-ci.outputs.python_matrix).windows }} | |
| env: | |
| UV_PYTHON: "3.12" | |
| PYTHONPATH: ${{ github.workspace }} | |
| OPENSQUILLA_TURN_CALL_LOG: "0" | |
| steps: | |
| - name: Prepare diagnostic report | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| report_dir="${RUNNER_TEMP}/ci-reports/${{ matrix.shard }}" | |
| mkdir -p "${report_dir}" | |
| printf 'CI_REPORT_DIR=%s\n' "${report_dir}" >> "${GITHUB_ENV}" | |
| { | |
| printf 'runner_os=%s\n' "${RUNNER_OS}" | |
| printf 'runner_arch=%s\n' "${RUNNER_ARCH}" | |
| printf 'commit_sha=%s\n' "${GITHUB_SHA}" | |
| printf 'run_attempt=%s\n' "${GITHUB_RUN_ATTEMPT}" | |
| printf 'shard=%s\n' "${{ matrix.shard }}" | |
| } > "${report_dir}/environment.txt" | |
| printf 'pytest_status=not_started\n' > "${report_dir}/first-failure.txt" | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| lfs: true | |
| - name: Configure runtime directories | |
| shell: bash | |
| run: | | |
| printf 'OPENSQUILLA_STATE_DIR=%s/opensquilla-state\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" | |
| printf 'OPENSQUILLA_LOG_DIR=%s/opensquilla-logs\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Set up Bun | |
| if: ${{ matrix.shard == 'core' }} | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version-file: src/opensquilla/cli/tui/opentui/package/.bun-version | |
| - name: Install OpenTUI host dependencies | |
| if: ${{ matrix.shard == 'core' }} | |
| shell: bash | |
| run: bun install --frozen-lockfile --cwd=src/opensquilla/cli/tui/opentui/package | |
| - name: Install dependencies | |
| shell: bash | |
| run: uv sync --extra dev --extra recommended --extra mcp --frozen | |
| - name: Record tool versions | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| { | |
| python --version | |
| uv --version | |
| uv run pytest --version | |
| } >> "${CI_REPORT_DIR}/environment.txt" 2>&1 | |
| - name: Provision distinct Windows test volumes | |
| if: ${{ matrix.shard == 'recovery-migration' }} | |
| shell: pwsh | |
| run: | | |
| $token = [guid]::NewGuid().ToString("N") | |
| $volumeA = Join-Path -Path $env:RUNNER_TEMP -ChildPath "opensquilla-windows-volume-a-$token" | |
| $volumeB = Join-Path -Path $env:LOCALAPPDATA -ChildPath "opensquilla-windows-volume-b-$token" | |
| if (Test-Path -LiteralPath $volumeA) { | |
| throw "Windows test volume root already exists: $volumeA" | |
| } | |
| if (Test-Path -LiteralPath $volumeB) { | |
| throw "Windows test volume root already exists: $volumeB" | |
| } | |
| $driveA = [System.IO.Path]::GetPathRoot($volumeA) | |
| $driveB = [System.IO.Path]::GetPathRoot($volumeB) | |
| $systemDriveRoot = [System.IO.Path]::GetPathRoot("$($env:SystemDrive)\") | |
| if (-not [string]::Equals($driveB, $systemDriveRoot, [StringComparison]::OrdinalIgnoreCase)) { | |
| throw "Windows test volume B must use SystemDrive" | |
| } | |
| if ([string]::Equals($driveA, $driveB, [StringComparison]::OrdinalIgnoreCase)) { | |
| throw "Windows test volume roots must use different drives" | |
| } | |
| $createdRoots = [System.Collections.Generic.List[string]]::new() | |
| try { | |
| New-Item -ItemType Directory -Path $volumeA -ErrorAction Stop | Out-Null | |
| $createdRoots.Add($volumeA) | |
| New-Item -ItemType Directory -Path $volumeB -ErrorAction Stop | Out-Null | |
| $createdRoots.Add($volumeB) | |
| "OPENSQUILLA_WINDOWS_TEST_VOLUME_A=$volumeA" | | |
| Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| "OPENSQUILLA_WINDOWS_TEST_VOLUME_B=$volumeB" | | |
| Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append | |
| } catch { | |
| foreach ($testRoot in $createdRoots) { | |
| if (Test-Path -LiteralPath $testRoot) { | |
| Remove-Item -LiteralPath $testRoot -Recurse -Force | |
| } | |
| } | |
| throw | |
| } | |
| - name: Test Windows shard | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| markers="not llm and not live_search and not live_skill_hub and not live_channel and not webui_browser and not tui_real_terminal and not local_golden and not agent_context_boundary and not llm_router_acc" | |
| maxfail_args=() | |
| if [[ "${{ github.event_name }}" == "pull_request" || "${{ github.event_name }}" == "merge_group" ]]; then | |
| maxfail_args+=(--maxfail=3) | |
| fi | |
| worker_args=() | |
| if [[ "${{ matrix.shard }}" == "recovery-migration" || | |
| "${{ matrix.shard }}" == "gateway-sqlite" || | |
| "${{ matrix.shard }}" == "desktop-installer-contracts" ]]; then | |
| # These shards start Windows ``spawn`` children, Node/Electron | |
| # processes, or gateways from several tests. Leave runner | |
| # headroom for those process-start handshakes instead of changing | |
| # their bounded readiness/timing assertions. | |
| worker_args+=(--workers=2) | |
| fi | |
| uv run python .github/scripts/windows_test_shards.py run \ | |
| "${{ matrix.shard }}" \ | |
| --junit "${CI_REPORT_DIR}/junit.xml" \ | |
| --summary "${CI_REPORT_DIR}/first-failure.txt" \ | |
| --metadata "${CI_REPORT_DIR}/windows-shard-metadata.json" \ | |
| "${worker_args[@]}" \ | |
| -- \ | |
| -q \ | |
| -m "${markers}" \ | |
| --durations=50 \ | |
| "${maxfail_args[@]}" \ | |
| 2>&1 | tee "${CI_REPORT_DIR}/pytest.log" | |
| - name: Clean up Windows test volumes | |
| if: ${{ always() && matrix.shard == 'recovery-migration' }} | |
| shell: pwsh | |
| run: | | |
| $testRoots = @( | |
| $env:OPENSQUILLA_WINDOWS_TEST_VOLUME_A, | |
| $env:OPENSQUILLA_WINDOWS_TEST_VOLUME_B | |
| ) | |
| foreach ($testRoot in $testRoots) { | |
| if ($testRoot -and (Test-Path -LiteralPath $testRoot)) { | |
| Remove-Item -LiteralPath $testRoot -Recurse -Force | |
| } | |
| } | |
| - name: Upload Windows shard report | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows-high-risk-${{ matrix.shard }}-attempt-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/ci-reports/${{ matrix.shard }} | |
| if-no-files-found: error | |
| retention-days: 14 | |
| macos-recovery: | |
| name: macOS profile recovery and native no-replace (3.12) | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'macos-recovery') }} | |
| runs-on: macos-latest | |
| timeout-minutes: 30 | |
| env: | |
| UV_PYTHON: "3.12" | |
| PYTHONPATH: ${{ github.workspace }} | |
| OPENSQUILLA_TURN_CALL_LOG: "0" | |
| steps: | |
| - name: Prepare macOS recovery report | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| report_dir="${RUNNER_TEMP}/macos-recovery" | |
| mkdir -p "${report_dir}" | |
| printf 'CI_REPORT_DIR=%s\n' "${report_dir}" >> "${GITHUB_ENV}" | |
| { | |
| printf 'runner_os=%s\n' "${RUNNER_OS}" | |
| printf 'runner_arch=%s\n' "${RUNNER_ARCH}" | |
| printf 'commit_sha=%s\n' "${GITHUB_SHA}" | |
| printf 'run_attempt=%s\n' "${GITHUB_RUN_ATTEMPT}" | |
| } > "${report_dir}/environment.txt" | |
| printf 'pytest_status=not_started\n' > "${report_dir}/first-failure.txt" | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Configure runtime directories | |
| shell: bash | |
| run: | | |
| printf 'OPENSQUILLA_STATE_DIR=%s/opensquilla-state\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" | |
| printf 'OPENSQUILLA_LOG_DIR=%s/opensquilla-logs\n' "$RUNNER_TEMP" >> "$GITHUB_ENV" | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| shell: bash | |
| run: uv sync --extra dev --extra recommended --extra mcp --frozen | |
| - name: Test native profile recovery contracts | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| pytest_args=( | |
| tests/test_recovery | |
| tests/test_migration/test_opensquilla_home_migration.py | |
| tests/test_desktop/test_electron_startup_contract.py | |
| -q | |
| --durations=50 | |
| --junitxml="${CI_REPORT_DIR}/junit.xml" | |
| ) | |
| if [[ "${{ github.event_name }}" == "pull_request" || "${{ github.event_name }}" == "merge_group" ]]; then | |
| pytest_args+=(--maxfail=3) | |
| fi | |
| set +e | |
| uv run pytest "${pytest_args[@]}" \ | |
| 2>&1 | tee "${CI_REPORT_DIR}/pytest.log" | |
| status=${PIPESTATUS[0]} | |
| set -e | |
| printf 'pytest_exit_code=%s\n' "${status}" > "${CI_REPORT_DIR}/first-failure.txt" | |
| exit "${status}" | |
| - name: Upload macOS recovery report | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-recovery-attempt-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/macos-recovery | |
| if-no-files-found: error | |
| retention-days: 14 | |
| desktop-recovery-e2e: | |
| # Every matrix cell gets a fresh hosted runner. Keep ownership and the | |
| # heavier workbench cell separate on both Windows and macOS so process, | |
| # native-focus, and local-socket state cannot leak across those domains. | |
| name: Desktop recovery E2E (${{ matrix.os }}, ${{ matrix.shard }}) | |
| needs: [plan-ci, frontend-artifact] | |
| if: ${{ always() && needs.plan-ci.result == 'success' && needs.frontend-artifact.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'desktop-recovery-e2e') }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.plan-ci.outputs.desktop_matrix) }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 45 | |
| env: | |
| UV_PYTHON: "3.12" | |
| PYTHONPATH: ${{ github.workspace }} | |
| OPENSQUILLA_TURN_CALL_LOG: "0" | |
| PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright | |
| # Electron's postinstall download is otherwise repeated on every fresh | |
| # hosted runner. Keep it beside the Playwright cache so a cold macOS | |
| # worker cannot spend the case watchdog budget downloading the binary. | |
| ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron | |
| CI_E2E_SHARD: ${{ matrix.shard }} | |
| # Keep merge-critical workbench coverage deterministic and bounded. | |
| # Scheduled/manual runs retain the wider geometry/rearm stress cycles. | |
| OPENSQUILLA_WORKBENCH_E2E_MODE: ${{ (github.event_name == 'pull_request' || github.event_name == 'merge_group') && 'smoke' || 'stress' }} | |
| # A hung Electron/gateway child must fail one case with diagnostics, | |
| # rather than consuming the whole 45-minute matrix cell. | |
| OPENSQUILLA_DESKTOP_CASE_TIMEOUT_MS: "900000" | |
| steps: | |
| - name: Prepare Desktop recovery report | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| report_dir="${RUNNER_TEMP}/desktop-recovery-e2e" | |
| mkdir -p "${report_dir}" | |
| printf 'CI_REPORT_DIR=%s\n' "${report_dir}" >> "${GITHUB_ENV}" | |
| { | |
| printf 'runner_os=%s\n' "${RUNNER_OS}" | |
| printf 'runner_arch=%s\n' "${RUNNER_ARCH}" | |
| printf 'commit_sha=%s\n' "${GITHUB_SHA}" | |
| printf 'run_attempt=%s\n' "${GITHUB_RUN_ATTEMPT}" | |
| printf 'workbench_e2e_mode=%s\n' "${OPENSQUILLA_WORKBENCH_E2E_MODE}" | |
| } > "${report_dir}/environment.txt" | |
| printf 'e2e_status=not_started\n' > "${report_dir}/first-failure.txt" | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Download verified frontend artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: opensquilla-webui-dist | |
| path: opensquilla-webui/dist/ | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22.12.0' | |
| cache: 'npm' | |
| cache-dependency-path: | | |
| desktop/electron/package-lock.json | |
| opensquilla-webui/package-lock.json | |
| - name: Stage verified frontend artifact for Desktop | |
| working-directory: opensquilla-webui | |
| run: node scripts/stage-dist.mjs | |
| - name: Verify downloaded frontend artifact on consumer OS | |
| shell: bash | |
| run: node opensquilla-webui/scripts/verify-dist.mjs opensquilla-webui/dist | |
| - name: Restore Electron binary cache | |
| id: electron-cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: ${{ env.ELECTRON_CACHE }} | |
| key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('desktop/electron/package-lock.json') }} | |
| - name: Install Python dependencies | |
| shell: bash | |
| run: uv sync --extra dev --extra recommended --extra mcp --frozen | |
| - name: Install Desktop dependencies | |
| working-directory: desktop/electron | |
| shell: bash | |
| run: npm ci | |
| - name: Seed Electron binary cache from nightly main | |
| if: ${{ github.event_name == 'schedule' && steps.electron-cache.outputs.cache-hit != 'true' }} | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: ${{ env.ELECTRON_CACHE }} | |
| key: electron-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('desktop/electron/package-lock.json') }} | |
| - name: Install WebUI recovery dependencies | |
| if: ${{ (runner.os == 'Windows' || runner.os == 'macOS') && matrix.shard == 'profiles' }} | |
| working-directory: opensquilla-webui | |
| shell: bash | |
| run: npm ci | |
| - name: Resolve Playwright browser revision | |
| id: playwright-browser | |
| if: ${{ (runner.os == 'Windows' || runner.os == 'macOS') && matrix.shard == 'profiles' }} | |
| shell: bash | |
| run: | | |
| revision="$(node -e "const b=require('./opensquilla-webui/node_modules/playwright-core/browsers.json').browsers.find((item)=>item.name==='chromium'); if(!b)process.exit(2); process.stdout.write(b.revision)")" | |
| printf 'revision=%s\n' "${revision}" >> "${GITHUB_OUTPUT}" | |
| - name: Restore Playwright browser | |
| id: playwright-cache | |
| if: ${{ (runner.os == 'Windows' || runner.os == 'macOS') && matrix.shard == 'profiles' }} | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} | |
| key: playwright-chromium-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright-browser.outputs.revision }} | |
| - name: Install WebUI recovery browser | |
| if: ${{ (runner.os == 'Windows' || runner.os == 'macOS') && matrix.shard == 'profiles' }} | |
| working-directory: opensquilla-webui | |
| shell: bash | |
| run: | | |
| if [[ "${RUNNER_OS}" == "Linux" ]]; then | |
| npx playwright install --with-deps chromium | |
| else | |
| npx playwright install chromium | |
| fi | |
| - name: Run cross-platform production-dist browser session hang contract | |
| if: ${{ (runner.os == 'Windows' || runner.os == 'macOS') && matrix.shard == 'profiles' }} | |
| working-directory: opensquilla-webui | |
| shell: bash | |
| env: | |
| OPENSQUILLA_WEBUI_BASE_URL: http://127.0.0.1:18791 | |
| OPENSQUILLA_PLAYWRIGHT_MANAGE_WEBUI: gateway | |
| run: | | |
| set -euo pipefail | |
| npm run test:e2e -- \ | |
| history-hydration.spec.ts \ | |
| --project=chromium \ | |
| --workers=1 \ | |
| --grep "terminates stalled" \ | |
| 2>&1 | tee "${CI_REPORT_DIR}/session-hang-recovery.log" | |
| - name: Seed Playwright browser cache from nightly main | |
| if: ${{ github.event_name == 'schedule' && (runner.os == 'Windows' || runner.os == 'macOS') && matrix.shard == 'profiles' && steps.playwright-cache.outputs.cache-hit != 'true' }} | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} | |
| key: playwright-chromium-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright-browser.outputs.revision }} | |
| - name: Record Desktop recovery tool versions | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| { | |
| python --version | |
| uv --version | |
| node --version | |
| npm --version | |
| } >> "${CI_REPORT_DIR}/environment.txt" 2>&1 | |
| - name: Build Desktop TypeScript | |
| working-directory: desktop/electron | |
| shell: bash | |
| run: npm run build | |
| - name: Run compiled Desktop recovery flows | |
| working-directory: desktop/electron | |
| shell: bash | |
| env: | |
| OPENSQUILLA_DESKTOP_CONSOLIDATION_SCREENSHOT: ${{ runner.temp }}/desktop-recovery-e2e/consolidated-primary.png | |
| OPENSQUILLA_DESKTOP_IMPORT_SCREENSHOT_DIR: ${{ runner.temp }}/desktop-recovery-e2e/profile-import-screenshots | |
| run: | | |
| set -euo pipefail | |
| run_case() { | |
| local name="$1" | |
| local script="$2" | |
| local attempt="$3" | |
| local log_path="${CI_REPORT_DIR}/${name}-attempt-${attempt}.log" | |
| local telemetry_path="${CI_REPORT_DIR}/desktop-e2e-cases.jsonl" | |
| local telemetry=( | |
| node scripts/ci-case-telemetry.mjs run | |
| --case "${name}" | |
| --os "${RUNNER_OS}" | |
| --shard "${{ matrix.shard }}" | |
| --attempt "${attempt}" | |
| --output "${telemetry_path}" | |
| -- | |
| ) | |
| if [[ "${RUNNER_OS}" == "Linux" ]]; then | |
| "${telemetry[@]}" xvfb-run -a node "${script}" 2>&1 | tee "${log_path}" | |
| else | |
| "${telemetry[@]}" node "${script}" 2>&1 | tee "${log_path}" | |
| fi | |
| } | |
| classify_retryable_infrastructure_failure() { | |
| local name="$1" | |
| local log_path="$2" | |
| local classifications="${CI_REPORT_DIR}/retry-classifications.jsonl" | |
| local evidence_dir="${CI_REPORT_DIR}/retry-evidence" | |
| mkdir -p "${evidence_dir}" | |
| python3 - "${name}" "${RUNNER_OS}" "${log_path}" "${classifications}" "${evidence_dir}" <<'PY' | |
| import hashlib | |
| import json | |
| import re | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| case_name, runner_os, log_name, output_name, evidence_name = sys.argv[1:] | |
| log_path = Path(log_name) | |
| payload = log_path.read_bytes() | |
| text = ( | |
| payload.decode("utf-8", errors="replace") | |
| .replace("\r\n", "\n") | |
| .replace("\r", "\n") | |
| ) | |
| # Retry only a single case for narrowly identified hosted-runner | |
| # handoff/input failures. Generic timeouts, connection failures, | |
| # crashes, assertion errors, and product output mismatches are not | |
| # retryable. Case + OS binding prevents a coincidental message in | |
| # another functional assertion from matching. | |
| rules = ( | |
| { | |
| "id": "windows-delete-helper-handoff-timeout-v1", | |
| "os": "Windows", | |
| "cases": {"desktop-cleanup-flow"}, | |
| "pattern": re.compile( | |
| r"^Error: Timed out waiting for post-exit delete-all helper completion:" | |
| r"\s*; pending synthetic targets: .+$", | |
| re.MULTILINE, | |
| ), | |
| "companion_patterns": (), | |
| }, | |
| { | |
| "id": "windows-isolated-acl-worker-timeout-v1", | |
| "os": "Windows", | |
| "cases": {"offline-document-workbench-e2e"}, | |
| "pattern": re.compile( | |
| r"^(?:(?:E\s+)|(?:FAILED\s+\S+\s+-\s+))?" | |
| r"AssertionError: isolated Windows ACL hardening timed out: .+$", | |
| re.MULTILINE, | |
| ), | |
| # pytest can print the same allowed exception once in its | |
| # traceback and again in the short failure summary. Remove | |
| # only the structural traceback header; any unrelated | |
| # terminal exception inside that traceback remains visible | |
| # to the residual-failure scan below. | |
| "companion_patterns": ( | |
| re.compile( | |
| r"^\s*Traceback \(most recent call last\):\s*$", | |
| re.MULTILINE, | |
| ), | |
| ), | |
| }, | |
| { | |
| "id": "windows-loopback-no-buffer-space-v1", | |
| "os": "Windows", | |
| "cases": {"offline-document-workbench-e2e"}, | |
| "pattern": re.compile( | |
| r"^electronApplication\.evaluate: Error: " | |
| r"ERR_NO_BUFFER_SPACE \(-176\) loading " | |
| r"'http://127\.0\.0\.1:\d+/one'$", | |
| re.MULTILINE, | |
| ), | |
| # Chromium reports the socket-allocation failure first; the | |
| # outer Workbench runner then reports the exact failed child. | |
| # Remove only that wrapper so any separate assertion, crash, | |
| # or product error still blocks the retry below. | |
| "companion_patterns": ( | |
| re.compile( | |
| r"^Error: .*test-native-workbench-v2-electron\.mjs " | |
| r"failed with exit code 1$", | |
| re.MULTILINE, | |
| ), | |
| ), | |
| }, | |
| { | |
| "id": "macos-electron-foreground-prerequisite-v1", | |
| "os": "macOS", | |
| "cases": {"offline-document-workbench-e2e"}, | |
| "pattern": re.compile( | |
| r"^(?:(?:Error: )?electronApplication\.evaluate: Error: |Error: )" | |
| r"ELECTRON_FOREGROUND_PREREQUISITE_MISSING: .+$", | |
| re.MULTILINE, | |
| ), | |
| # The outer offline-workbench runner reports the native | |
| # subprocess exit after streaming the structured inner | |
| # exception. It is part of that same exception stack, not | |
| # a second failure, and is bound to the exact child script. | |
| "companion_patterns": ( | |
| re.compile( | |
| r"^Error: .*test-native-workbench-v2-electron\.mjs " | |
| r"failed with exit code 1$", | |
| re.MULTILINE, | |
| ), | |
| ), | |
| }, | |
| ) | |
| hard_failure_markers = ( | |
| ( | |
| "trusted-overlay-focus-contract-failed-v1", | |
| "TRUSTED_OVERLAY_FOCUS_CONTRACT_FAILED:", | |
| ), | |
| ( | |
| "trusted-overlay-input-contract-failed-v1", | |
| "TRUSTED_OVERLAY_INPUT_CONTRACT_FAILED:", | |
| ), | |
| ("desktop-e2e-phase-timeout-v1", "DESKTOP_E2E_PHASE_TIMEOUT:"), | |
| ("desktop-e2e-phase-failed-v1", "DESKTOP_E2E_PHASE_FAILED:"), | |
| ("desktop-e2e-process-exited-v1", "DESKTOP_E2E_PROCESS_EXITED:"), | |
| ) | |
| generic_failure_markers = ( | |
| ( | |
| "generic-assertion-error-v1", | |
| re.compile( | |
| r"^\s*(?:E\s+)?AssertionError(?:\s*[:(]|\s*$)", | |
| re.MULTILINE, | |
| ), | |
| ), | |
| ( | |
| "generic-error-or-exception-v1", | |
| re.compile( | |
| r"^\s*(?:E\s+)?(?!AssertionError\s*:)(?:Error|Exception|" | |
| r"[A-Za-z_][A-Za-z0-9_.]*(?:Error|Exception))\s*:", | |
| re.MULTILINE, | |
| ), | |
| ), | |
| ( | |
| "python-traceback-v1", | |
| re.compile( | |
| r"^\s*Traceback \(most recent call last\):\s*$", | |
| re.MULTILINE, | |
| ), | |
| ), | |
| ( | |
| "fatal-crash-process-exit-v1", | |
| re.compile( | |
| r"^(?:\s*(?:FATAL(?: ERROR)?|PANIC)\s*:|" | |
| r".*\b(?:Segmentation fault|core dumped)\b.*|" | |
| r".*\b(?:process|command)\b.*\b(?:crashed|exited unexpectedly|" | |
| r"failed with exit code|exited with (?:code|signal))\b.*)$", | |
| re.IGNORECASE | re.MULTILINE, | |
| ), | |
| ), | |
| ) | |
| matched_rules = [ | |
| rule | |
| for rule in rules | |
| if rule["os"] == runner_os | |
| and case_name in rule["cases"] | |
| and rule["pattern"].search(text) is not None | |
| ] | |
| matches = [rule["id"] for rule in matched_rules] | |
| # Remove every occurrence of the one permitted terminal exception, | |
| # including duplicate summaries and narrowly bound outer wrappers. | |
| # Any other terminal failure left in the combined case log blocks | |
| # the retry, even when a permitted signature also occurred. | |
| residual = text | |
| for rule in matched_rules: | |
| residual = rule["pattern"].sub("", residual) | |
| for companion_pattern in rule["companion_patterns"]: | |
| residual = companion_pattern.sub("", residual) | |
| blocked_markers = { | |
| marker_id | |
| for marker_id, marker_text in hard_failure_markers | |
| if marker_text in text | |
| } | |
| generic_residual = residual | |
| for _, marker_text in hard_failure_markers: | |
| generic_residual = re.sub( | |
| rf"^.*{re.escape(marker_text)}.*$", | |
| "", | |
| generic_residual, | |
| flags=re.MULTILINE, | |
| ) | |
| blocked_markers.update( | |
| marker_id | |
| for marker_id, marker_pattern in generic_failure_markers | |
| if marker_pattern.search(generic_residual) is not None | |
| ) | |
| retryable = len(matches) == 1 and not blocked_markers | |
| record = { | |
| "schema_version": 1, | |
| "case": case_name, | |
| "os": runner_os, | |
| "attempt": 1, | |
| "classification": matches[0] if retryable else "non_retryable", | |
| "retryable": retryable, | |
| "blocked_markers": sorted(blocked_markers), | |
| "log_sha256": hashlib.sha256(payload).hexdigest(), | |
| } | |
| with Path(output_name).open("a", encoding="utf-8") as handle: | |
| handle.write(json.dumps(record, sort_keys=True, separators=(",", ":")) + "\n") | |
| if retryable: | |
| shutil.copy2(log_path, Path(evidence_name) / log_path.name) | |
| raise SystemExit(0 if retryable else 1) | |
| PY | |
| } | |
| case "${{ matrix.shard }}" in | |
| all) | |
| entries=( | |
| 'profile-consolidation-flow:scripts/test-profile-consolidation-flow.mjs' | |
| 'primary-repair-accessibility:scripts/test-primary-repair-accessibility.mjs' | |
| 'profile-import-flow:scripts/test-profile-import-flow.mjs' | |
| 'desktop-cleanup-flow:scripts/test-desktop-cleanup-flow.mjs' | |
| 'gateway-ownership:scripts/test-desktop-gateway-ownership.mjs' | |
| 'gateway-orphan-recovery-flow:scripts/test-desktop-gateway-orphan-recovery-flow.mjs' | |
| 'window-background-flow:scripts/test-desktop-window-background-flow.mjs' | |
| 'theme-flow:scripts/test-desktop-theme-flow.mjs' | |
| 'native-workbench-surface:scripts/test-native-workbench-surface-electron.mjs' | |
| 'desktop-zoom-shortcuts:scripts/test-desktop-zoom-shortcuts.mjs' | |
| 'offline-document-workbench-e2e:scripts/test-offline-document-workbench-e2e.mjs' | |
| 'unsafe-legacy-recovery-no-write:scripts/test-unsafe-legacy-recovery-no-write.mjs' | |
| ) | |
| if [[ "${RUNNER_OS}" == "macOS" ]]; then | |
| entries+=( | |
| 'onboarding-flow:scripts/test-onboarding-flow.mjs' | |
| ) | |
| fi | |
| ;; | |
| profiles) | |
| entries=( | |
| 'onboarding-flow:scripts/test-onboarding-flow.mjs' | |
| 'profile-consolidation-flow:scripts/test-profile-consolidation-flow.mjs' | |
| 'primary-repair-accessibility:scripts/test-primary-repair-accessibility.mjs' | |
| 'profile-import-flow:scripts/test-profile-import-flow.mjs' | |
| 'desktop-cleanup-flow:scripts/test-desktop-cleanup-flow.mjs' | |
| ) | |
| ;; | |
| ownership) | |
| entries=( | |
| 'gateway-ownership:scripts/test-desktop-gateway-ownership.mjs' | |
| 'gateway-orphan-recovery-flow:scripts/test-desktop-gateway-orphan-recovery-flow.mjs' | |
| 'window-background-flow:scripts/test-desktop-window-background-flow.mjs' | |
| 'theme-flow:scripts/test-desktop-theme-flow.mjs' | |
| ) | |
| ;; | |
| ownership-workbench) | |
| entries=( | |
| 'gateway-ownership:scripts/test-desktop-gateway-ownership.mjs' | |
| 'gateway-orphan-recovery-flow:scripts/test-desktop-gateway-orphan-recovery-flow.mjs' | |
| 'window-background-flow:scripts/test-desktop-window-background-flow.mjs' | |
| 'theme-flow:scripts/test-desktop-theme-flow.mjs' | |
| 'native-workbench-surface:scripts/test-native-workbench-surface-electron.mjs' | |
| 'desktop-zoom-shortcuts:scripts/test-desktop-zoom-shortcuts.mjs' | |
| 'offline-document-workbench-e2e:scripts/test-offline-document-workbench-e2e.mjs' | |
| 'unsafe-legacy-recovery-no-write:scripts/test-unsafe-legacy-recovery-no-write.mjs' | |
| ) | |
| ;; | |
| workbench) | |
| entries=( | |
| 'native-workbench-surface:scripts/test-native-workbench-surface-electron.mjs' | |
| 'desktop-zoom-shortcuts:scripts/test-desktop-zoom-shortcuts.mjs' | |
| 'offline-document-workbench-e2e:scripts/test-offline-document-workbench-e2e.mjs' | |
| 'unsafe-legacy-recovery-no-write:scripts/test-unsafe-legacy-recovery-no-write.mjs' | |
| ) | |
| ;; | |
| *) | |
| echo "ERROR: unknown Desktop recovery E2E shard: ${{ matrix.shard }}" >&2 | |
| exit 2 | |
| ;; | |
| esac | |
| for entry in "${entries[@]}" | |
| do | |
| name="${entry%%:*}" | |
| script="${entry#*:}" | |
| if run_case "${name}" "${script}" 1; then | |
| continue | |
| fi | |
| first_log="${CI_REPORT_DIR}/${name}-attempt-1.log" | |
| printf 'failed_case=%s\n' "${name}" > "${CI_REPORT_DIR}/first-failure.txt" | |
| if classify_retryable_infrastructure_failure "${name}" "${first_log}" | |
| then | |
| printf 'retry_case=%s\nretry_reason=classified_hosted_runner_infrastructure\n' \ | |
| "${name}" >> "${CI_REPORT_DIR}/first-failure.txt" | |
| if run_case "${name}" "${script}" 2; then | |
| continue | |
| fi | |
| fi | |
| exit 1 | |
| done | |
| printf 'e2e_status=passed\n' >> "${CI_REPORT_DIR}/first-failure.txt" | |
| - name: Upload Desktop recovery summary | |
| if: ${{ success() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: desktop-recovery-e2e-${{ matrix.os }}-${{ matrix.shard }}-attempt-${{ github.run_attempt }} | |
| path: | | |
| ${{ runner.temp }}/desktop-recovery-e2e/environment.txt | |
| ${{ runner.temp }}/desktop-recovery-e2e/first-failure.txt | |
| ${{ runner.temp }}/desktop-recovery-e2e/desktop-e2e-cases.jsonl | |
| ${{ runner.temp }}/desktop-recovery-e2e/retry-classifications.jsonl | |
| ${{ runner.temp }}/desktop-recovery-e2e/retry-evidence | |
| if-no-files-found: error | |
| retention-days: 14 | |
| - name: Upload Desktop recovery failure report | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: desktop-recovery-e2e-${{ matrix.os }}-${{ matrix.shard }}-attempt-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/desktop-recovery-e2e | |
| if-no-files-found: error | |
| retention-days: 14 | |
| skill-hub: | |
| name: skill-hub-contract (${{ matrix.os }}) | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'skill-hub') }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install test dependencies | |
| run: uv sync --extra dev --extra recommended --frozen | |
| - name: Run offline Community Skill contracts | |
| shell: bash | |
| env: | |
| OPENSQUILLA_STATE_DIR: ${{ runner.temp }}/opensquilla-skill-hub-state | |
| OPENSQUILLA_LOG_DIR: ${{ runner.temp }}/opensquilla-skill-hub-logs | |
| PYTHONUTF8: "1" | |
| run: | | |
| set -euo pipefail | |
| uv run pytest \ | |
| tests/test_skills_manifest.py \ | |
| tests/test_skills_bundled_baseline.py \ | |
| tests/test_skills_hot_reload.py \ | |
| tests/test_skills_default_prompt_contract.py \ | |
| tests/test_skills_loader_namespaces.py \ | |
| tests/test_skills_tree.py \ | |
| tests/test_skills_hub_archive.py \ | |
| tests/test_skills_hub_clawhub.py \ | |
| tests/test_skills_hub_github.py \ | |
| tests/test_skills_hub_router.py \ | |
| tests/test_skills_hub_source.py \ | |
| tests/test_skills_hub_installer_security.py \ | |
| tests/test_skills_hub_lockfile_contract.py \ | |
| tests/test_skills_hub_doctor.py \ | |
| tests/test_skills_hash_consumers.py \ | |
| tests/test_skills/test_hub_management_service.py \ | |
| tests/test_skills/test_hub_scanner.py \ | |
| tests/test_skills/test_hub_transaction_recovery.py \ | |
| tests/test_skills/test_hub_transaction_process_gates.py \ | |
| tests/test_gateway/test_rpc_skills_install_visibility.py \ | |
| tests/test_gateway/test_rpc_skills_exact_identity.py \ | |
| tests/test_gateway/test_rpc_skills_coding_gate.py \ | |
| tests/test_gateway/test_rpc_skills_reload.py \ | |
| tests/test_gateway/test_skill_management_service_injection.py \ | |
| tests/test_tools/test_skill_view_resources.py \ | |
| tests/test_scripts/test_bench_skill_integrity.py \ | |
| tests/test_cli/test_cli_product_completeness.py \ | |
| tests/test_cli/test_skills_doctor_cmd.py \ | |
| tests/test_cli/test_skills_gateway_fallback.py \ | |
| tests/test_cli/test_skills_search_cmd.py \ | |
| tests/test_engine/turn_runner/test_provider_and_tools_stage_unit.py \ | |
| -m "not live_skill_hub" \ | |
| --strict-markers \ | |
| -q | |
| release-packaging: | |
| name: Release packaging contracts | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'release-packaging') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| env: | |
| UV_PYTHON: "3.12" | |
| PYTHONPATH: ${{ github.workspace }} | |
| OPENSQUILLA_TURN_CALL_LOG: "0" | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| lfs: true | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| shell: bash | |
| run: uv sync --extra dev --extra recommended --extra mcp --frozen | |
| - name: Run release packaging contract tests | |
| shell: bash | |
| run: | | |
| uv run pytest \ | |
| tests/test_scripts/test_build_wheelhouse_zip.py \ | |
| tests/test_install_scripts.py \ | |
| tests/test_root_start_scripts.py \ | |
| tests/test_release_consistency.py \ | |
| tests/test_public_release_hygiene.py \ | |
| tests/test_sandbox/test_release_contract.py \ | |
| -q | |
| readme-locale-check: | |
| name: README locale parity | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'readme-locale') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: opensquilla-webui/.node-version | |
| - name: Check README locale parity | |
| working-directory: opensquilla-webui | |
| run: node scripts/check-readme-locales.mjs | |
| managed-toolchain-artifacts: | |
| name: Managed Toolchain Artifact E2E | |
| needs: plan-ci | |
| if: ${{ always() && needs.plan-ci.result == 'success' && contains(fromJSON(needs.plan-ci.outputs.required_suites), 'managed-toolchain') }} | |
| uses: ./.github/workflows/managed-toolchain-artifacts.yml | |
| main-canary: | |
| name: Queue/main installation and offline gateway canary | |
| needs: queue-attestation | |
| if: >- | |
| ${{ always() | |
| && ((github.event_name == 'push' | |
| && (vars.CI_OPTIMIZATION_MODE || 'enforce') == 'enforce') | |
| || (github.event_name == 'merge_group' | |
| && (vars.CI_OPTIMIZATION_MODE || 'enforce') == 'enforce' | |
| && needs.queue-attestation.result == 'success' | |
| && needs.queue-attestation.outputs.reusable == 'true')) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| env: | |
| UV_PYTHON: "3.12" | |
| PYTHONPATH: ${{ github.workspace }} | |
| OPENSQUILLA_TURN_CALL_LOG: "0" | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - name: Configure isolated runtime directories | |
| shell: bash | |
| run: | | |
| printf 'OPENSQUILLA_STATE_DIR=%s/opensquilla-state\n' "${RUNNER_TEMP}" >> "${GITHUB_ENV}" | |
| printf 'OPENSQUILLA_LOG_DIR=%s/opensquilla-logs\n' "${RUNNER_TEMP}" >> "${GITHUB_ENV}" | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Set up Node.js | |
| if: ${{ github.event_name == 'push' }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: opensquilla-webui/.node-version | |
| cache: npm | |
| cache-dependency-path: opensquilla-webui/package-lock.json | |
| - name: Install locked development dependencies | |
| run: uv sync --extra dev --extra recommended --extra mcp --frozen | |
| - name: Build production WebUI artifact | |
| if: ${{ github.event_name == 'push' }} | |
| working-directory: opensquilla-webui | |
| run: | | |
| npm ci | |
| npm run build:artifact | |
| npm run verify:release-dist | |
| - name: Build and install wheel in a clean environment | |
| if: ${{ github.event_name == 'push' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| wheel_dir="${RUNNER_TEMP}/canary-wheel" | |
| venv_dir="${RUNNER_TEMP}/canary-venv" | |
| uv build --wheel --out-dir "${wheel_dir}" | |
| wheels=("${wheel_dir}"/opensquilla-*.whl) | |
| if [[ "${#wheels[@]}" -ne 1 || ! -f "${wheels[0]}" ]]; then | |
| echo "ERROR: expected exactly one canary wheel" >&2 | |
| exit 1 | |
| fi | |
| uv venv "${venv_dir}" --python 3.12 | |
| uv pip install --python "${venv_dir}/bin/python" "${wheels[0]}" | |
| "${venv_dir}/bin/python" -c "import opensquilla; import opensquilla.gateway.app" | |
| "${venv_dir}/bin/opensquilla" --help >/dev/null | |
| - name: Run offline gateway and provider canary | |
| run: >- | |
| uv run pytest | |
| tests/functional/test_gateway_silent_reply_process_e2e.py | |
| -q --durations=10 | |
| ci-result: | |
| name: CI result | |
| needs: | |
| - queue-attestation | |
| - plan-ci | |
| - workflow-lint | |
| - readme-locale-check | |
| - frontend-artifact | |
| - frontend-check | |
| - gateway-contract-windows | |
| - webui-chat-recovery | |
| - tui-check | |
| - desktop-check | |
| - ubuntu-quality | |
| - ubuntu-full | |
| - windows-full | |
| - macos-recovery | |
| - desktop-recovery-e2e | |
| - skill-hub | |
| - release-packaging | |
| - managed-toolchain-artifacts | |
| - main-canary | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Accept verified queue or main fast path | |
| if: >- | |
| ${{ (github.event_name == 'merge_group' | |
| && (vars.CI_OPTIMIZATION_MODE || 'enforce') == 'enforce' | |
| && needs.queue-attestation.result == 'success' | |
| && needs.queue-attestation.outputs.reusable == 'true') | |
| || (github.event_name == 'push' | |
| && (vars.CI_OPTIMIZATION_MODE || 'enforce') == 'enforce') }} | |
| env: | |
| QUEUE_RESULT: ${{ needs.queue-attestation.result }} | |
| QUEUE_REUSABLE: ${{ needs.queue-attestation.outputs.reusable }} | |
| QUEUE_REASON: ${{ needs.queue-attestation.outputs.reason }} | |
| MAIN_CANARY_RESULT: ${{ needs.main-canary.result }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${GITHUB_EVENT_NAME}" == "merge_group" ]]; then | |
| if [[ "${QUEUE_RESULT}" != "success" || "${QUEUE_REUSABLE}" != "true" ]]; then | |
| echo "ERROR: merge queue fast path lacks trusted green evidence: ${QUEUE_REASON}" >&2 | |
| exit 1 | |
| fi | |
| if [[ "${MAIN_CANARY_RESULT}" != "success" ]]; then | |
| echo "ERROR: merge queue combined-tree canary must succeed; got ${MAIN_CANARY_RESULT:-missing}." >&2 | |
| exit 1 | |
| fi | |
| echo "Merge queue reused trusted CI evidence and passed the combined-tree canary: ${QUEUE_REASON}" | |
| elif [[ "${MAIN_CANARY_RESULT}" != "success" ]]; then | |
| echo "ERROR: main canary must succeed; got ${MAIN_CANARY_RESULT:-missing}." >&2 | |
| exit 1 | |
| else | |
| echo "Main installation and offline gateway canary succeeded." | |
| fi | |
| - name: Check out repository | |
| if: >- | |
| ${{ github.event_name != 'push' | |
| || (vars.CI_OPTIMIZATION_MODE || 'enforce') != 'enforce' }} | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 2 | |
| persist-credentials: false | |
| - name: Set up Python | |
| if: >- | |
| ${{ !((github.event_name == 'merge_group' | |
| && (vars.CI_OPTIMIZATION_MODE || 'enforce') == 'enforce' | |
| && needs.queue-attestation.result == 'success' | |
| && needs.queue-attestation.outputs.reusable == 'true') | |
| || (github.event_name == 'push' | |
| && (vars.CI_OPTIMIZATION_MODE || 'enforce') == 'enforce')) }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Check required CI results | |
| if: >- | |
| ${{ !((github.event_name == 'merge_group' | |
| && (vars.CI_OPTIMIZATION_MODE || 'enforce') == 'enforce' | |
| && needs.queue-attestation.result == 'success' | |
| && needs.queue-attestation.outputs.reusable == 'true') | |
| || (github.event_name == 'push' | |
| && (vars.CI_OPTIMIZATION_MODE || 'enforce') == 'enforce')) }} | |
| env: | |
| RESULT_PLANNER: ${{ needs.plan-ci.result }} | |
| RESULT_WORKFLOW_LINT: ${{ needs.workflow-lint.result }} | |
| RESULT_README_LOCALE: ${{ needs.readme-locale-check.result }} | |
| RESULT_FRONTEND_ARTIFACT: ${{ needs.frontend-artifact.result }} | |
| RESULT_FRONTEND: ${{ needs.frontend-check.result }} | |
| RESULT_CONTRACT_WINDOWS: ${{ needs.gateway-contract-windows.result }} | |
| RESULT_TUI: ${{ needs.tui-check.result }} | |
| RESULT_DESKTOP: ${{ needs.desktop-check.result }} | |
| RESULT_UBUNTU: ${{ needs.ubuntu-quality.result }} | |
| RESULT_UBUNTU_FULL: ${{ needs.ubuntu-full.result }} | |
| RESULT_WINDOWS_FULL: ${{ needs.windows-full.result }} | |
| RESULT_MACOS_RECOVERY: ${{ needs.macos-recovery.result }} | |
| RESULT_DESKTOP_RECOVERY_E2E: ${{ needs.desktop-recovery-e2e.result }} | |
| RESULT_WEBUI_CHAT_RECOVERY: ${{ needs.webui-chat-recovery.result }} | |
| RESULT_SKILL_HUB: ${{ needs.skill-hub.result }} | |
| RESULT_RELEASE: ${{ needs.release-packaging.result }} | |
| RESULT_MANAGED_TOOLCHAIN_ARTIFACTS: ${{ needs.managed-toolchain-artifacts.result }} | |
| REQUIRED_SUITES: ${{ needs.plan-ci.outputs.required_suites }} | |
| run: python .github/scripts/check_ci_results.py | |
| - name: Create trusted CI evidence v2 | |
| id: attestation | |
| if: >- | |
| ${{ env.CI_OPTIMIZATION_MODE != 'legacy' | |
| && (github.event_name == 'pull_request' | |
| || (github.event_name == 'merge_group' | |
| && !(env.CI_OPTIMIZATION_MODE == 'enforce' | |
| && needs.queue-attestation.result == 'success' | |
| && needs.queue-attestation.outputs.reusable == 'true'))) }} | |
| env: | |
| PLANNED_SUCCESSFUL_SUITES: ${{ needs.plan-ci.outputs.required_suites }} | |
| PLANNED_PLANNER_DIGEST: ${{ needs.plan-ci.outputs.plan_digest }} | |
| PLANNED_SUITE_EXECUTION_DIGESTS: ${{ needs.plan-ci.outputs.suite_execution_digests }} | |
| PLANNED_PLATFORM_MATRIX: ${{ needs.plan-ci.outputs.platform_matrix }} | |
| PLANNED_FULL_FALLBACK: ${{ needs.plan-ci.outputs.full_fallback }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "${PLANNED_SUCCESSFUL_SUITES}" \ | |
| || -z "${PLANNED_PLANNER_DIGEST}" \ | |
| || -z "${PLANNED_SUITE_EXECUTION_DIGESTS}" \ | |
| || -z "${PLANNED_PLATFORM_MATRIX}" \ | |
| || -z "${PLANNED_FULL_FALLBACK}" ]]; then | |
| echo "ERROR: planner evidence metadata is incomplete." >&2 | |
| exit 2 | |
| fi | |
| plan_basis="change_set" | |
| if [[ "${PLANNED_FULL_FALLBACK}" == "true" ]]; then | |
| plan_basis="full_fallback" | |
| fi | |
| python3 .github/scripts/ci_attestation.py create \ | |
| --repository "${GITHUB_REPOSITORY}" \ | |
| --event-path "${GITHUB_EVENT_PATH}" \ | |
| --run-id "${GITHUB_RUN_ID}" \ | |
| --run-attempt "${GITHUB_RUN_ATTEMPT}" \ | |
| --workflow-ref "${GITHUB_WORKFLOW_REF}" \ | |
| --optimization-mode "${CI_OPTIMIZATION_MODE}" \ | |
| --output "${RUNNER_TEMP}/ci-attestation.json" \ | |
| --github-output "${GITHUB_OUTPUT}" \ | |
| --successful-suites "${PLANNED_SUCCESSFUL_SUITES}" \ | |
| --planner-digest "${PLANNED_PLANNER_DIGEST}" \ | |
| --suite-execution-digests "${PLANNED_SUITE_EXECUTION_DIGESTS}" \ | |
| --platform-matrix "${PLANNED_PLATFORM_MATRIX}" \ | |
| --plan-basis "${plan_basis}" | |
| - name: Upload tree-indexed CI evidence v2 | |
| if: ${{ steps.attestation.outcome == 'success' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ci-evidence-v2-tree-${{ steps.attestation.outputs.tree_sha }} | |
| path: ${{ runner.temp }}/ci-attestation.json | |
| if-no-files-found: error | |
| retention-days: 4 | |
| overwrite: true | |
| - name: Upload PR-head-indexed CI evidence v2 | |
| if: ${{ github.event_name == 'pull_request' && steps.attestation.outcome == 'success' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ci-evidence-v2-pr-${{ steps.attestation.outputs.pull_request_number }}-${{ steps.attestation.outputs.head_sha }} | |
| path: ${{ runner.temp }}/ci-attestation.json | |
| if-no-files-found: error | |
| retention-days: 4 | |
| overwrite: true | |
| - name: Create full-nightly health evidence | |
| if: ${{ github.event_name == 'schedule' }} | |
| run: >- | |
| python3 .github/scripts/ci_attestation.py create-nightly-health | |
| --repository "${GITHUB_REPOSITORY}" | |
| --run-id "${GITHUB_RUN_ID}" | |
| --run-attempt "${GITHUB_RUN_ATTEMPT}" | |
| --workflow-ref "${GITHUB_WORKFLOW_REF}" | |
| --output "${RUNNER_TEMP}/ci-nightly-health.json" | |
| - name: Upload full-nightly health evidence | |
| if: ${{ github.event_name == 'schedule' }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ci-nightly-health-v1 | |
| path: ${{ runner.temp }}/ci-nightly-health.json | |
| if-no-files-found: error | |
| retention-days: 4 | |
| overwrite: true |