ADR-001: Actions Runs List — Switch Data Source from /actions/tasks to /actions/runs
| Field | Value |
|---|---|
| ID | ADR-001 |
| Status | PROPOSED |
| Deciders | Maintainers |
| Date | 2025-07 |
| Related Issue | #29 — Timestamp enrichment via event_store |
Context
Section titled “Context”The proxy exposes GET /api/v1/repos/{owner}/{repo}/actions/runs as an enriched endpoint over Forgejo.
Historically, Forgejo had no native /actions/runs endpoint, so the proxy synthesised run data by
querying the lower-level /actions/tasks endpoint and grouping/enriching the results.
Forgejo 14.x introduced a native GET /api/v1/repos/{owner}/{repo}/actions/runs endpoint that returns
first-class run data with exact timestamps.
This ADR evaluates whether the proxy should switch its internal data source from /actions/tasks to the
new /actions/runs endpoint.
Domain Model Clarification
Section titled “Domain Model Clarification”Understanding the difference between the two Forgejo primitives is essential before comparing the options.
ActionTask — /api/v1/repos/{owner}/{repo}/actions/tasks
Section titled “ActionTask — /api/v1/repos/{owner}/{repo}/actions/tasks”A task is a single job execution attempt within a workflow run.
- Each workflow run consists of one or more jobs.
- Each job can have one or more attempts (retries), each of which is a separate task.
- Tasks represent individual job executions with their own step-level results.
Key fields:
| Field | Description |
|---|---|
id | Task-level identifier |
run_id | Points to the parent run |
run_number | Per-repository sequential run counter |
name | Job name |
status | Job status (string enum) |
conclusion | Job conclusion |
started_at | When this job started |
completed_at | When this job completed |
This is a lower-level primitive — it maps to individual job/attempt combinations within a run.
Endpoint (via proxy):
https://forgejo-proxy.hochguertel.work/api/v1/repos/{owner}/{repo}/actions/tasks
→ forwarded to: https://pastoral-oyster.pikapod.net/api/v1/repos/{owner}/{repo}/actions/tasks
ActionRun — /api/v1/repos/{owner}/{repo}/actions/runs
Section titled “ActionRun — /api/v1/repos/{owner}/{repo}/actions/runs”A run is a complete workflow execution triggered by an event (push, PR, tag, etc.).
- Contains metadata about the entire workflow lifecycle.
- Holds
created,started,stopped, andupdatedtimestamps at the workflow level.
Key fields:
| Field | Description |
|---|---|
id | Global sequential run ID (across all repos) |
index_in_repo | Per-repository sequential counter — equivalent to run_number |
title | Workflow run title (usually the commit message subject) |
commit_sha | Full commit SHA |
trigger_user | User who triggered the run |
status | Integer enum (see table below) |
conclusion | Run conclusion |
created | Exact creation timestamp |
started | Exact start timestamp |
stopped | Exact stop timestamp (null if still running) |
updated | Exact last-update timestamp |
duration | Total duration in seconds |
workflow_id | Workflow filename (e.g. ci.yml) |
Native field names use non-_at suffixes (created, started, stopped) — not created_at.
Endpoint (via proxy — currently intercepted):
https://forgejo-proxy.hochguertel.work/api/v1/repos/{owner}/{repo}/actions/runs
→ native Forgejo: https://pastoral-oyster.pikapod.net/api/v1/repos/{owner}/{repo}/actions/runs
Summary of Difference
Section titled “Summary of Difference”Current State — Proxy Data Flow
Section titled “Current State — Proxy Data Flow”The proxy’s GET /api/v1/repos/{owner}/{repo}/actions/runs currently works as follows:
- Receives the client request at
https://forgejo-proxy.hochguertel.work/api/v1/repos/{owner}/{repo}/actions/runs - Calls Forgejo’s
/api/v1/repos/{owner}/{repo}/actions/tasksinternally - Groups tasks by
run_numberto reconstruct logical runs - Queries
event_store.db(SQLite) to enrichcreated_atandfinished_attimestamps — because tasks do not carry reliable run-level timestamps - Builds the
jobs[]array from the grouped task data - Returns a normalised response using snake_case
*_atfield names
flowchart TD Client[CLI / API Client] -->|GET /api/v1/repos/owner/repo/actions/runs| Proxy Proxy -->|GET /api/v1/repos/owner/repo/actions/tasks| Forgejo[Forgejo\nhttps://pastoral-oyster.pikapod.net] Proxy -->|SELECT first_seen_at, finished_at| EventStore[(SQLite\nevent_store.db)] Forgejo -->|tasks list| Proxy Proxy -->|GROUP BY run_number| Grouping[Task Grouping\nLogic] Grouping --> EventStore EventStore -->|approximate timestamps| Grouping Grouping -->|enriched response| ClientCurrent Proxy Response Model
Section titled “Current Proxy Response Model”For each run in the list response:
{ "run_number": 79, "display_title": "feat: add feature", "head_sha": "abc123", "head_branch": "main", "status": "success", "conclusion": "success", "created_at": "2025-06-01T10:00:00Z", "updated_at": "2025-06-01T10:05:00Z", "run_started_at": "2025-06-01T10:00:30Z", "finished_at": "2025-06-01T10:05:00Z", "workflow_id": "ci.yml", "jobs": [ { "name": "build", "status": "success" } ]}Forgejo Native API — What Was Discovered
Section titled “Forgejo Native API — What Was Discovered”Forgejo 14.x now exposes a native GET /api/v1/repos/{owner}/{repo}/actions/runs endpoint.
Native Response Model
Section titled “Native Response Model”{ "id": 839, "index_in_repo": 79, "title": "feat: add feature", "commit_sha": "abc123def456...", "prettyref": "refs/heads/main", "status": 3, "trigger_user": { "login": "alice", "id": 1 }, "created": "2025-06-01T10:00:00Z", "started": "2025-06-01T10:00:30Z", "stopped": "2025-06-01T10:05:00Z", "updated": "2025-06-01T10:05:00Z", "duration": 270, "jobs": null, "workflow_id": "ci.yml"}Status Integer Enum
Section titled “Status Integer Enum”| Value | Meaning |
|---|---|
1 | waiting |
2 | running |
3 | success |
4 | failure |
5 | cancelled |
6 | skipped |
Field Comparison: Native vs Proxy
Section titled “Field Comparison: Native vs Proxy”| Field | Native Forgejo | Proxy (current) | Notes |
|---|---|---|---|
| Run identifier | id (global) + index_in_repo | run_number | index_in_repo = run_number |
| Created timestamp | created ✅ exact | created_at ⚠️ approximate | Key improvement |
| Started timestamp | started ✅ exact | run_started_at (task-derived) | Key improvement |
| Stopped/finished | stopped ✅ exact | finished_at ⚠️ approximate | Key improvement |
| Updated timestamp | updated ✅ exact | updated_at ⚠️ approximate | Key improvement |
| Jobs breakdown | ❌ null in list | ✅ jobs[] | Proxy adds unique value |
| Workflow filter | ❌ not supported | ✅ ?workflow_id= | Proxy adds unique value |
| Status type | Integer enum | String enum | Proxy normalises |
| Field naming | created, started, stopped | created_at, run_started_at, finished_at | Proxy normalises |
Decision
Section titled “Decision”Switch the proxy list endpoint to use the native /actions/runs as its primary data source.
The proxy will:
- Call Forgejo’s native
https://pastoral-oyster.pikapod.net/api/v1/repos/{owner}/{repo}/actions/runs - Map native field names to the normalised snake_case
*_atformat consumers expect - Enrich each run with a
jobs[]array by querying/actions/tasksgrouped byrun_number - Apply
?workflow_id=and?status=filtering (translating string status to integer for the native call) - Remove the
event_storedependency for timestamp data
What the Proxy Continues to Add
Section titled “What the Proxy Continues to Add”| Feature | Proxy Contribution |
|---|---|
jobs[] in list response | Forgejo native list does not include jobs |
?workflow_id= filter | Not natively supported |
?status= filter (string) | Native uses integer enum; proxy translates |
| Normalised field names | created_at, run_started_at, finished_at instead of created/stopped |
| Backwards-compatible API surface | Consumers need no changes |
Proposed Data Flow
Section titled “Proposed Data Flow”flowchart TD Client[CLI / API Client] -->|GET /api/v1/repos/owner/repo/actions/runs| Proxy Proxy -->|GET /api/v1/repos/owner/repo/actions/runs| ForgejoRuns[Forgejo Runs API\nhttps://pastoral-oyster.pikapod.net] Proxy -->|GET /api/v1/repos/owner/repo/actions/tasks| ForgejoTasks[Forgejo Tasks API\nhttps://pastoral-oyster.pikapod.net] ForgejoRuns -->|exact timestamps: created, started, stopped, updated| Proxy ForgejoTasks -->|jobs list, grouped by run_number| Proxy Proxy -->|enriched with jobs + normalised fields| ClientImpact on event_store.db
Section titled “Impact on event_store.db”Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Accurate timestamps —
created,started,stopped,updatedare exact Forgejo values, not proxy-observed approximations. - Simpler code path — No more task grouping logic or
event_storetimestamp lookups. - Better correctness — Historical runs fetched fresh will have accurate timing data.
- Reduced state —
event_store.dbno longer needed for runs list timestamps.
Negative / Trade-offs
Section titled “Negative / Trade-offs”- Two upstream calls instead of one — the proxy must call both
/actions/runsand/actions/tasksto build the enriched response (unlessjobs[]is made optional/lazy). - Field mapping required — The proxy must translate Forgejo’s integer status enum and non-
_atfield names. - Potential for partial failure — If
/actions/tasksis unavailable,jobs[]cannot be populated.
Neutral
Section titled “Neutral”- The external API surface (what consumers call) remains unchanged.
run_numbermaps directly toindex_in_repo— no consumer-visible identifier change.
Alternatives Considered
Section titled “Alternatives Considered”Option A: Keep current approach (tasks + event_store)
Section titled “Option A: Keep current approach (tasks + event_store)”Rejected. Timestamps remain approximate. The event_store adds operational complexity (SQLite file,
persistence, state drift). Now that a native runs endpoint exists, the approximation is unnecessary.
Option B: Pass through native runs endpoint directly (no enrichment)
Section titled “Option B: Pass through native runs endpoint directly (no enrichment)”Rejected. Removes the jobs[] enrichment that consumers depend on, and changes the field names
and status type in a breaking way.
Option C: Native runs as primary, tasks for jobs only (this ADR)
Section titled “Option C: Native runs as primary, tasks for jobs only (this ADR)”Accepted. Best balance of accuracy, simplicity, and backwards compatibility.
Related Issues
Section titled “Related Issues”- Issue #29 — Timestamp enrichment via event_store — the problem this decision partially resolves
- Issue tracker: https://pastoral-oyster.pikapod.net/tools/th-forgejo-cli/issues