Integration

Count anything from CI.

Deploys shipped, tests run, releases cut, days since the last incident — your pipelines already know these numbers. One workflow step publishes them to counters, and widgets or Boards put them on the wall.

Retry-safe by construction

CI retries jobs; naive counting double-counts or undercounts. The examples scope each Idempotency-Key to a workflow job attempt. Aggregation across repos and vendors is just “use the same counter key”. The reusable Action still lives in a private repository, so the examples use its public HTTP fallback until the Action is published.

1

Put your API key in a repo secret

Create a key under Dashboard → API keys, then add it in GitHub: Settings → Secrets and variables → Actions → COUNTERS_API_KEY. The key never appears in workflow files or logs.

2

Count a deploy

.github/workflows/deploy.yml (excerpt)
- name: Count the deploy
  continue-on-error: true
  env:
    COUNTERS_API_KEY: ${{ secrets.COUNTERS_API_KEY }}
    IDEMPOTENCY_KEY: gh:${{ github.repository }}:${{ github.run_id }}:${{ github.run_attempt }}:${{ github.job }}:deploys
  run: |
    [ -z "$COUNTERS_API_KEY" ] && exit 0
    curl -fsS --max-time 10 -X POST "https://api.counters.dev/v1/counters/deploys/add" \
      -H "Authorization: Bearer $COUNTERS_API_KEY" \
      -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "amount": "1" }'
3

Gather CI stats — amounts from earlier steps

Any number a step can compute becomes a counter delta: total tests, built artifacts, migrated rows. Arbitrary precision means lifetime totals never overflow. Member writes update current board values immediately; enable per-member series for the counter in the dashboard before relying on member-series charts or history.

count tests run
- name: Run tests
  id: tests
  run: |
    npx jest --json --outputFile=results.json || true
    echo "total=$(jq '.numTotalTests' results.json)" >> "$GITHUB_OUTPUT"
    jq -e '.numFailedTests == 0' results.json > /dev/null

- name: Count tests run
  if: always()
  continue-on-error: true
  env:
    COUNTERS_API_KEY: ${{ secrets.COUNTERS_API_KEY }}
    AMOUNT: ${{ steps.tests.outputs.total }}
    IDEMPOTENCY_KEY: gh:${{ github.repository }}:${{ github.run_id }}:${{ github.run_attempt }}:${{ github.job }}:tests
  run: |
    [ -z "$COUNTERS_API_KEY" ] && exit 0
    BODY=$(jq -cn --arg amount "$AMOUNT" '{amount:$amount}')
    curl -fsS --max-time 10 -X POST "https://api.counters.dev/v1/counters/ci.tests_run/add" \
      -H "Authorization: Bearer $COUNTERS_API_KEY" \
      -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
      -H "Content-Type: application/json" \
      -d "$BODY"
build seconds by app
- name: Count build seconds by app
  if: always()
  continue-on-error: true
  env:
    COUNTERS_API_KEY: ${{ secrets.COUNTERS_API_KEY }}
    AMOUNT: ${{ steps.metrics.outputs.elapsed }}
    IDEMPOTENCY_KEY: gh:${{ github.repository }}:${{ github.run_id }}:${{ github.run_attempt }}:${{ github.job }}:build-seconds:web
  run: |
    [ -z "$COUNTERS_API_KEY" ] && exit 0
    BODY=$(jq -cn --arg amount "$AMOUNT" '{amount:$amount}')
    curl -fsS --max-time 10 -X POST "https://api.counters.dev/v1/counters/ci.build_seconds/members/web/add" \
      -H "Authorization: Bearer $COUNTERS_API_KEY" \
      -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
      -H "Content-Type: application/json" \
      -d "$BODY"
4

The “days since incident” pattern

A daily cron adds 1; declaring an incident clears the counter. clear starts a new epoch instead of deleting history — the epoch number is your incident count.

two tiny workflows
# daily-tick.yml — cron adds 1 every midnight
- name: Tick the streak
  continue-on-error: true
  env:
    COUNTERS_API_KEY: ${{ secrets.COUNTERS_API_KEY }}
  run: |
    [ -z "$COUNTERS_API_KEY" ] && exit 0
    curl -fsS --max-time 10 -X POST "https://api.counters.dev/v1/counters/days.since_incident/add" \
      -H "Authorization: Bearer $COUNTERS_API_KEY" \
      -H "Idempotency-Key: tick:${{ github.run_id }}" \
      -H "Content-Type: application/json" \
      -d '{ "amount": "1" }'

# incident.yml (workflow_dispatch) — clear resets to 0; the epoch number
# becomes your all-time incident count, and history stays queryable
- name: Declare the incident
  continue-on-error: true
  env:
    COUNTERS_API_KEY: ${{ secrets.COUNTERS_API_KEY }}
  run: |
    [ -z "$COUNTERS_API_KEY" ] && exit 0
    curl -fsS --max-time 10 -X POST "https://api.counters.dev/v1/counters/days.since_incident/clear" \
      -H "Authorization: Bearer $COUNTERS_API_KEY" \
      -H "Idempotency-Key: incident:${{ github.run_id }}"
5

Not on GitHub?

It’s all one HTTP call — every CI can do this:

any CI
curl -fsS -X POST "https://api.counters.dev/v1/counters/deploys/add" \
  -H "Authorization: Bearer $COUNTERS_API_KEY" \
  -H "Idempotency-Key: $CI_PIPELINE_ID" \
  -H "Content-Type: application/json" \
  -d '{ "amount": "1" }'