repostats.app

CI/CD integration

Trigger a fresh analysis from your build pipeline, pull the numbers as JSON, and use them however you like — gate releases on a regression budget, post a PR comment, ping Slack, feed a dashboard, embed a fresh badge in the docs.

The endpoints you'll use

The JSON endpoints return 404 if a repo hasn't been analyzed yet — POST to /api/analyze first.

Pattern 1 · curl in any pipeline

Works in GitHub Actions, GitLab CI, CircleCI, Jenkins, Buildkite — anywhere with bash and curl.

# 1. Trigger (or pick up cached) analysis
curl -sS -X POST https://repostats.app/api/analyze \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://github.com/'"$GITHUB_REPOSITORY"'"}' > /dev/null

# 2. Poll up to 3 minutes for the result
for i in $(seq 1 60); do
  sleep 3
  body=$(curl -sS -w '%{http_code}' "https://repostats.app/api/repo/$GITHUB_REPOSITORY?view=totals")
  status="${body: -3}"; data="${body:0:${#body}-3}"
  [ "$status" = "200" ] && break
done

# 3. Read the metrics with jq
echo "$data" | jq -r '"cost=$\(.cocomo_cost_usd|round) loc=\(.code) files=\(.files)"'

Pattern 2 · GitHub Actions workflow

Paste this into .github/workflows/repostats.yml — runs on every push to main, plus every PR, and writes a job summary with the headline numbers.

name: repostats
on:
  push:    { branches: [main] }
  pull_request:

jobs:
  analyze:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger repostats analysis
        run: |
          curl -sS -X POST https://repostats.app/api/analyze \
            -H 'Content-Type: application/json' \
            -d "{\"url\":\"https://github.com/${{ github.repository }}\",\"force\":true}"
      - name: Wait for completion + read totals
        id: read
        run: |
          for i in $(seq 1 60); do
            sleep 3
            code=$(curl -sS -o totals.json -w '%{http_code}' \
              "https://repostats.app/api/repo/${{ github.repository }}?view=totals")
            [ "$code" = "200" ] && break
          done
          jq -r '"cost=$\(.cocomo_cost_usd|round)\nloc=\(.code)\nfiles=\(.files)"' totals.json >> "$GITHUB_OUTPUT"
      - name: Write job summary
        run: |
          {
            echo "### 📊 repostats"
            echo ""
            echo "| metric | value |"
            echo "|---|---|"
            echo "| cost to develop | \$${{ steps.read.outputs.cost }} |"
            echo "| lines of code   | ${{ steps.read.outputs.loc }} |"
            echo "| files           | ${{ steps.read.outputs.files }} |"
            echo ""
            echo "[Full report →](https://repostats.app/r/${{ github.repository }})"
          } >> "$GITHUB_STEP_SUMMARY"

Pattern 3 · fail the build on a regression budget

Compare the current numbers against a checked-in baseline. If LOC or cost jumped more than you allow, exit non-zero.

# prerequisite: .repostats-budget.json checked into the repo
#   { "max_code": 100000, "max_cost_usd": 5000000 }

curl -sS "https://repostats.app/api/repo/$GITHUB_REPOSITORY?view=totals" > totals.json

code=$(jq    '.code'             totals.json)
cost=$(jq -r '.cocomo_cost_usd | floor' totals.json)

max_code=$(jq '.max_code'     .repostats-budget.json)
max_cost=$(jq '.max_cost_usd' .repostats-budget.json)

if [ "$code" -gt "$max_code" ] || [ "$cost" -gt "$max_cost" ]; then
  echo "::error::repostats budget exceeded — code=$code (max $max_code), cost=$cost (max $max_cost)"
  exit 1
fi

Pattern 4 · PR comment with the cost delta

Same as Pattern 2 but also fetches the base-branch SHA's analysis (if it exists) and posts the delta as a sticky PR comment. Sketch:

- name: Comment delta on PR
  if: github.event_name == 'pull_request'
  uses: marocchino/sticky-pull-request-comment@v2
  with:
    header: repostats
    message: |
      **repostats** — cost +$${{ steps.delta.outputs.cost }} (was \$${{ steps.delta.outputs.cost_before }})
      lines +${{ steps.delta.outputs.loc }} (was ${{ steps.delta.outputs.loc_before }})
      [Full report →](https://repostats.app/r/${{ github.repository }})

Pattern 5 · live badge in your README

Already covered on the Embed page. TL;DR:

[![repostats](https://repostats.app/badge/owner/repo.svg)](https://repostats.app/r/owner/repo)

Auth, rate limits, fair use

Right now the public endpoint is unauthenticated and rate-limited per IP (one cold analysis every few seconds). Cached GETs aren't rate-limited. Private repos aren't supported in v1. If your CI hits a lot of repos in one batch, paginate the work or get in touch and we'll wire up an API key.

Response schemas

GET /api/repo/{owner}/{repo}?view=totals returns:

{
  "sha": "d60ec36cab33...",
  "default_branch": "master",
  "files": 88121,
  "lines": 42014443,
  "code":  32003044,
  "comment": 4740609,
  "blank":   5270790,
  "complexity": 2658913,
  "bytes": 1325419528,
  "languages": 51,
  "cocomo_effort_months":   65621.06,
  "cocomo_schedule_months": 218.77,
  "cocomo_people":          589.75,
  "cocomo_cost_usd":        1452420856.81,
  "clone_ms": 33113,
  "scc_ms":   7308
}