chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,876 @@
|
||||
name: CI
|
||||
|
||||
"on":
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
OPENSQUILLA_TESTING: "true"
|
||||
|
||||
jobs:
|
||||
classify-changes:
|
||||
name: Classify changed files
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
docs_only: ${{ steps.classify.outputs.docs_only }}
|
||||
runtime_changed: ${{ steps.classify.outputs.runtime_changed }}
|
||||
test_changed: ${{ steps.classify.outputs.test_changed }}
|
||||
ci_changed: ${{ steps.classify.outputs.ci_changed }}
|
||||
dependency_changed: ${{ steps.classify.outputs.dependency_changed }}
|
||||
release_changed: ${{ steps.classify.outputs.release_changed }}
|
||||
windows_full_required: ${{ steps.classify.outputs.windows_full_required }}
|
||||
frontend_changed: ${{ steps.classify.outputs.frontend_changed }}
|
||||
tui_changed: ${{ steps.classify.outputs.tui_changed }}
|
||||
desktop_changed: ${{ steps.classify.outputs.desktop_changed }}
|
||||
python_changed: ${{ steps.classify.outputs.python_changed }}
|
||||
platform_sensitive_changed: ${{ steps.classify.outputs.platform_sensitive_changed }}
|
||||
build_wheel_required: ${{ steps.classify.outputs.build_wheel_required }}
|
||||
full_required: ${{ steps.classify.outputs.full_required }}
|
||||
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 "${{ github.event_name }}" in
|
||||
pull_request)
|
||||
git diff --name-only \
|
||||
"${{ github.event.pull_request.base.sha }}" \
|
||||
"${{ github.event.pull_request.head.sha }}" > "${changed_files}"
|
||||
;;
|
||||
push)
|
||||
before="${{ github.event.before }}"
|
||||
after="${{ github.event.after }}"
|
||||
if [[ -n "${before}" && "${before}" != "0000000000000000000000000000000000000000" ]]; then
|
||||
git diff --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: Classify changed files
|
||||
id: classify
|
||||
shell: bash
|
||||
run: bash .github/scripts/classify-ci-changes.sh "${CHANGED_FILES}"
|
||||
|
||||
workflow-lint:
|
||||
name: Validate GitHub Actions workflows
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run actionlint
|
||||
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color=false
|
||||
|
||||
frontend-check:
|
||||
name: Frontend build and typecheck
|
||||
needs: classify-changes
|
||||
if: ${{ needs.classify-changes.outputs.frontend_changed == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
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: '20.19.0'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: opensquilla-webui/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: opensquilla-webui
|
||||
run: npm ci
|
||||
|
||||
- name: Run frontend unit tests
|
||||
working-directory: opensquilla-webui
|
||||
run: npm run test:unit
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: opensquilla-webui
|
||||
run: npm run build
|
||||
|
||||
- name: Verify committed dist is fresh
|
||||
run: |
|
||||
if [ ! -f "src/opensquilla/gateway/static/dist/index.html" ]; then
|
||||
echo "ERROR: Frontend dist not found"
|
||||
exit 1
|
||||
fi
|
||||
# The build above regenerates src/opensquilla/gateway/static/dist. If
|
||||
# the committed dist differs from a fresh build, a Web UI source change
|
||||
# was made without rebuilding + committing the bundle it ships (the
|
||||
# gateway serves the committed dist, not source). Fail so a stale
|
||||
# bundle can never ship (e.g. an approval-button removal that only
|
||||
# lands in source — the #413 residue).
|
||||
if ! git diff --quiet -- src/opensquilla/gateway/static/dist; then
|
||||
echo "ERROR: committed Web UI dist is stale — rebuild and commit it:"
|
||||
echo " cd opensquilla-webui && npm ci && npm run build"
|
||||
echo "Changed dist files:"
|
||||
git --no-pager diff --stat -- src/opensquilla/gateway/static/dist
|
||||
exit 1
|
||||
fi
|
||||
echo "Frontend dist verified fresh"
|
||||
|
||||
tui-check:
|
||||
name: OpenTUI package tests
|
||||
needs: classify-changes
|
||||
if: ${{ needs.classify-changes.outputs.tui_changed == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
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: '20.19.0'
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- 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
|
||||
run: bun run test:bun
|
||||
|
||||
desktop-check:
|
||||
name: Desktop Electron unit tests
|
||||
needs: classify-changes
|
||||
if: ${{ needs.classify-changes.outputs.desktop_changed == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
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: '20.19.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-secret-storage-policy.mjs
|
||||
node scripts/test-update-resolver.mjs
|
||||
node scripts/test-desktop-locale.mjs
|
||||
node scripts/test-desktop-profile-substrate.mjs
|
||||
node scripts/test-desktop-profile-context.mjs
|
||||
node scripts/test-desktop-cleanup-contract.mjs
|
||||
|
||||
ubuntu-quality:
|
||||
name: Lint, test, and build (ubuntu-latest, 3.12)
|
||||
needs: classify-changes
|
||||
if: ${{ needs.classify-changes.outputs.python_changed == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
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: ${{ needs.classify-changes.outputs.full_required == '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: 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
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
markers="not llm and not live_search 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"
|
||||
|
||||
if [[ "${{ needs.classify-changes.outputs.full_required }}" == "true" ]]; then
|
||||
uv run pytest tests -q -m "${markers}" --durations=50
|
||||
else
|
||||
uv run pytest \
|
||||
tests/unit \
|
||||
tests/test_ci \
|
||||
--ignore=tests/test_ci/test_router_artifact_manifest.py \
|
||||
tests/test_desktop/test_electron_startup_contract.py \
|
||||
tests/test_recovery \
|
||||
tests/test_migration/test_opensquilla_home_migration.py \
|
||||
tests/test_engine/test_coding_mode.py \
|
||||
tests/test_engine/test_stream_wrappers.py \
|
||||
tests/test_tools/test_shell_process_isolation.py \
|
||||
-q -m "${markers}" --durations=50
|
||||
fi
|
||||
|
||||
- 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
|
||||
|
||||
- name: tracemalloc leak smoke test
|
||||
if: ${{ needs.classify-changes.outputs.full_required == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
uv run python -m pytest tests/test_gateway/test_task_runtime_terminal_cleanup.py::test_no_leak_under_load -v
|
||||
|
||||
- name: Build wheel
|
||||
if: ${{ needs.classify-changes.outputs.build_wheel_required == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
shell: bash
|
||||
run: uv build --wheel
|
||||
|
||||
windows-compat:
|
||||
name: Windows compatibility smoke (3.12)
|
||||
needs: classify-changes
|
||||
if: ${{ needs.classify-changes.outputs.python_changed == 'true' || needs.classify-changes.outputs.platform_sensitive_changed == 'true' || needs.classify-changes.outputs.dependency_changed == 'true' || needs.classify-changes.outputs.release_changed == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
runs-on: windows-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
|
||||
|
||||
- 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 Windows compatibility smoke
|
||||
shell: bash
|
||||
run: |
|
||||
uv run pytest \
|
||||
tests/test_artifacts.py \
|
||||
tests/test_contrib/test_codetask \
|
||||
tests/test_desktop/test_electron_startup_contract.py \
|
||||
tests/test_engine/test_coding_mode.py \
|
||||
tests/test_engine/test_stream_wrappers.py \
|
||||
tests/test_tools/test_shell_process_isolation.py \
|
||||
-q
|
||||
|
||||
windows-full:
|
||||
name: Windows high-risk (${{ matrix.shard }})
|
||||
needs: classify-changes
|
||||
if: ${{ needs.classify-changes.outputs.windows_full_required == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard:
|
||||
- core
|
||||
- gateway-sqlite
|
||||
- recovery-migration
|
||||
- desktop-installer-contracts
|
||||
|
||||
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: ${{ matrix.shard == 'core' }}
|
||||
|
||||
- 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: latest
|
||||
|
||||
- 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 native recovery move primitives first
|
||||
if: ${{ matrix.shard == 'recovery-migration' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
maxfail_args=()
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
maxfail_args+=(--maxfail=3)
|
||||
fi
|
||||
|
||||
set +e
|
||||
uv run pytest \
|
||||
tests/test_recovery/test_atomic_and_locking.py::test_native_move_moves_a_regular_tree_between_real_parents \
|
||||
tests/test_recovery/test_atomic_and_locking.py::test_windows_real_legacy_lock_survives_profile_move_and_rebind \
|
||||
tests/test_recovery/test_atomic_and_locking.py::test_windows_real_replacement_locks_survive_two_profile_moves \
|
||||
tests/test_recovery/test_atomic_and_locking.py::test_windows_real_recent_locked_profile_tree_moves_without_metadata_false_positive \
|
||||
tests/test_recovery/test_atomic_and_locking.py::test_windows_primitive_collision_preserves_both_trees \
|
||||
tests/test_recovery/test_atomic_and_locking.py::test_windows_native_move_refuses_real_cross_volume_move \
|
||||
tests/test_recovery/test_atomic_and_locking.py::test_windows_native_move_handles_real_path_longer_than_260_characters \
|
||||
tests/test_recovery/test_atomic_and_locking.py::test_windows_native_move_rejects_real_junction_in_source_tree \
|
||||
tests/test_recovery/test_atomic_and_locking.py::test_windows_no_replace_pins_both_parents_during_real_mutation_window \
|
||||
-q \
|
||||
--durations=25 \
|
||||
--junitxml="${CI_REPORT_DIR}/native-move-junit.xml" \
|
||||
"${maxfail_args[@]}" \
|
||||
2>&1 | tee "${CI_REPORT_DIR}/native-move.log"
|
||||
status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
if [[ "${status}" -ne 0 ]]; then
|
||||
printf 'native_move_exit_code=%s\n' "${status}" \
|
||||
> "${CI_REPORT_DIR}/first-failure.txt"
|
||||
exit "${status}"
|
||||
fi
|
||||
|
||||
- name: Test Windows shard
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
markers="not llm and not live_search 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" ]]; 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" \
|
||||
-- \
|
||||
-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: classify-changes
|
||||
if: ${{ needs.classify-changes.outputs.platform_sensitive_changed == 'true' || needs.classify-changes.outputs.desktop_changed == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
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
|
||||
maxfail_args=()
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
maxfail_args+=(--maxfail=3)
|
||||
fi
|
||||
|
||||
set +e
|
||||
uv run pytest \
|
||||
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" \
|
||||
"${maxfail_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:
|
||||
name: Desktop recovery E2E (${{ matrix.os }})
|
||||
needs: classify-changes
|
||||
if: ${{ needs.classify-changes.outputs.platform_sensitive_changed == 'true' || needs.classify-changes.outputs.desktop_changed == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
|
||||
env:
|
||||
UV_PYTHON: "3.12"
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
OPENSQUILLA_TURN_CALL_LOG: "0"
|
||||
|
||||
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}"
|
||||
} > "${report_dir}/environment.txt"
|
||||
printf 'e2e_status=not_started\n' > "${report_dir}/first-failure.txt"
|
||||
|
||||
- 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: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.19.0'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: 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: 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_RECOVERY_SCREENSHOT: ${{ runner.temp }}/desktop-recovery-e2e/recovery-page.png
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
run_case() {
|
||||
local name="$1"
|
||||
local script="$2"
|
||||
if [[ "${RUNNER_OS}" == "Linux" ]]; then
|
||||
xvfb-run -a node "${script}" 2>&1 | tee "${CI_REPORT_DIR}/${name}.log"
|
||||
else
|
||||
node "${script}" 2>&1 | tee "${CI_REPORT_DIR}/${name}.log"
|
||||
fi
|
||||
}
|
||||
|
||||
for entry in \
|
||||
'profile-recovery-flow:scripts/test-profile-recovery-flow.mjs' \
|
||||
'profile-recovery-accessibility:scripts/test-profile-recovery-accessibility.mjs' \
|
||||
'profile-import-flow:scripts/test-profile-import-flow.mjs' \
|
||||
'unsafe-profile-no-write:scripts/test-unsafe-profile-no-write.mjs'
|
||||
do
|
||||
name="${entry%%:*}"
|
||||
script="${entry#*:}"
|
||||
if ! run_case "${name}" "${script}"; then
|
||||
printf 'failed_case=%s\n' "${name}" > "${CI_REPORT_DIR}/first-failure.txt"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
printf 'e2e_status=passed\n' > "${CI_REPORT_DIR}/first-failure.txt"
|
||||
|
||||
- name: Upload Desktop recovery report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: desktop-recovery-e2e-${{ matrix.os }}-attempt-${{ github.run_attempt }}
|
||||
path: ${{ runner.temp }}/desktop-recovery-e2e
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
release-packaging:
|
||||
name: Release packaging contracts
|
||||
needs: classify-changes
|
||||
if: ${{ needs.classify-changes.outputs.release_changed == 'true' || needs.classify-changes.outputs.full_required == 'true' }}
|
||||
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 \
|
||||
-q
|
||||
|
||||
readme-locale-check:
|
||||
name: README locale parity
|
||||
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: '20.19.0'
|
||||
|
||||
- name: Check README locale parity
|
||||
working-directory: opensquilla-webui
|
||||
run: node scripts/check-readme-locales.mjs
|
||||
|
||||
ci-result:
|
||||
name: CI result
|
||||
needs:
|
||||
- classify-changes
|
||||
- workflow-lint
|
||||
- readme-locale-check
|
||||
- frontend-check
|
||||
- tui-check
|
||||
- desktop-check
|
||||
- ubuntu-quality
|
||||
- windows-compat
|
||||
- windows-full
|
||||
- macos-recovery
|
||||
- desktop-recovery-e2e
|
||||
- release-packaging
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Check required CI results
|
||||
env:
|
||||
RESULT_CLASSIFY: ${{ needs.classify-changes.result }}
|
||||
RESULT_WORKFLOW_LINT: ${{ needs.workflow-lint.result }}
|
||||
RESULT_README_LOCALE: ${{ needs.readme-locale-check.result }}
|
||||
RESULT_FRONTEND: ${{ needs.frontend-check.result }}
|
||||
RESULT_TUI: ${{ needs.tui-check.result }}
|
||||
RESULT_DESKTOP: ${{ needs.desktop-check.result }}
|
||||
RESULT_UBUNTU: ${{ needs.ubuntu-quality.result }}
|
||||
RESULT_WINDOWS_SMOKE: ${{ needs.windows-compat.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_RELEASE: ${{ needs.release-packaging.result }}
|
||||
FLAG_DOCS_ONLY: ${{ needs.classify-changes.outputs.docs_only }}
|
||||
FLAG_RUNTIME_CHANGED: ${{ needs.classify-changes.outputs.runtime_changed }}
|
||||
FLAG_TEST_CHANGED: ${{ needs.classify-changes.outputs.test_changed }}
|
||||
FLAG_CI_CHANGED: ${{ needs.classify-changes.outputs.ci_changed }}
|
||||
FLAG_DEPENDENCY_CHANGED: ${{ needs.classify-changes.outputs.dependency_changed }}
|
||||
FLAG_RELEASE_CHANGED: ${{ needs.classify-changes.outputs.release_changed }}
|
||||
FLAG_WINDOWS_FULL_REQUIRED: ${{ needs.classify-changes.outputs.windows_full_required }}
|
||||
FLAG_FRONTEND_CHANGED: ${{ needs.classify-changes.outputs.frontend_changed }}
|
||||
FLAG_TUI_CHANGED: ${{ needs.classify-changes.outputs.tui_changed }}
|
||||
FLAG_DESKTOP_CHANGED: ${{ needs.classify-changes.outputs.desktop_changed }}
|
||||
FLAG_PYTHON_CHANGED: ${{ needs.classify-changes.outputs.python_changed }}
|
||||
FLAG_PLATFORM_SENSITIVE_CHANGED: ${{ needs.classify-changes.outputs.platform_sensitive_changed }}
|
||||
FLAG_BUILD_WHEEL_REQUIRED: ${{ needs.classify-changes.outputs.build_wheel_required }}
|
||||
FLAG_FULL_REQUIRED: ${{ needs.classify-changes.outputs.full_required }}
|
||||
run: python .github/scripts/check_ci_results.py
|
||||
Reference in New Issue
Block a user