Skip to content

fix(cli): reject non-positive list limits #4360

fix(cli): reject non-positive list limits

fix(cli): reject non-positive list limits #4360

Workflow file for this run

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