Skip to content

ADR-001: Actions Runs List — Switch Data Source from /actions/tasks to /actions/runs

FieldValue
IDADR-001
StatusPROPOSED
DecidersMaintainers
Date2025-07
Related Issue#29 — Timestamp enrichment via event_store

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.


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:

FieldDescription
idTask-level identifier
run_idPoints to the parent run
run_numberPer-repository sequential run counter
nameJob name
statusJob status (string enum)
conclusionJob conclusion
started_atWhen this job started
completed_atWhen 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, and updated timestamps at the workflow level.

Key fields:

FieldDescription
idGlobal sequential run ID (across all repos)
index_in_repoPer-repository sequential counter — equivalent to run_number
titleWorkflow run title (usually the commit message subject)
commit_shaFull commit SHA
trigger_userUser who triggered the run
statusInteger enum (see table below)
conclusionRun conclusion
createdExact creation timestamp
startedExact start timestamp
stoppedExact stop timestamp (null if still running)
updatedExact last-update timestamp
durationTotal duration in seconds
workflow_idWorkflow 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



The proxy’s GET /api/v1/repos/{owner}/{repo}/actions/runs currently works as follows:

  1. Receives the client request at https://forgejo-proxy.hochguertel.work/api/v1/repos/{owner}/{repo}/actions/runs
  2. Calls Forgejo’s /api/v1/repos/{owner}/{repo}/actions/tasks internally
  3. Groups tasks by run_number to reconstruct logical runs
  4. Queries event_store.db (SQLite) to enrich created_at and finished_at timestamps — because tasks do not carry reliable run-level timestamps
  5. Builds the jobs[] array from the grouped task data
  6. Returns a normalised response using snake_case *_at field 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| Client

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.

{
"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"
}
ValueMeaning
1waiting
2running
3success
4failure
5cancelled
6skipped
FieldNative ForgejoProxy (current)Notes
Run identifierid (global) + index_in_reporun_numberindex_in_repo = run_number
Created timestampcreated ✅ exactcreated_at ⚠️ approximateKey improvement
Started timestampstarted ✅ exactrun_started_at (task-derived)Key improvement
Stopped/finishedstopped ✅ exactfinished_at ⚠️ approximateKey improvement
Updated timestampupdated ✅ exactupdated_at ⚠️ approximateKey improvement
Jobs breakdownnull in listjobs[]Proxy adds unique value
Workflow filter❌ not supported?workflow_id=Proxy adds unique value
Status typeInteger enumString enumProxy normalises
Field namingcreated, started, stoppedcreated_at, run_started_at, finished_atProxy normalises

Switch the proxy list endpoint to use the native /actions/runs as its primary data source.

The proxy will:

  1. Call Forgejo’s native https://pastoral-oyster.pikapod.net/api/v1/repos/{owner}/{repo}/actions/runs
  2. Map native field names to the normalised snake_case *_at format consumers expect
  3. Enrich each run with a jobs[] array by querying /actions/tasks grouped by run_number
  4. Apply ?workflow_id= and ?status= filtering (translating string status to integer for the native call)
  5. Remove the event_store dependency for timestamp data
FeatureProxy Contribution
jobs[] in list responseForgejo native list does not include jobs
?workflow_id= filterNot natively supported
?status= filter (string)Native uses integer enum; proxy translates
Normalised field namescreated_at, run_started_at, finished_at instead of created/stopped
Backwards-compatible API surfaceConsumers need no changes

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| Client

  • Accurate timestampscreated, started, stopped, updated are exact Forgejo values, not proxy-observed approximations.
  • Simpler code path — No more task grouping logic or event_store timestamp lookups.
  • Better correctness — Historical runs fetched fresh will have accurate timing data.
  • Reduced stateevent_store.db no longer needed for runs list timestamps.
  • Two upstream calls instead of one — the proxy must call both /actions/runs and /actions/tasks to build the enriched response (unless jobs[] is made optional/lazy).
  • Field mapping required — The proxy must translate Forgejo’s integer status enum and non-_at field names.
  • Potential for partial failure — If /actions/tasks is unavailable, jobs[] cannot be populated.
  • The external API surface (what consumers call) remains unchanged.
  • run_number maps directly to index_in_repo — no consumer-visible identifier change.

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.