Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

#138

P2: Workflow format gaps: triggers, job graph, secrets, services, and metadata

closed · opened by keenan 9d ago

keenan opened this 9d ago

Why

The workflow format is deliberately small: ci/workflow.go defines only version, on.push (branches/tags), and jobs made of shell steps with an image, runner labels, env, artifacts, and pages. That keeps the push-time checker small and the queue payload bounded, but it also means the format cannot describe several things ordinary CI needs. This issue is the single place to record the gaps, decide which are intended non-goals, and link the work that closes them.

The immediate user-visible gap is that a workflow has no name or description. The workflows page shows the file path (.vole/workflows/verify.yaml) and job IDs, so a reader has to open the YAML comments to learn what a workflow is for.

Current shape

  • Schema: ci/workflow.go; reference: docs/reference/workflow-reference.md.
  • Docs: docs/workflows.md; UI: server/web/workflows.go, server/web/templates/workflows.html.
  • Each job is queued as its own run (server/ci.go), so CI jobs are independent and unordered.
  • Only a push is a trigger; change requests get combined-result runs automatically.
  • The runner’s /cache, network, and resource limits are vole runner flags, not workflow fields.

Scope

1. Identity and description

  • Add top-level name: and description:; show them on the workflows page and run pages.
  • Add a job name: for display, and optionally a job description:.
  • Add a “What a workflow cannot express” section to docs/reference/workflow-reference.md, matching the existing section in docs/reference/image-reference.md.
  • State in docs/workflows.md that CI jobs are independent and unordered (only a local run uses declaration order).

2. Triggers

  • on.change_request (opened/updated/ready), with the base commit and changed files available to steps.
  • on.schedule (cron).
  • on.workflow_dispatch with typed inputs, plus a manual-run button and CLI command.
  • on.push.paths and paths-ignore.
  • Negated branch/tag patterns (!main), or document that they are not supported.

3. Job graph and execution

  • needs: with job-level dependencies and cycle detection.
  • Job outputs: and step outputs, so a later job can consume a value.
  • strategy.matrix, including fail-fast behavior.
  • Step if:, continue-on-error, and a retry policy.
  • Job-level timeout; today only steps are capped, at one hour each.
  • Concurrency groups and cancel-in-progress. A push now only cancels queued runs for the commit it replaced, and never stops a running job.
  • Reusable workflows or composite steps (uses:), or an explicit decision that shell in an image is the reuse mechanism.
  • Workflow-level env and defaults (shell, working directory); step-level env, working-directory, and shell.

4. Inputs, state, and containers

  • Secrets: a store, injection at job or step scope, masking in logs, and an audit trail. This is the biggest blocker for deploy and publish jobs.
  • Expressions/contexts (${{ }}) or a smaller documented interpolation syntax: rejected as a non-goal; the shell is the expression language and the event file is the context.
  • Service containers or sidecars, such as a database for tests.
  • Artifact download between jobs, plus artifact names and a retention setting.
  • Cache key, restore keys, and save policy. /cache is ambient today and keyed by repository, trust, and image.
  • Run summaries and annotations, so a job can report a result without a log read.

5. Deployment and policy

  • Environments, protection rules, and approvals for deploys; a tag currently publishes automatically.
  • Per-workflow or per-job permissions/tokens, and OIDC, instead of relying on the runner’s key.
  • A way for a job to declare network and resource needs (network: bridge, cpu/memory) instead of making them runner flags.
  • Failure notifications or webhooks.

6. Documentation and parity

  • Document rerun, cancel, and attempt semantics on the workflow and run pages; queue behavior currently lives only in docs/runner.md.
  • Document the local-run parity gaps in one place: vole workflow run ignores on.push, runs-on, artifacts, and sites.
  • Link the five-attempt requeue rule from docs/workflows.md rather than only from docs/runner.md.

Acceptance criteria

  • Every scope item above has a decision: adopt (with a sub-issue), defer (with a reason), or reject as a non-goal.
  • Adopted items are split into sub-issues linked from this one, in priority order. Suggested first slice: secrets, service containers, needs/outputs, and change-request/path triggers.
  • The cheap metadata item (name/description) is either implemented or explicitly deferred.
  • docs/reference/workflow-reference.md documents the current limits, including the absence of needs, secrets, matrix, and non-push triggers.
  • The workflows page shows a human name and description when the workflow declares them.

Related

  • #11 hardened the runner trust boundary; secrets and service containers must preserve that model.
  • #17 and #18 built the run metadata and log UI this would extend.
  • #64 added combined-result runs for change requests; an on.change_request trigger should reuse that path instead of adding a second scheduler.

Comments (7)

keenan 7d ago

Part 4 now has a design and a sub-issue: #154 (01M6ENRK326CJ1TBCP0KYV4ZZ9).

Decisions against the §4 checklist, so this item can be ticked:

  • A store — adopt. Encrypted at data/secrets.db, sealed with AES-256-GCM under data/secrets.key, repository scope only. Instance-wide secrets are deferred: they would need a second access model for one store.
  • Injection at job scope — adopt. Step scope is rejected: a job is one container, so a value is either in the job or not. Step scope would hide the name, not the value.
  • Masking in logs — adopt, best effort, on the runner before a chunk is uploaded, so data/logs/ never holds a value.
  • Audit trail — adopt. One line per delivery and per human read, naming the account, the run, the name, and the version.
  • No audience or protected-ref rule. The approval gate already decides which code runs, so a second one would be a rule to keep in sync with the first. Approving a tip grants it the repository’s secrets, and the docs will say so in those words.
  • Missing or oversized secrets leave the variable unset and log one warning; the job still runs, so a workflow can have optional secrets. Required ones are ${NAME:?} in the step.
  • Still separate items here: expressions, step-level env, and the deploy-side protections in §5 (environments, approvals, OIDC).
keenan 7d ago

Expressions are resolved as a non-goal, not deferred. The docs change is #158.

  • ${{ }}: reject. It would be a second programming language beside the shell: parser, type rules, functions, truthiness, escaping, and a validation story, on every push. The shell is already in every step and does the same work with the image’s own tools.
  • A smaller interpolation syntax: reject. Any substitution into run turns event text into code. A comment body, an issue title, and a manual input are written by someone else, which is why the format pins them as a JSON file instead of a command. A syntax limited to env would still invite the same mistake and would not remove the shell quoting rules a step needs anyway.
  • What a step gets instead: env, the secrets the job declares, and /run/vole/event.json with VOLE_EVENT_PATH. A manual input is read from that file like any other event field. Nothing is missing from the context; it is data rather than a template.
  • When conditions arrive: if if, needs, matrix values, or outputs are added, they should be typed fields with small, checkable rules, not a general expression language.

docs/reference/workflow-reference.md now has a Context and expressions section with a manual-input example, and docs/workflows.md says the same beside the trigger examples. I also removed the now-false note that a printed secret reaches the log, since the runner masks delivered values.

The §4 checklist item is ticked. There is no sub-issue to schedule.

keenan 6d ago

Audited the checklist against the tree rather than against this issue’s text, which predates most of what has landed. Done items are facts with a pointer; the rest are still open, except the two already decided in the comments above.

1. Identity and description — done

  • Top-level name: / description:, and a job name: / description:, are parsed, validated, pinned with the run, and shown on the workflows and run pages (ci/workflow.go, server/web/workflows.go).
  • “What a workflow cannot express” exists in the reference, and docs/workflows.md says jobs are independent and unordered.

2. Triggers — done

on.change-request, on.issue, on.comment, on.schedule, on.workflow-dispatch with typed inputs, on.push.paths / paths-ignore, and negated patterns are all implemented and documented. The event filters (mentioned, replied-to, assigned-to, author.name, author.role) came with them.

3. Job graph and execution — open, except one

Still open: needs: and outputs; strategy.matrix; step if:, continue-on-error, and retry; job-level timeout; concurrency groups and cancel-in-progress; workflow-level env and defaults; step env, working-directory, and shell. Reusable workflows (uses:) still has no decision recorded — that one wants one, because “shell in an image” is a real answer and should be written down either way.

4. Inputs, state, and containers — secrets and expressions settled

Secrets shipped in #154, expressions were rejected in the comment above with the docs to match. Still open: service containers; artifact download between jobs and artifact names/retention; cache keys and restore/save policy; run summaries and annotations.

5. Deployment and policy — network landed, one item left plus limits

  • Network: done, closed by #159 and in review as #160. A job declares network: none | bridge; a bridge job is only leased to a runner that advertises it, none is enforced on any runner, and a local run applies the same rule. Two document claims were wrong and are fixed: a job could not ask for a network before this, and the workflow format had no way to say it. verify and release will declare the field in a follow-up: the pre-receive validator is the running server binary, so a workflow file cannot use a field that server does not know yet.
  • Still open: per-workflow or per-job permissions/tokens and OIDC (this is the one that blocks a coding agent from writing back with anything narrower than a long-lived repository secret); per-job CPU/memory/PID limits, which are the same shape as network: and worth doing together when someone needs them; environments and deploy approvals; notifications or webhooks.

6. Documentation and parity — two of three done

  • Done: the local-run parity gaps are in docs/workflows.md and the reference, including that network: is not ignored the way triggers and runs-on are.
  • Done: docs/workflows.md links the requeue rule to docs/runner.md.
  • Open: rerun, cancel, and attempt semantics are only in the UI. The run page has the buttons and the log has per-attempt ranges, but nothing documents them, and “only a queued run can be canceled” is a rule a reader learns by clicking.

What I would take next

  1. Job-level timeout and cancellation of a running job. Small, and the same pair an agent needs so a runaway session can be stopped.
  2. Per-job tokens (the §5 item). Design it in its own issue: it decides what a job may do beyond running, which is a trust question, not a schema one.
  3. needs: and outputs, if the job graph is still wanted after network: showed that a single job with a capable image covers most of the cases.
keenan 6d ago

Two more items from §3 are now in review, one is filed, and the network item has landed.

§5 network — merged. #160 is on main. A job declares network: none | bridge, the request is pinned with the run, the lease only gives a bridge job to a runner that advertises one, and a local run applies the same rule. The follow-up that makes verify and release declare it is still waiting on the instance running a binary that knows the field (a pushed workflow is validated by the running server, not by the commit under review).

§3 job budget — in review as #163 (01M6FD499J7GZBJN3HZZFNZ9C3). timeout: on a job is a wall clock its steps share; a step that declares none runs with what is left, so a single long step needs one number. The step ceiling goes from one hour to four, the default stays ten minutes, and the budget does not cover the image build. The field is optional, so nothing changes for a workflow that does not use it.

§3 cancel a running run — filed as #162 with a design. The queue row takes a cancel marker, the runner learns about it on the heartbeat it already sends while a step runs, and the finished attempt stays canceled with its partial log. It is a protocol change rather than a format one, which is why it is not in #163.

Still open in §3: needs: with outputs, strategy.matrix, step if: / continue-on-error / retry, concurrency groups besides cancel, workflow-level env and step env / working-directory / shell, and an explicit decision on reusable workflows.

keenan 5d ago

Audited against the tree again. Since the last comment, the three items it named as next have landed:

  • #161 gave a job its own timeout:.
  • #162 lets a maintainer stop a run that is already going.
  • #165 and #173 gave a job a per-run credential through acts-as.

There were no open sub-issues or change requests from this tracker before this pass; #183 and #184 are the two filed now. Here is what is left and the decision for each, so the checklist can be read as a decision rather than a to-do list.

1. Identity and description — done

The fields are parsed, validated, pinned, and shown on the workflow and run pages. The reference has “What a workflow cannot express” and workflows.md says a pushed run has no ordering between jobs.

2. Triggers — done

on.change-request, on.issue, on.comment, on.schedule, on.workflow-dispatch, on.push.paths/paths-ignore, and negated patterns are in the format, with the event filters. Nothing to decide.

3. Job graph and execution — decided, nothing to build now

  • needs: and outputs — defer, not next. A job is its own queued run with its own checkout, and there is no artifact transfer between runs. needs before that would only serialize failures, not let a later job reuse a build. Revisit when a real workflow has a build that a second job must consume.
  • strategy.matrix — reject as a non-goal. A matrix multiplies queue rows and adds a second expansion layer to a format whose one unit is a container. A loop in the shell does the same work, and a job that needs two toolchains puts both in its image.
  • Step if:, continue-on-error, retry — reject as non-goals. The first line of a run: is already if, for, or until; continue-on-error is || true, and a retry is a loop. Typed fields here would be the second language the expressions decision rejected.
  • Job-level timeout: — done (#161).
  • Concurrency groups and cancel-in-progress — defer. The two cases that matter today are covered: a push cancels superseded queued runs, and #162 stops a running one. A named group that serializes two deploys belongs with the deploy work, not the job format.
  • Reusable workflows or composite steps (uses:) — reject as a non-goal. Reuse is an image: a definition under .vole/images or a published image is the shared unit, and a job runs the steps it declares. The reference already says this; recording it here makes it a decision rather than an omission.
  • Workflow-level env, step env, working-directory, shell — reject as non-goals. A step can export a variable or cd at the top of its run:. Job env is the container’s environment, which is the only scope with a boundary. A workflow-level value would be copied into every job, and a step-level one would restate the lines the step already writes. /bin/sh -c is the one shell the image must have; a different shell is something the step invokes.

4. Inputs, state, containers

  • Secrets — done (#154).
  • Expressions — rejected in the comment above.
  • Service containers — adopt; filed as #184 with a design. It is the largest remaining gap for ordinary CI: a test that needs a database currently has to carry it in the job image.
  • Artifact download between jobs, artifact names and retention — defer with needs. A run is one job, so there is no later job to receive the file. Base names already name an artifact and a collision is refused; retention is an instance policy, not something a workflow has needed.
  • Cache key, restore keys, save policy — reject as a non-goal. /cache is a directory the runner mounts. A step keys it by writing under a path it chooses, and the runner bounds it. A key API would duplicate what env and the image already do.
  • Run summaries and annotations — defer. The exit code, the step log, and the artifacts are the summary today. An annotation is a UI feature and can wait for a workflow that has something to say beyond its log.

5. Deployment and policy

  • Network — done (#159, #160). The repository’s own verify and release workflows still rely on the runner flag instead of declaring network: bridge; that is a one-line follow-up, not a format gap.
  • Per-job permissions and tokens — done through acts-as (#165, #173). The account’s role is the permission, and the credential dies with the run.
  • OIDC — defer. acts-as is the credential Vole gives a job. OIDC is federation to a cloud provider, which is a different trust design and no workflow has asked for it. If a deploy needs it, it belongs with environments.
  • Environments, protection rules, and deploy approvals — adopt, next after #184. A tag publishes automatically today; this is the remaining safety gap. It should reuse the approval path code already has rather than add a second gate. I will file it once #184 has a shape, because it changes the lease and approval path, not only the schema.
  • Per-job CPU, memory, and PID limits — defer. The runner flags work, and a job-level field would use the same capability/lease mechanism as network:. Wait for a workflow that needs it.
  • Failure notifications or webhooks — defer. Watching the run list is the current answer. A webhook needs delivery, retry, and a secret story, which is larger than the trigger and not blocking anything.

6. Documentation and parity — closed by #183

Change request #183 adds the attempt and rerun paragraph to docs/workflows.md: what an attempt is, the five-attempt cap, and what rerun does. Cancel was already there, the local-run parity gaps and the requeue link are in the same page, and docs/cli.md already documents run cancel and run rerun. That closes the last item in this section once it lands.

Tracking

  • #184 is the adopted item to build next.
  • The deploy-approval item is adopted after it, and gets its own issue when #184 has a design.
  • The rejected items are not getting issues. If one becomes real, it starts from a workflow that cannot be written, not from this checklist.
keenan 4d ago

Decision: environments, protection rules, and deploy approvals are rejected as a non-goal, not deferred.

The code-approval gate already decides what may land, and a tag is pushed by a maintainer. A second human gate at deploy time would mostly repeat that decision, and a gate that repeats a decision is the one people learn to click through.

The accepted tradeoff is explicit:

  • repository secrets stay repository-scoped. Any job an approved tip can queue may read the values it declares; there is no environment-scoped secret store.
  • a tag publishes automatically once it is pushed. The maintainer who can push a tag is the deploy approver.
  • an automated account with maintain can tag or dispatch a deploy without a second person. The account’s role is the limit.

Revisit this if production-only credentials arrive, or if deploys start from schedules, manual dispatch, or bot events where no change request names the commit being deployed. At that point the useful design is environment-scoped secrets plus a gate on the run, not a second review of the diff.

That removes the last adopted item from this tracker. Everything else is either done or has a recorded decision in the comments above. The repository’s verify and release workflows still rely on the runner’s --container-network flag instead of declaring network: bridge; that is workflow hygiene, not a format gap, and it is being handled as its own change.

Closing this as the decision log. Reopen it if a deferred item gets a concrete workflow that cannot be written.

keenan 4d ago

The workflow follow-up mentioned above is change request #186 (01M6M51A2RFGYF73V45M3TDFFF).