Skip to content

fix(github): reduce rate-limit retry storms #4847

fix(github): reduce rate-limit retry storms

fix(github): reduce rate-limit retry storms #4847

Workflow file for this run

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: Upload coverage to Codecov
if: ${{ success() && (github.event_name == 'push' || needs.changes.outputs.backend == '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 || '' }}
fail_ci_if_error: false
- name: Upload Vitest results to Codecov
if: ${{ !cancelled() && (github.event_name == 'push' || needs.changes.outputs.backend == '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: 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