conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

commit 0ae4cfed4ba3c105744ddd9de196e9c37fee44c5
parent 150b37b08fc94a0c6b6ef6d3b71a528c551783fd
Author: finwo <finwo@pm.me>
Date:   Sun, 20 Sep 2026 04:17:48 +0200

Document reading a task back and name the worker api for who calls it

Diffstat:
Mdocs/api.md | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 74 insertions(+), 10 deletions(-)

diff --git a/docs/api.md b/docs/api.md @@ -12,17 +12,24 @@ A worker deals only in tasks. It is never told which project or job a task belongs to, and the task id it receives carries no structure it could read that out of. What it gets is a context and a script. -The conductor exposes four HTTP surfaces, all under `/api/v1`. Everything +The conductor exposes five HTTP surfaces, all under `/api/v1`. Everything else, including signing in and managing projects, workers and users, happens in the interface. -| Surface | Authentication | -| ------------------------------------------ | --------------------- | -| `POST /api/v1/projects/:project/trigger` | per project HMAC | -| `POST /api/v1/projects/:project/jobs` | per project HMAC | -| `GET /api/v1/projects/:project/jobs/:job` | HMAC, session, or anonymous for a public job | -| `/api/v1/tasks/...` | worker token | -| `GET /api/v1/projects/.../artifacts/...` | session, or anonymous for a public job | +| Surface | Authentication | +| --------------------------------------------- | --------------------- | +| `POST /api/v1/projects/:project/trigger` | per project HMAC | +| `POST /api/v1/projects/:project/jobs` | per project HMAC | +| `GET /api/v1/projects/:project/jobs/:job` | HMAC, session, or anonymous for a public job | +| `GET /api/v1/tasks/:task` | session, or anonymous for a public task | +| `GET /api/v1/projects/:project/tasks/:task` | HMAC, session, or anonymous for a public task | +| `POST /api/v1/tasks/claim`, `/tasks/:task/...` | worker token | +| `GET /api/v1/projects/.../artifacts/...` | session, or anonymous for a public job | + +Note the two readings of `/api/v1/tasks/:task`. Asking after the task +itself is a public read; everything below it is the worker protocol and +needs a token. They are separate plugins for that reason, so the token +requirement cannot accidentally spread to the first or lapse on the rest. Errors are `{"error": "..."}` with a meaningful status. A resource the caller may not see returns 404 rather than 403, so absence and denial are @@ -112,8 +119,65 @@ Three ways to be allowed: the job is public, the caller holds the project's trigger secret, or the caller is signed in and may manage the project. One credential therefore covers starting a build and watching it. -The task API ------------- +Reading a task back +------------------- + +``` +GET /api/v1/tasks/:task +GET /api/v1/projects/:project/tasks/:task +``` + +A task's state and the artifacts it stored. Both paths return the same +body. A task id is unique on its own, so naming the project is optional; +doing so only widens what may reach a private task, because there is then a +trigger secret to check against. + +| | public task | trigger secret | session that may manage the project | +| --- | --- | --- | --- | +| `/tasks/:task` | yes | not applicable | yes | +| `/projects/:project/tasks/:task` | yes | yes | yes | + +A signature on the bare path proves nothing, since there is no project in +it to look a secret up from. On the scoped path the project must be the one +the task belongs to, so a guessed id cannot be read through some other +project that happens to be readable. Signing a GET means an HMAC over an +empty body. + +```json +{ + "task": { + "id": "m2y5t7deprrj9s6jvv4d", "name": "package:arch=x86_64,pkg=musl", + "base_name": "package", "state": "success", "arch": "x86_64", + "image": "debian:bookworm-slim", "attempt": 1, "max_attempts": 1, + "allow_failure": false, "exit_code": 0, "error": null, + "worker_name": "builder-1", "timeout": 3600, "workdir": "/work", + "needs": ["build:arch=x86_64"], "matrix": { "pkg": "musl" }, + "artifact_paths": ["dist/**"], "log_size": 4096, + "created_at": 1800000000000, "started_at": null, "finished_at": null, + "project_id": "demo", "job_id": "m2y5rj7ykdm9sfbr54hf", "job_number": 12, + "ref": "refs/heads/main", "sha": "<40 hex>", "visibility": "public" + }, + "artifacts": [ + { "id": "...", "path": "dist/app", "size": 1048576, "sha256": "<64 hex>", + "created_at": 1800000000000, "expires_at": null, + "url": "https://ci.example.com/api/v1/projects/demo/jobs/<job>/tasks/<task>/artifacts/<id>" } + ] +} +``` + +`artifact_paths` is what the pipeline told the task to collect; `artifacts` +is what it actually stored. Each carries a ready download `url`, which is a +convenience rather than the permission: that route checks visibility again +for itself. + +There is no `env` and no `services` here, deliberately. Both can hold +project variables, and a service carries an environment of its own. The +`script` is left out for the same reason: a public job can be built from a +repository that is not public. A worker gets all three because it has to; +nobody else does. + +The worker API +-------------- Documented because a worker is a normal client of it, and anyone may write another. Every call needs `Authorization: Bearer <worker token>`, and a