name: PR Quality on: pull_request: types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] permissions: contents: read pull-requests: read concurrency: group: pr-quality-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: scope-plan: name: scope-plan runs-on: ubuntu-latest outputs: areas: ${{ steps.policy.outputs.areas }} area_labels: ${{ steps.policy.outputs.area_labels }} blocked: ${{ steps.policy.outputs.blocked }} blocking_reasons: ${{ steps.policy.outputs.blocking_reasons }} desktop_checks: ${{ steps.policy.outputs.desktop_checks }} server_checks: ${{ steps.policy.outputs.server_checks }} adapter_checks: ${{ steps.policy.outputs.adapter_checks }} desktop_native_checks: ${{ steps.policy.outputs.desktop_native_checks }} provider_contract_checks: ${{ steps.policy.outputs.provider_contract_checks }} chat_contract_checks: ${{ steps.policy.outputs.chat_contract_checks }} persistence_checks: ${{ steps.policy.outputs.persistence_checks }} policy_checks: ${{ steps.policy.outputs.policy_checks }} docs_checks: ${{ steps.policy.outputs.docs_checks }} coverage_checks: ${{ steps.policy.outputs.coverage_checks }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Collect changed files env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} REPOSITORY: ${{ github.repository }} run: | gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename' > changed-files.txt jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH" > labels.txt - name: Build deterministic scope plan id: policy run: bun run scripts/pr/change-policy.ts --files changed-files.txt --labels-file labels.txt --plan-only policy-enforcement: name: policy-enforcement needs: scope-plan if: ${{ always() && needs.scope-plan.result == 'success' }} runs-on: ubuntu-latest steps: - name: Checkout policy sources if: needs.scope-plan.outputs.policy_checks == 'true' uses: actions/checkout@v4 - name: Setup Bun if: needs.scope-plan.outputs.policy_checks == 'true' uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Install root dependencies if: needs.scope-plan.outputs.policy_checks == 'true' run: bun install --frozen-lockfile - name: Run policy regression tests if: needs.scope-plan.outputs.policy_checks == 'true' run: bun run check:policy - name: Enforce change policy env: BLOCKED: ${{ needs.scope-plan.outputs.blocked }} BLOCKING_REASONS: ${{ needs.scope-plan.outputs.blocking_reasons }} run: | if [ "$BLOCKED" = "true" ]; then echo "::error::${BLOCKING_REASONS:-Change policy blocked this pull request}" exit 1 fi echo "Change policy passed" desktop-checks: name: desktop-checks needs: scope-plan if: needs.scope-plan.outputs.desktop_checks == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Install root dependencies run: bun install --frozen-lockfile - name: Install desktop dependencies working-directory: desktop run: bun install --frozen-lockfile - name: Run desktop checks run: bun run check:desktop server-checks: name: server-checks needs: scope-plan if: needs.scope-plan.outputs.server_checks == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Install root dependencies run: bun install --frozen-lockfile - name: Run root runtime checks run: bun run check:server provider-contract-checks: name: provider-contract-checks needs: scope-plan if: needs.scope-plan.outputs.provider_contract_checks == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Install root dependencies run: bun install --frozen-lockfile - name: Run offline provider contracts run: bun run check:provider-contract chat-contract-checks: name: chat-contract-checks needs: scope-plan if: needs.scope-plan.outputs.chat_contract_checks == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Install root dependencies run: bun install --frozen-lockfile - name: Install desktop dependencies working-directory: desktop run: bun install --frozen-lockfile - name: Run desktop-server chat contracts run: bun run check:chat-contract adapter-checks: name: adapter-checks needs: scope-plan if: needs.scope-plan.outputs.adapter_checks == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Install adapter dependencies working-directory: adapters run: bun install --frozen-lockfile - name: Run adapter checks run: bun run check:adapters desktop-native-checks: name: desktop-native-checks needs: scope-plan if: needs.scope-plan.outputs.desktop_native_checks == 'true' runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install Linux dependencies run: | sudo apt-get update sudo apt-get install -y \ build-essential curl wget file \ libxdo-dev libssl-dev patchelf \ libfuse2 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Install root dependencies run: bun install --frozen-lockfile - name: Install desktop dependencies working-directory: desktop run: bun install --frozen-lockfile - name: Run desktop native checks run: bun run check:native persistence-checks: name: persistence-checks needs: scope-plan if: needs.scope-plan.outputs.persistence_checks == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Install root dependencies run: bun install --frozen-lockfile - name: Install desktop dependencies working-directory: desktop run: bun install --frozen-lockfile - name: Run persistence upgrade contracts run: bun run check:persistence-upgrade docs-checks: name: docs-checks needs: scope-plan if: needs.scope-plan.outputs.docs_checks == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - name: Run docs checks run: npm ci && npm run docs:build coverage-checks: name: coverage-checks needs: scope-plan if: needs.scope-plan.outputs.coverage_checks == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.12 - name: Install root dependencies run: bun install --frozen-lockfile - name: Install desktop dependencies working-directory: desktop run: bun install --frozen-lockfile - name: Install adapter dependencies working-directory: adapters run: bun install --frozen-lockfile - name: Run coverage checks env: COVERAGE_BASE_REF: origin/${{ github.base_ref }} run: bun run check:coverage - name: Summarize coverage report if: always() run: | latest_report="$(find artifacts/coverage -name coverage-report.md -print | sort | tail -n 1)" if [ -n "$latest_report" ]; then cat "$latest_report" >> "$GITHUB_STEP_SUMMARY" fi - name: Upload coverage report if: always() uses: actions/upload-artifact@v4 with: name: coverage-report path: artifacts/coverage/ retention-days: 14 pr-quality-gate: name: pr-quality-gate needs: - scope-plan - policy-enforcement - desktop-checks - server-checks - provider-contract-checks - chat-contract-checks - adapter-checks - desktop-native-checks - persistence-checks - docs-checks - coverage-checks if: always() runs-on: ubuntu-latest steps: - name: Require every selected quality job to pass run: | failures=0 require_success() { local name="$1" local result="$2" if [ "$result" != "success" ]; then echo "::error::$name ended with result: $result" failures=$((failures + 1)) else echo "$name: success" fi } require_selected() { local name="$1" local expected="$2" local result="$3" if [ "$expected" = "true" ]; then require_success "$name" "$result" elif [ "$result" != "skipped" ]; then echo "::error::$name was not selected but ended with result: $result" failures=$((failures + 1)) else echo "$name: skipped by scope plan" fi } require_success "scope-plan" "${{ needs.scope-plan.result }}" require_success "policy-enforcement" "${{ needs.policy-enforcement.result }}" require_selected "desktop-checks" "${{ needs.scope-plan.outputs.desktop_checks }}" "${{ needs.desktop-checks.result }}" require_selected "server-checks" "${{ needs.scope-plan.outputs.server_checks }}" "${{ needs.server-checks.result }}" require_selected "provider-contract-checks" "${{ needs.scope-plan.outputs.provider_contract_checks }}" "${{ needs.provider-contract-checks.result }}" require_selected "chat-contract-checks" "${{ needs.scope-plan.outputs.chat_contract_checks }}" "${{ needs.chat-contract-checks.result }}" require_selected "adapter-checks" "${{ needs.scope-plan.outputs.adapter_checks }}" "${{ needs.adapter-checks.result }}" require_selected "desktop-native-checks" "${{ needs.scope-plan.outputs.desktop_native_checks }}" "${{ needs.desktop-native-checks.result }}" require_selected "persistence-checks" "${{ needs.scope-plan.outputs.persistence_checks }}" "${{ needs.persistence-checks.result }}" require_selected "docs-checks" "${{ needs.scope-plan.outputs.docs_checks }}" "${{ needs.docs-checks.result }}" require_selected "coverage-checks" "${{ needs.scope-plan.outputs.coverage_checks }}" "${{ needs.coverage-checks.result }}" if [ "$failures" -gt 0 ]; then exit 1 fi