gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Issues

CI/CD: implement Forgejo/Gitea runner protocol (runner.v1) so stock runners work against git-shark #2

Summary

As an admin, I can set up CI/CD runners in git-shark: register runner instances, and have workflows in repositories executed on push, with live logs and results visible in the UI.

This is an epic. Strategy decision up front: do not write our own runner. Reuse the existing Forgejo Runner / Gitea act_runner by implementing their server-side protocol in git-shark. Stock runner binaries then work against git-shark unchanged.

Why the Forgejo/Gitea runner path (research summary)

  • Wire protocol is small and open. Runners talk Connect RPC (buf) over plain HTTP — no extra port, reuses the normal HTTP listener. One service runner.v1.RunnerService with 5 methods (Register, Declare, FetchTask, UpdateTask, UpdateLog) plus a PingService health check. Proto definitions are MIT-licensed (gitea.com/gitea/actions-proto-def) — we can generate Java stubs directly from the .proto files. Forgejo confirmed it still shares Gitea's proto contract (forgejo/runner#525), so both forgejo-runner and act_runner are compatible clients.
  • Workflows are GitHub-Actions-compatible YAML in .forgejo/workflows/ / .gitea/workflows/ — familiar format, existing ecosystem of actions.
  • GitLab Runner (fallback) is strictly worse for us: its server side must fully compile .gitlab-ci.yml (including include, extends, rules, YAML anchors, !reference, DAG, matrix) into pre-resolved job rows, and several endpoints (trace PATCH, status PUT) are only documented in the runner's source, not as a stable public contract.
  • Writing our own containerized runner: worst case only; not needed given the above.
  • The real cost is server-side, not the protocol: the runner intentionally receives a pre-resolved single-job payload and does no trigger/DAG/matrix work itself. Gitea's server does this with the Go act library; there is no Java port, so git-shark must implement workflow parsing, trigger evaluation, needs resolution and matrix expansion itself. This is the main implementation gap and the reason for phasing.

Architecture (target state)

push → detect .forgejo/workflows/*.yml at new head
     → evaluate `on:` triggers → create run + job rows (DAG, matrix-expanded)
     → runner long-polls FetchTask → gets Task { expanded single-job YAML,
       github context, secrets, vars, needs-outputs }
     → runner executes steps locally (its bundled act engine)
     → UpdateLog streams log rows (offset + ack_index resume protocol)
     → UpdateTask reports step/task state → run UI updates

Key protocol facts to honor:

  • FetchTask carries tasks_version so idle polling is cheap (no DB scan when nothing changed).
  • UpdateLog: runner sends rows from index, server acks durable offset (ack_index); reconnect-tolerant.
  • UpdateTask: step states carry log_index/log_length pointers into the log stream — server maps steps to log ranges via these, no log parsing.
  • Secrets/variables are delivered inline in FetchTaskResponse (plaintext over TLS — same trust model as GitHub self-hosted runners).

Phases

Phase 1 — MVP: runner registration + basic run loop

  • Connect RPC endpoint in Quarkus serving runner.v1 (generate Java stubs from the MIT-licensed protos; Connect protocol with binary protobuf codec over HTTP).
  • New tables: runner (uuid, token hash, name, labels, status, last_seen), action_run, action_task (job), action_log (or log blob storage), plus registration tokens.
  • Admin UI: generate registration token, list runners with status (idle/active/offline from Declare/FetchTask heartbeats), delete runner. Instance-scope only for v1 (repo/org scopes later).
  • Trigger: on: push only. Single-job workflows, no needs, no matrix. Parse workflow YAML from .forgejo/workflows/ at the pushed commit.
  • Run UI per repository: run list, run detail with per-step status + logs.
  • Task state machine incl. timeout/zombie handling (runner disappears mid-task → fail after deadline).

Phase 2 — real workflow semantics

  • Trigger evaluation: branch/tag/path filters, more events (tag push, merge-request events mapped to pull_request).
  • needs DAG resolution + passing TaskNeed outputs; matrix expansion (one Task per cell).
  • Secrets & variables: repo-level CRUD UI (owner-only), delivered via Task.Secrets/Task.Vars.
  • Concurrency/cancellation: cancel run from UI, re-run, cancel superseded runs on force-push.
  • Label-based task-to-runner matching.

Phase 3 — ecosystem compatibility

  • Artifacts: implement the GitHub Actions "Results" artifact service (Twirp, github.actions.results.api.v1.ArtifactServiceCreateArtifact/FinalizeArtifact/ListArtifacts/…) so actions/upload-artifact@v4 works; expose via ACTIONS_RESULTS_URL. Artifact download from run UI.
  • Cache: none server-side initially — act_runner ships its own cache server (ACTIONS_CACHE_URL, runner-local or shared external_server); document that in admin docs. (Gitea's server-side cache v2 is itself still an open proposal, gitea#33393.)
  • Repo/org-scoped runners; ephemeral runners (--ephemeral, server must enforce single-job-then-delete).
  • Commit/MR status integration (run result shown on commits and merge requests).

Out of scope

  • Writing our own runner binary.
  • GitLab runner compatibility.
  • Windows/macOS runner concerns (runner's problem, not ours).
  • Full GitHub Actions parity (reusable workflows, environments, deployments, OIDC token issuance).

Security notes

  • Registration tokens and runner secrets: store hashed, admin-only UI.
  • Secrets go to any runner that picks up the task — document clearly that runner hosts are trusted infrastructure; no fork-PR workflows with secrets in v1 (skip secret delivery for tasks triggered by non-writers when MR triggers arrive in Phase 2).
  • Workflow YAML is untrusted input: hard limits on size, job count, matrix size, log volume per task.

Acceptance criteria (Phase 1 = definition of done for this issue; 2/3 split into follow-ups)

  • [ ] Admin can create a registration token in the admin UI; stock forgejo-runner register against git-shark succeeds.
  • [ ] Registered runners appear in admin UI with status; Declare updates version/labels.
  • [ ] Push with .forgejo/workflows/*.yml (on: push, single job) creates a run; runner fetches, executes, streams logs; UI shows live per-step status and logs; final state (success/failure) correct.
  • [ ] Log resume after runner reconnect works (ack_index honored).
  • [ ] Task fails cleanly when runner vanishes (timeout).
  • [ ] Failing tests first: protocol round-trip against real forgejo-runner in an integration test (container), plus unit tests for trigger eval and task state machine.
  • [ ] Docs: docs/admins/ (runner setup, endpoints, new tables, reverse-proxy notes for Connect RPC), docs/users/ (writing workflows), docs/maintainers/ (protocol implementation decisions, phase gap list), root README.md feature list.

References

  • Proto: https://gitea.com/gitea/actions-proto-def (MIT), Go stubs incl. Connect handlers: https://gitea.com/gitea/actions-proto-go
  • Gitea Actions design (server/runner split): https://docs.gitea.com/usage/actions/design
  • Forgejo ↔ Gitea shared protocol confirmation: https://code.forgejo.org/forgejo/runner/issues/525
  • Forgejo runner registration flows: https://forgejo.org/docs/latest/admin/actions/registration/
  • Prior art (client-side protocol reimplementation): https://github.com/ChristopherHX/gitea-actions-runner

Migrated from https://github.com/workaround-org/git-shark/issues/13 Migrated from https://gitshark.ha1nz.de/repos/miggi/GitShark/issues/2 (original author: miggi, created 2026-07-09)

Comments 2

miggi miggi 2026-07-20
Phase 1 implemented and on main.
miggi miggi 2026-07-22
## Status update — CI/CD runner (Forgejo/Gitea `runner.v1`) ### Phase 1 — DONE ✅ (pushed to `main`, CI green Native+JVM) The full run loop works end-to-end, verified against a **real `act_runner` container**. - **Persistence** — `action_run` / `action_task` / `action_log` (V23), per-repo run number, surrogate int64 `task.seq` (V24). - **Workflow ingest on push** — `WorkflowIngestService` parses `.forgejo/workflows/` + `.gitea/workflows/` at the pushed commit, materializes a run + one task per job. - **FetchTask** — runner claims the oldest pending task, `FOR UPDATE SKIP LOCKED` (no double-dispatch), delivered with the `github.*` context it needs to run. - **UpdateTask / UpdateLog** — result roll-up, runner freed on finish, resume-safe log append (`ack_index`). - **Zombie reclaim** — scheduled sweep fails a task whose runner vanished past its deadline (`GITSHARK_CI_TASK_TIMEOUT`, default 1h); a late runner can't resurrect a reclaimed task. - **Actions UI** — per-repo run list + run detail (jobs + streamed logs). - **Real-runner integration test** — `gitea/act_runner` registers → fetches → runs → reports SUCCESS with logs. ### Phase 2 — mostly DONE - ✅ **Trigger filters** — `on.push` with `branches`/`tags`/`paths` (+ `-ignore`), GitHub-style globs, tag pushes. - ✅ **Label matching** — a task only goes to a runner advertising its `runs-on` labels. - ✅ **Secrets & variables** — per-repo, secrets encrypted at rest (`GITSHARK_SECRET_KEY`), delivered to runners; owner-only management UI at **Settings → CI secrets & variables**. - ✅ **`needs` ordering** — a job waits for its dependencies to succeed; a failed need cancels dependents (cascading) so the run terminates; needs' results delivered to the runner. *(pushed once `f1a438d` lands — currently committed locally)* ### Phase 2/3 — STILL TO DO - ⬜ **`needs` outputs** — capture a job's `outputs` from UpdateTask and pass to dependents (`needs.*.outputs`). Currently result-only. - ⬜ **`matrix` expansion** — needs a per-job **payload expander** (split the workflow into one single-job/single-cell YAML per matrix combination). Sizable sub-project — we currently send the raw workflow + `github.job`, fine for single-job. - ⬜ **Concurrency / cancel / re-run** — cancel a running job (signal via the UpdateTask response), re-run, cancel superseded runs on force-push. - ⬜ **Non-push events (`pull_request`, schedule, manual)** — **blocked**: no MR/PR-triggered CI path exists yet; needs that integration designed first. - ⬜ **Later (phase 3):** artifacts (`ACTIONS_RESULTS_URL`), server-side cache docs, repo/org-scoped & ephemeral runners, commit/MR status integration. **Suggested split:** finish `needs` outputs + cancel/re-run under this issue; move `matrix` (payload-expander) and `pull_request` events to a dedicated Phase-3 issue since they're deeper / blocked. *Docs kept in sync throughout: `docs/users/ci-runners.md`, `docs/admins/ci-runners.md`, `docs/maintainers/ci-runners.md`, root `README.md`.*

Log in to comment.

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog