Capture CI/CD metrics in PostHog - track workflow duration, success/failure rates, and analyze performance trends with PostHog's full analytics toolkit.
jobs:
tests:
# ... your test job
ci-metrics:
needs: [tests]
if: always()
runs-on: ubuntu-latest
steps:
- uses: PostHog/posthog-github-action@v1
with:
posthog-token: ${{ secrets.POSTHOG_API_KEY }}
event: 'ci-metrics'
capture-run-duration: true
github-token: ${{ secrets.GITHUB_TOKEN }}
status-job: 'tests'This captures run duration, pass/fail status, and GitHub context (repo, branch, commit, actor).
- Trends & insights - duration over time, failure rates by branch/author
- Dashboards - build CI health dashboards alongside product metrics
- Alerts - get notified when builds slow down or failure rates spike
- Correlation - connect CI data with your product analytics
Required Your write-only PostHog API Token (Project API Key).
Your PostHog API Host.
Defaults to https://us.i.posthog.com
The event name to send to PostHog (e.g., "ci-metrics"). Optional — omit it when you only want to create an annotation. At least one of event or annotation must be provided.
Optional properties to add to the event. These should be passed as a string containing a JSON object.
The action runs JSON.parse(properties) on the input.
Set to true to capture run duration via GitHub API. Adds duration_seconds and run metadata.
Set to true to capture timing and status for each job in the workflow. Emits one additional event per completed job (named {event}-job, e.g., ci-metrics-job). All events share the same workflow_run group for correlation in PostHog.
GitHub token for API access. Required when capture-run-duration, capture-job-durations, or status-job is set.
Optional runner label to include in properties (e.g., 'depot').
Job name to check for workflow status. Captures that job's conclusion (success, failure, cancelled) as conclusion.
Note: Your metrics job must needs the target job and use if: always() to run even on failure.
Create a PostHog annotation with this text (e.g., "Deployed to production"). Can be used standalone or alongside event capture.
API key for the annotations REST API — a personal API key with the annotation:write scope. Defaults to posthog-token.
Event ingestion uses a project API key, while the annotations REST API needs a personal key, so the two auth schemes differ. For annotation-only usage you can just set posthog-token to the personal key. When you capture an event and create an annotation in the same step, set posthog-token to the project key and annotation-token to the personal key.
Annotation scope: 'project' (default) or 'organization'.
annotation-hidden
Set to 'true' to hide the annotation from the PostHog UI (charts and the annotations list) while keeping it readable via the API and MCP. Use for high-frequency markers like deployments that would otherwise crowd the UI. Defaults to 'false'.
Host for the PostHog app API used to create annotations — https://us.posthog.com (default), https://eu.posthog.com, or your own host for a self-hosted instance. This is distinct from posthog-api-host, which is the event ingestion host (*.i.posthog.com); the annotations endpoint lives on the app host. The host is not restricted to PostHog Cloud, so self-hosted deployments work.
Project to create the annotation in. Defaults to '@current' (the project the token belongs to). Set an explicit numeric id for deterministic targeting.
Set to 'true' to skip creating the annotation when a matching one already exists, making re-runs idempotent (e.g. a GitHub "Re-run jobs" won't duplicate a deploy marker). The match is by annotation-dedupe-key, or the full annotation text if no key is set. Defaults to 'false'.
Content prefix used to detect an existing annotation when annotation-dedupe is 'true'. Defaults to the full annotation text. Use a stable prefix (e.g. "Deployed acme/app@abc1234 to prod") so a volatile suffix — like a list of PR numbers — doesn't defeat deduplication.
The following GitHub context properties are automatically added to every event:
sha- The commit SHAref- The branch or tag refworkflow- The workflow namerunNumber- The run numberrunId- The run IDrepository- The repository namerepositoryOwner- The repository owneractor- The user who triggered the workflowactor_type-"bot"ifactorends in[bot]or matches a known bot account (currentlyCopilot), otherwise"human". Useful for slicing CI metrics by bot vs. human-driven runs.eventName- The event that triggered the workflow
When capture-run-duration is enabled:
duration_seconds- Time elapsed since run startedurl- URL to the runattempt- The attempt numberstarted_at- ISO 8601 timestamp
When status-job is set:
conclusion- Referenced job's conclusion (success,failure,cancelled)
When capture-job-durations is enabled, each per-job event ({event}-job) includes:
name- The job's display nameduration_seconds- Time from job start to completionconclusion- Job result (success,failure,cancelled,skipped)started_at- ISO 8601 timestampcompleted_at- ISO 8601 timestamprunner- The runner that executed the job
All events are tagged with a workflow_run group (owner/repo/run_id) for correlation in PostHog.
When capture-job-durations is enabled, group properties are also set via groupIdentify:
workflow_run: PostHog/posthog/12345
├── group properties: { conclusion: 'success' } ← only with capture-job-durations
│
├── event: ci-metrics ← run-level metrics
│ └── { duration_seconds: 1080, conclusion: 'success', ... }
│
├── event: ci-metrics-job ← per-job metrics
│ └── { name: 'Build', duration_seconds: 120, conclusion: 'success', ... }
│
└── event: ci-metrics-job
└── { name: 'Test', duration_seconds: 300, conclusion: 'success', ... }
This enables:
- Filter job events by workflow outcome - use group property
workflow_run.conclusion - See all jobs in a run - filter by
workflow_run = owner/repo/run_id - Correlate across events - breakdown by group in insights
- uses: PostHog/posthog-github-action@v1
with:
posthog-token: ${{ secrets.POSTHOG_API_KEY }}
event: "deploy-completed"
properties: '{"environment": "production"}'- uses: PostHog/posthog-github-action@v1
with:
posthog-token: ${{ secrets.POSTHOG_API_KEY }}
event: "ci-metrics"
capture-run-duration: true
github-token: ${{ secrets.GITHUB_TOKEN }}- uses: PostHog/posthog-github-action@v1
with:
posthog-token: ${{ secrets.POSTHOG_API_KEY }}
event: "ci-metrics"
capture-run-duration: true
capture-job-durations: true
github-token: ${{ secrets.GITHUB_TOKEN }}
status-job: 'Tests'- uses: PostHog/posthog-github-action@v1
with:
posthog-token: ${{ secrets.POSTHOG_API_KEY }}
posthog-api-host: "https://eu.i.posthog.com"
event: "ci-metrics"
capture-run-duration: true
github-token: ${{ secrets.GITHUB_TOKEN }}- uses: PostHog/posthog-github-action@v1
with:
posthog-token: ${{ secrets.POSTHOG_ANNOTATION_API_KEY }}
annotation: "Deployed ${{ github.repository }}@${{ github.sha }} to production"
annotation-scope: organizationHidden, idempotent deploy marker (readable via API/MCP, hidden from the UI)
- uses: PostHog/posthog-github-action@v1
with:
posthog-token: ${{ secrets.POSTHOG_ANNOTATION_API_KEY }}
annotation: "Deployed ${{ github.repository }}@${{ github.sha }} to prod-us"
annotation-hidden: true
annotation-project-id: "2"
# Re-running the workflow won't create a duplicate marker for this repo@sha+env
annotation-dedupe: true
annotation-dedupe-key: "Deployed ${{ github.repository }}@${{ github.sha }} to prod-us"name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
ci-metrics:
needs: [test]
if: always()
runs-on: ubuntu-latest
steps:
- uses: PostHog/posthog-github-action@v1
with:
posthog-token: ${{ secrets.POSTHOG_API_KEY }}
event: "ci-metrics"
capture-run-duration: true
capture-job-durations: true
github-token: ${{ secrets.GITHUB_TOKEN }}
status-job: 'test'- Node.js 20+
- act for local testing (requires Docker)
# Install dependencies
npm install
# Copy secrets template and add your PostHog API token
cp .secrets.example .secretsThis action uses @vercel/ncc to bundle all dependencies into a single file. You must rebuild before committing changes:
npm run buildThis compiles index.js and all dependencies into dist/index.js.
Test the action locally using act:
# Dry run (validates workflow without running)
npm run test:dry-run
# Full test (sends real event to PostHog)
npm testNote: act requires Docker to be running.
Always rebuild before releasing to ensure the dist folder is up to date:
npm run build# Update version in package.json manually
git add package.json dist/
git commit -m "1.x.x"
# Create PR and merge to maingit checkout main && git pull
git tag v1.x.x
git push origin v1.x.xThe release workflow creates the GitHub Release and updates the v1 tag automatically.
MIT