fix(analytics): redact Slack tokens in weekly value report rollups #5360
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' | |
| # Path-aware validation. Keep this as one self-hosted job so a PR uses one | |
| # runner slot and one dependency install instead of fanning out into several | |
| # competing installs on the same VPS. Fork PRs still run on GitHub-hosted | |
| # runners because their code is untrusted. | |
| validate-code: | |
| name: validate-code | |
| needs: changes | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.rees == 'true' || 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: 45 | |
| env: | |
| VITE_GITTENSORY_API_ORIGIN: https://gittensory-api.aethereal.dev | |
| 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: Lint workflows | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: npm run actionlint | |
| - name: Check migrations | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: npm run db:migrations:check | |
| - name: Typecheck | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: npm run typecheck | |
| - name: Prepare test reports dir | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: mkdir -p reports/junit | |
| - name: Test with coverage | |
| id: coverage | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| 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: Verify coverage report exists | |
| if: ${{ success() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') }} | |
| run: | | |
| if [ ! -s coverage/lcov.info ]; then | |
| echo "::error title=Coverage::coverage/lcov.info is missing or empty" | |
| exit 1 | |
| fi | |
| # Direct upload for trusted contexts (push + same-repo PRs), where secrets.CODECOV_TOKEN is | |
| # available. | |
| - name: Upload coverage to Codecov | |
| if: ${{ success() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') && github.event.pull_request.head.repo.fork != true }} | |
| 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 || '' }} | |
| # Coverage upload is part of the hard gate: upload or service errors | |
| # should fail CI instead of allowing a PR to merge without patch data. | |
| fail_ci_if_error: true | |
| # Fork PRs run without secrets, so they cannot use the token path above. codecov-action v4+ supports | |
| # tokenless upload for public repos, but its commit auto-detection cannot be trusted here: for | |
| # pull_request events GITHUB_SHA is the ephemeral auto-merge commit, and codecov-cli's fallback to | |
| # recover the real head sha assumes HEAD is that 2-parent merge commit -- our checkout step above | |
| # deliberately checks out github.event.pull_request.head.sha directly (so tests run the contributor's | |
| # actual commit, not a synthetic merge), so HEAD has one parent and that recovery can't fire. Pass | |
| # the same explicit overrides as the trusted upload above so the report attaches to the real PR head, | |
| # not a merge sha GitHub's PR checks list has no reason to display. | |
| # | |
| # override_branch is prefixed with the fork owner (owner:branch) -- per Codecov's own docs, only a | |
| # branch string containing a colon is recognized as "unprotected" and eligible for a tokenless | |
| # upload; a bare branch name looks like it could be a real (possibly protected) branch on the base | |
| # repo and gets rejected with "Token required because branch is protected" even with no token | |
| # configured at all. codecov-cli's own auto-detection never adds this prefix either (verified in its | |
| # source), so this must be supplied explicitly. | |
| - name: Upload coverage to Codecov (fork PR tokenless) | |
| if: ${{ success() && needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.fork == true }} | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| files: ./coverage/lcov.info | |
| disable_search: true | |
| override_branch: ${{ github.event.pull_request.head.repo.owner.login }}:${{ github.event.pull_request.head.ref }} | |
| override_commit: ${{ github.event.pull_request.head.sha }} | |
| override_pr: ${{ github.event.pull_request.number }} | |
| fail_ci_if_error: true | |
| # Test results are useful Codecov annotations, not the coverage gate. Keep | |
| # their upload non-blocking so a JUnit ingestion hiccup does not fail CI | |
| # after the tests and hard coverage upload have already passed. | |
| - name: Upload Vitest results to Codecov | |
| if: ${{ !cancelled() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') && github.event.pull_request.head.repo.fork != true }} | |
| 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 | |
| - name: Upload Vitest results to Codecov (fork PR tokenless) | |
| if: ${{ !cancelled() && needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.fork == true }} | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| files: ./reports/junit/vitest.xml | |
| report_type: test_results | |
| disable_search: true | |
| override_branch: ${{ github.event.pull_request.head.repo.owner.login }}:${{ github.event.pull_request.head.ref }} | |
| override_commit: ${{ github.event.pull_request.head.sha }} | |
| override_pr: ${{ github.event.pull_request.number }} | |
| fail_ci_if_error: false | |
| - name: Worker runtime tests | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: npm run test:workers | |
| - name: Build MCP | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' }} | |
| run: npm run build:mcp | |
| - name: MCP package check | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' }} | |
| run: npm run test:mcp-pack | |
| - name: REES build, source-map validation, and tests | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.rees == 'true' }} | |
| run: npm run rees:test | |
| - name: OpenAPI drift check | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| run: npm run ui:openapi:check | |
| - name: UI/MCP version audit | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| run: npm run ui:version-audit | |
| - name: UI lint | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| run: npm run ui:lint | |
| - name: UI typecheck | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| run: npm run ui:typecheck | |
| - name: UI tests | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| run: npm run ui:test | |
| - name: UI build | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| 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 path-aware job and the PR-only dependency review gate so that | |
| # requirement keeps working unchanged. | |
| # Path-filtered jobs report "skipped", which is treated as success. | |
| validate: | |
| name: validate | |
| needs: [changes, validate-code, 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 |