DORA dashboard
The DORA dashboard surfaces the four metrics from the DevOps Research and Assessment (DORA) State of DevOps report, computed live per-tenant from a dora_deployments table. Visible at Org admin → DORA.
No external service. No data sent to a third-party SaaS dashboard. The numbers are computed on the same Postgres instance that holds your repos, audit log, and findings — and they share the same EU residency and tenant-isolation guarantees as everything else under /<slug>/_admin/*.
The four metrics
| Metric | What it measures | How fremforge computes it |
|---|---|---|
| Deployment frequency | How often you ship to production | Count of dora_deployments rows where environment = 'production' AND success = true, divided by the rolling window. Reported as deployments/day, then bucketed into the DORA categories (Elite: ≥1/day, High: 1/day–1/week, Medium: 1/week–1/month, Low: <1/month). |
| Lead time for changes | Time from code-commit to deploy | deployed_at − committed_at, p50 + p95 over the window. Only counts deployments that supplied a commit_sha + committed_at. Reported in hours/days. |
| Change-failure rate | Share of deployments that fail / require a fix | count(success=false OR is_rework=true) / count(*) over the window. Expressed as a percentage. |
| Mean time to restore (MTTR) | Time to recover from a failed deployment | For each success=false event, the wall-clock from deployed_at to the next successful deployment to the same (repo, environment). Reported as median. |
Window defaults to 30 days rolling. Configurable at Org admin → DORA → Settings between 7d / 30d / 90d.
How deployments get into the table
Two ingest paths run side-by-side. Use whichever matches your release shape; many tenants use both.
1. Forgejo release webhook (automatic)
When a repo creates a Forgejo release (annotated tag v* or any release object published through the UI / POST /api/v1/repos/{owner}/{repo}/releases), fremforge auto-ingests one dora_deployments row with:
environment=production(default)deployed_at= releasecreated_atcommit_sha= releasetarget_commitishresolved to a SHAcommitted_at= the commit’sauthor.datesuccess=trueexternal_ref=release:{release_id}(idempotent — replay the webhook safely)
No customer wiring required. The webhook target is configured at the platform level; per-org enablement is automatic.
2. POST /api/v1/dora/deployments (custom — recommended for richer telemetry)
For tenants that don’t tag releases (continuous deployment, GitOps push-on-merge, blue/green flips) the auto-ingest misses every deploy. POST one record per deployment from your CI workflow:
curl -X POST https://frem.sh/api/v1/dora/deployments \
-H "Authorization: Bearer ffp_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"repo_full_name": "acme/web",
"environment": "production",
"deployed_at": "2026-05-14T10:00:00Z",
"commit_sha": "abc1234...",
"committed_at": "2026-05-14T09:30:00Z",
"success": true,
"external_ref": "deploy-12345"
}'Required: repo_full_name, environment, deployed_at.
Optional but recommended:
commit_sha+committed_at— without these, the row drops out of the lead-time calculation entirely. The other three metrics still count it.success(defaultstrue) — setfalseon rollback / failed deploy. Drives change-failure rate + MTTR.external_ref— dedupe key, tenant-scoped. The endpoint is idempotent on(tenant_id, repo_full_name, environment, external_ref): replay the call from a retried CI job, get a 200 with the existing row’s id rather than a duplicate insert. Omit if you don’t want dedupe.is_rework(defaultsfalse) — flag this as a hotfix / revert / follow-up. Counts toward change-failure rate even whensuccess=true. Drives the 5th metric (Rework Rate) per the 2025 DORA report.
Required scope on the token: findings:write — same scope CI workflows use for dep-scan + SBOM ingest. Mint at Org admin → API tokens (or use token exchange from your customer IdP for a short-lived token).
Response shape (201 on first insert, 200 on dedupe-hit):
{
"id": "0191e3b6-0000-7000-0000-000000000001",
"tenant_id": "0191e3b6-0000-7000-0000-000000000000",
"repo_full_name": "acme/web",
"environment": "production",
"deployed_at": "2026-05-14T10:00:00Z",
"deduped": false
}Example workflow integrations
GitHub-style Actions workflow on fremforge
# .forgejo/workflows/dora-ingest.yaml
name: Report deploy to DORA
on:
workflow_run:
workflows: [Deploy to production]
types: [completed]
jobs:
report:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: fremforge
steps:
- uses: actions/checkout@v4
- name: POST dora deployment
run: |
curl -X POST https://frem.sh/api/v1/dora/deployments \
-H "Authorization: Bearer ${{ secrets.FREMFORGE_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"repo_full_name": "${{ github.repository }}",
"environment": "production",
"deployed_at": "'"$(date -u +%FT%TZ)"'",
"commit_sha": "${{ github.sha }}",
"committed_at": "'"$(git show -s --format=%cI HEAD)"'",
"success": true,
"external_ref": "actions-run:${{ github.run_id }}"
}'Argo CD post-sync hook
# argocd-postsync-dora.yaml
apiVersion: batch/v1
kind: Job
metadata:
generateName: argocd-dora-
annotations:
argocd.argoproj.io/hook: PostSync
spec:
template:
spec:
restartPolicy: Never
containers:
- name: dora-report
image: curlimages/curl:8.5.0
envFrom:
- secretRef: { name: fremforge-dora-token }
args:
- sh
- -c
- |
curl -sS -X POST https://frem.sh/api/v1/dora/deployments \
-H "Authorization: Bearer $FREMFORGE_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"repo_full_name\": \"$REPO\",
\"environment\": \"$ENV\",
\"deployed_at\": \"$(date -u +%FT%TZ)\",
\"commit_sha\": \"$ARGOCD_APP_REVISION\",
\"success\": true,
\"external_ref\": \"argocd:$ARGOCD_APP_NAME:$ARGOCD_APP_REVISION\"
}"Marking a deploy as a rollback
curl -X POST https://frem.sh/api/v1/dora/deployments \
-H "Authorization: Bearer ffp_..." \
-H "Content-Type: application/json" \
-d '{
"repo_full_name": "acme/web",
"environment": "production",
"deployed_at": "2026-05-14T10:30:00Z",
"success": false,
"is_rework": true,
"external_ref": "rollback-from-deploy-12345"
}'This row drops change-failure rate AND starts the MTTR clock. The next success=true deploy to (acme/web, production) closes it.
Reading the dashboard
The admin page shows the latest computed values for the current window plus a 30-day sparkline per metric. Hover any data point for the underlying deploy list. Click a metric to drill into the per-repo breakdown.
The DORA categories (Elite / High / Medium / Low) for each metric are computed live against the Accelerate thresholds — the same bands the Google Cloud DORA report uses.
What we deliberately don’t do
- No team-level aggregation in v1. DORA’s “Team” axis requires per-deploy team-attribution, which isn’t reliably derivable from
repo_full_namealone in monorepo + multi-tenant deploys. Available via the per-repo breakdown today; first-class team aggregation is on the roadmap. - No outside-the-tenant comparison. Industry benchmarks aren’t shown — comparison would require sending your numbers to a third-party aggregator, which conflicts with the EU-residency contract. Use the public DORA report for “where do we stand vs. the industry” framing.
- No automatic “incident” linkage. MTTR is computed from your
success=false→ nextsuccess=truepair. We don’t try to infer incident windows from PagerDuty, Statuspage, or Forgejo issue activity — the customer’s deploy success/failure signal is the authoritative input.
Related
- Audit log — every DORA ingest emits an audit event (
api.dora.deployments.ingested) so an operator can verify the lineage of any displayed number. - Agent-native authentication — token-exchange a customer-IdP JWT for a short-lived
findings:writetoken if you don’t want to maintain a long-lived PAT. - API tokens — mint long-lived
findings:writePATs for CI workflows at User settings → API tokens; see Authentication policy for org-wide max-lifetime + rotation policy.