Skip to content

feat(actions): run Nomad task action blocks as implicit workload actions - #840

Merged
notheotherben merged 2 commits into
mainfrom
claude/project-thread-gos1lc
Nov 6, 2026
Merged

notheotherben merged 2 commits into
mainfrom
claude/project-thread-gos1lc

Conversation

@notheotherben

Copy link
Copy Markdown
Member

Requested by Ben · project thread

Each action block in a workload's job specs becomes one of the workload's implicit actions. Nothing needs adding to workload.yaml. For example, nomad-auto-update's check-now now shows up in Buckle's UI and runs from there.

Discovery (server, from Git)

  • buckle_core::engine::task_actions: when a revision loads, the loader parses each job's HCL as written (hcl-rs, the same approach as declared_vars) and stores the result on LoadedWorkload.task_actions.
  • Names: the block label. It becomes task.label when two tasks share a label or the label is already an implicit or declared action's name.
  • Description: the command line, when command and args are written as plain strings.
  • Not found: a spec that doesn't parse (a .j2 spec whose template syntax isn't HCL) offers no actions, and neither does an action inside a dynamic block.
  • actions::list offers them with kind nomad.action, implicit, and no confirmation.

Where it runs (any / all allocations)

  • Defaults: one running allocation of the group. For a system job, every allocation.
  • When the operator can pick: when the group may run several allocations (a system job, or a count other than a literal 1). In that case ActionInfo.instances carries the default.
  • How the pick travels: RunActionRequest.instances / StartActionRollout.instances → Command::RunAction / RolloutAction (the action digest includes it) → RunAction.instances (proto field 8) → the executor. The audit message records it.

Execution (executor)

  • Transport: Nomad's action endpoint is a WebSocket (GET /v1/job/:id/action?action=&allocID=&task=&tty=false). A new Transport::socket opens it in two ways:
    • over TCP/TLS, as a reqwest HTTP/1.1 upgrade on the same CA config;
    • over the task API socket, as a hyper upgrade.
      This adds tokio-tungstenite with only the handshake feature.
  • Before running: the executor checks that the registered job still defines the action. If not, it fails with "roll out the revision that adds it".
  • Steps: one step per allocation, run in order, stopping at the first non-zero exit.
  • Output: each line streams as StepProgress. The last 64 KiB, scrubbed of the assignment's secrets, is kept in StepOutcome.output / StepReport.output.
  • ACL: no change. The deploy identity's namespace write already grants alloc-exec. raw_exec tasks would also need alloc-node-exec.

UI

  • A task action that offers the choice opens a dialog with One allocation / Every allocation. No note is required.
  • The workload-wide rollout dialog offers the same choice.
  • The run report shows Show output on steps that printed something. The output stays out of the DOM until opened.

Tests

  • core: discovery, defaults per job type and count, naming collisions.
  • nomad: action frames, line streaming and exit code against the fake; WebSocket upgrade round trips over TCP and the Unix socket; refused-upgrade message.
  • dev agent (CI): a real raw_exec action, exit 0 and exit 3.
  • agent: any, all and no-choice cases; non-zero exit with scrubbed output; an action the registered job lacks never opens a socket.
  • server: the listing over an HCL job; the instances pick reaches RunAction.
  • UI: unit tests, plus an e2e test of the picker on a placement row.

Not run locally: the e2e suite and screenshots, because trunk couldn't be installed in this environment. CI runs both.


Generated by Claude Code

…tions

Each `action` block in a workload's job specs is offered as one of the
workload's implicit actions, with nothing added to workload.yaml:

- Discovery: the revision loader reads the job HCL as written (hcl-rs)
  and names each block by its label (task-qualified on a collision);
  plain-string command lines become the description.
- Where it runs: one running allocation of its group, or every one of a
  system job's. Groups that may run several allocations offer the choice
  (`ActionInfo.instances`), carried by run and action-rollout requests,
  `RolloutAction` and `RunAction.instances` to the executor.
- Execution: Nomad's action WebSocket (`/v1/job/:id/action`), opened by
  a new `Transport::socket` over TCP/TLS (reqwest HTTP/1.1 upgrade) and
  the task API socket (hyper upgrade). One step per allocation, stopping
  at the first non-zero exit; the executor checks the registered job
  still defines the action.
- Output: lines stream as step progress; the scrubbed last 64 KiB lands
  in `StepOutcome.output` / `StepReport.output`, behind "Show output" in
  the run report.
- UI: a task action offering the choice opens a dialog with One / Every
  allocation (no note required); workload-wide rollouts offer the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WkowUvEdVzjDvfrZRY8Uot
@notheotherben notheotherben self-assigned this Nov 6, 2026
…er's own connection

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WkowUvEdVzjDvfrZRY8Uot
@notheotherben
notheotherben merged commit 7f90cf4 into main Nov 6, 2026
9 checks passed
@notheotherben
notheotherben deleted the claude/project-thread-gos1lc branch November 6, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants