feat(enrichment): forward PR body and linked-issue envelope to REES #4796
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| permissions: | |
| contents: read | |
| concurrency: | |
| # Keep pull requests in one ref-scoped group so newer commits cancel superseded PR runs. | |
| # Add github.sha for push runs so distinct main commits do not cancel each other's validation. | |
| group: ci-${{ github.ref }}-${{ github.event_name == 'push' && github.sha || 'pr' }} | |
| # NB: keep this a literal boolean. An expression here (cancel-in-progress: ${{ ... }}) made GitHub | |
| # fail the workflow at startup (startup_failure), so `validate` never reported. | |
| cancel-in-progress: true | |
| jobs: | |
| # Detect which areas a PR touches so the heavy jobs can skip when irrelevant. | |
| # On push to main everything runs regardless (keeps the coverage baseline solid). | |
| changes: | |
| name: changes | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 5 | |
| outputs: | |
| backend: ${{ steps.filter.outputs.backend }} | |
| ui: ${{ steps.filter.outputs.ui }} | |
| mcp: ${{ steps.filter.outputs.mcp }} | |
| rees: ${{ steps.filter.outputs.rees }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Check whitespace | |
| run: git diff --check | |
| - name: Filter changed paths | |
| id: filter | |
| uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 | |
| with: | |
| filters: | | |
| backend: | |
| - 'src/**' | |
| - 'test/**' | |
| - 'vitest*.config.ts' | |
| - 'tsconfig*.json' | |
| - 'package.json' | |
| - 'package-lock.json' | |
| - 'scripts/**' | |
| - 'migrations/**' | |
| - '.github/workflows/**' | |
| ui: | |
| - 'apps/gittensory-ui/**' | |
| - 'apps/gittensory-extension/**' | |
| - 'src/**' | |
| - 'scripts/write-ui-openapi.ts' | |
| - 'scripts/build-extension.mjs' | |
| - 'package.json' | |
| - 'package-lock.json' | |
| mcp: | |
| # Self-contained package: build is `node --check` on its own files | |
| # and the pack check only inspects the tarball. Root src/ cannot | |
| # affect it, so it is intentionally NOT a trigger here. | |
| - 'packages/gittensory-mcp/**' | |
| - 'scripts/check-mcp-package.mjs' | |
| - 'package-lock.json' | |
| rees: | |
| - 'review-enrichment/**' | |
| - '.github/workflows/ci.yml' | |
| # Fast-failing checks first: workflow lint + typecheck. | |
| lint: | |
| name: lint | |
| needs: changes | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| - name: Install dependencies (retry on transient failures) | |
| run: | | |
| for attempt in 1 2 3; do | |
| if npm ci --prefer-offline --no-audit --no-fund; then | |
| exit 0 | |
| fi | |
| echo "::warning::npm ci failed (attempt ${attempt}/3); retrying in 10s" | |
| sleep 10 | |
| done | |
| echo "::error::npm ci failed after 3 attempts" | |
| exit 1 | |
| - name: Lint workflows | |
| run: npm run actionlint | |
| - name: Check migrations | |
| run: npm run db:migrations:check | |
| - name: Typecheck | |
| run: npm run typecheck | |
| # Unit/integration suite + coverage. This used to fan out into 3 GitHub matrix | |
| # jobs because hosted-runner minutes were the bottleneck. On the self-hosted | |
| # runner, the extra checkout/install/artifact fan-in surface has become more | |
| # expensive and fragile than the saved wall-clock, so keep coverage in one job | |
| # and let Vitest use the runner's local worker concurrency. | |
| test: | |
| name: test | |
| needs: changes | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| # Full history so Codecov can resolve the merge base for patch coverage. | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| - name: Install dependencies (retry on transient failures) | |
| run: | | |
| for attempt in 1 2 3; do | |
| if npm ci --prefer-offline --no-audit --no-fund; then | |
| exit 0 | |
| fi | |
| echo "::warning::npm ci failed (attempt ${attempt}/3); retrying in 10s" | |
| sleep 10 | |
| done | |
| echo "::error::npm ci failed after 3 attempts" | |
| exit 1 | |
| - name: Prepare test reports dir | |
| run: mkdir -p reports/junit | |
| - name: Test with coverage | |
| id: coverage | |
| env: | |
| VITEST_JUNIT_PATH: reports/junit/vitest.xml | |
| run: npm run test:coverage -- --maxWorkers=100% | |
| - name: Test failure guidance | |
| if: ${{ failure() && steps.coverage.conclusion == 'failure' }} | |
| run: | | |
| echo "::error title=Tests::The backend test coverage suite failed." | |
| echo "Coverage itself is gated by Codecov on changed lines (codecov/patch), computed from the complete lcov generated by this job." | |
| echo "Reproduce locally with: 'npm run test:coverage'." | |
| - name: Upload coverage to Codecov | |
| if: ${{ success() }} | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./coverage/lcov.info | |
| disable_search: true | |
| override_branch: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} | |
| override_commit: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| override_pr: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }} | |
| fail_ci_if_error: false | |
| - name: Upload Vitest results to Codecov | |
| if: ${{ !cancelled() }} | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./reports/junit/vitest.xml | |
| report_type: test_results | |
| disable_search: true | |
| override_branch: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} | |
| override_commit: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| override_pr: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }} | |
| fail_ci_if_error: false | |
| # Worker-pool runtime tests (separate vitest config); split out of `test` so it | |
| # runs in parallel instead of serially after the coverage run. | |
| workers: | |
| name: workers | |
| needs: changes | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| - name: Install dependencies (retry on transient failures) | |
| run: | | |
| for attempt in 1 2 3; do | |
| if npm ci --prefer-offline --no-audit --no-fund; then | |
| exit 0 | |
| fi | |
| echo "::warning::npm ci failed (attempt ${attempt}/3); retrying in 10s" | |
| sleep 10 | |
| done | |
| echo "::error::npm ci failed after 3 attempts" | |
| exit 1 | |
| - name: Worker runtime tests | |
| run: npm run test:workers | |
| # MCP package build + publishable-package smoke check. | |
| mcp: | |
| name: mcp | |
| needs: changes | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| - name: Install dependencies (retry on transient failures) | |
| run: | | |
| for attempt in 1 2 3; do | |
| if npm ci --prefer-offline --no-audit --no-fund; then | |
| exit 0 | |
| fi | |
| echo "::warning::npm ci failed (attempt ${attempt}/3); retrying in 10s" | |
| sleep 10 | |
| done | |
| echo "::error::npm ci failed after 3 attempts" | |
| exit 1 | |
| - name: Build MCP | |
| run: npm run build:mcp | |
| - name: MCP package check | |
| run: npm run test:mcp-pack | |
| # Standalone Railway review-enrichment service. | |
| rees: | |
| name: rees | |
| needs: changes | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.rees == 'true' }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| cache-dependency-path: review-enrichment/package-lock.json | |
| - name: REES build, source-map validation, and tests | |
| run: npm run rees:test | |
| # UI checks: each split into its own step for legible failures. | |
| ui: | |
| name: ui | |
| needs: changes | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 15 | |
| env: | |
| VITE_GITTENSORY_API_ORIGIN: https://gittensory-api.aethereal.dev | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| - name: Install dependencies (retry on transient failures) | |
| run: | | |
| for attempt in 1 2 3; do | |
| if npm ci --prefer-offline --no-audit --no-fund; then | |
| exit 0 | |
| fi | |
| echo "::warning::npm ci failed (attempt ${attempt}/3); retrying in 10s" | |
| sleep 10 | |
| done | |
| echo "::error::npm ci failed after 3 attempts" | |
| exit 1 | |
| - name: OpenAPI drift check | |
| run: npm run ui:openapi:check | |
| - name: UI/MCP version audit | |
| run: npm run ui:version-audit | |
| - name: UI lint | |
| run: npm run ui:lint | |
| - name: UI typecheck | |
| run: npm run ui:typecheck | |
| - name: UI tests | |
| run: npm run ui:test | |
| - name: UI build | |
| run: npm run ui:build | |
| # Diff-scoped security gate: fails only on vulnerabilities this PR introduces. | |
| # Ambient advisories in untouched deps are handled by Renovate + the scheduled | |
| # audit workflow, so one upstream CVE never blocks unrelated PRs. | |
| security: | |
| name: security | |
| if: ${{ github.event_name == 'pull_request' }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| - name: Dependency review | |
| uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 | |
| with: | |
| fail-on-severity: moderate | |
| comment-summary-in-pr: on-failure | |
| # Single required status check. Branch protection points at "validate"; this | |
| # aggregates the fan-out jobs so that requirement keeps working unchanged. | |
| # Path-filtered jobs report "skipped", which is treated as success. | |
| validate: | |
| name: validate | |
| needs: [changes, lint, test, workers, mcp, rees, ui, security] | |
| if: ${{ always() }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 2 | |
| steps: | |
| - name: All required jobs passed | |
| if: ${{ !(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} | |
| run: echo "All required CI jobs passed (path-filtered jobs reported skipped, which is OK)." | |
| - name: A required job failed | |
| if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} | |
| run: | | |
| echo "::error title=CI::A required CI job failed or was cancelled." | |
| echo "Job results: ${{ join(needs.*.result, ', ') }}" | |
| exit 1 |