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.
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.
Count a deploy
- 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" }'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.
- 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"- 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"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.
# 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 }}"Not on GitHub?
It’s all one HTTP call — every CI can do this:
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" }'