For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /dd/0013-actions-yaml-automation-system.md.

Themelio compatibility reference. This page describes the retained Themelio implementation or its historical design. Check the parallel-operation guide before applying it to the current platform.

DD-0013: actions.yaml Automation System — Implementation

Related ADR: ADR-0013: actions.yaml Automation System

Status: Draft

Date: 2026-06-04

Authors: TBD

ADR-derived technical notes

These notes are reconciled against the current code (services/web and services/k8s-root-actor). Where the ADR description and the implementation diverge, the code wins and the gap is flagged. The Design section below distinguishes as-built from target throughout.

YAML Schema Specification

The actions.yaml file at the root of a GitLab repository defines a map of named actions. The fields the runtime actually consumes today are a subset of the documented schema:

action_name:
  title: "Display Title"             # Optional, shown in UI (loadActionsYaml)
  label: "Button Label"              # Optional, falls back to title
  type: "slurm-job"                  # Required: slurm-job | k8s-deployment | k8s-inferenceservice
  ref: "#/references/gradio-space"   # Optional, server-side reference (resolved by k8s-root-actor)
  parameters:                        # Optional, map of parameter definitions (UI only).
    param_name:                      # Each parameter follows the JSON Schema specification.
      type: "string"                 # JSON Schema types: string | number | integer | boolean
      title: "Parameter Display Name"
      default: "default_value"
      enum: ["a", "b"]               # JSON Schema enum for constrained values
  repos:                             # Optional, extra repositories to clone (the repo holding
    - url: "https://gitlab.example.com/repo.git"   # actions.yaml is always cloned too — see below)
      dir: "repo"                    # Optional, derived from URL if omitted
  env:                               # Optional, "KEY=value" strings (parseActionEnv).
    - "KEY=value"                    # Consumed by all three action types (2026-06-07).
  pre:                               # Optional, pre-execution commands
    - "echo 'Setting up'"
  script: |                          # Required
    #!/bin/bash
    echo "Running with parameter: $param_name"
  post:                              # Optional, post-execution commands (slurm-job only today)
    - "echo 'Cleaning up'"
  options:                           # Optional, action-specific options
    cpus: 4
    ram: 16                          # interpreted as Gi by templates
    gpus: 1
    gpuLabel: nvidia.com/a100        # whole A100; use nvidia.com/a100-mig-1g.5gb for a MIG slice
    port: 8080
    modelName: "$param_name"

The TypeScript shape that k8s-root-actor deserializes into is ActionConfig in services/k8s-root-actor/src/types/action.ts. Note env is a string[] of KEY=value entries (not a map), and options keys are cpus | ram | gpus | gpuLabel | port | modelName. gpuLabel (k8s-deployment / k8s-inferenceservice only) selects the GPU resource key — for example, nvidia.com/a100 for a whole A100 or nvidia.com/a100-mig-1g.5gb for a MIG slice; an unsupported value is rejected with HTTP 400. There is no Zod/JSON-Schema validation today — js-yaml output is cast to ActionConfig.

Field semantics worth calling out:

  • parameters — each parameter definition follows the JSON Schema specification (type, title, default, enum, etc.). The web UI builds its input controls from these definitions, and the target validation (see Design §7) checks user-supplied values against the JSON Schema before substitution.
  • repos — an optional list of extra repositories to clone into the workspace. The repository that contains the actions.yaml (the "main repo") is always cloned first, independently of this list. For any repository whose URL is on the Themelio GitLab domain, a short-lived user GitLab PAT is automatically minted and injected (oauth2:<token>@…) so the clone can authenticate; non-Themelio URLs are cloned as-is. See the cloning logic in services/k8s-root-actor/src/templates/k8s-common/prepare.sh.hbs and Design §4.2.
  • env — KEY=value entries parsed by parseActionEnv. As of 2026-06-07 all three action types consume env: k8s-deployment/k8s-inferenceservice render it into the manifest env block (yamlq-quoted), and slurm-job emits export NAME='value' lines (formatEnvExports, shq-quoted) into the remote script.sh.

Parameter substitution (as-built — known gap)

Update (2026-06-07): closed. This naive replace was superseded by validated, context-escaped substitution (validateParameterValue + shq/yamlq in action-executor.ts). See Design §7. The snippet below is the original pre-hardening code, kept for history.

Substitution happens in the web service, before k8s-root-actor is called, in services/web/src/utils/actions/action-executor.ts:

for (const [parameterKey, parameterValue] of Object.entries(parameters)) {
  actionsYaml = actionsYaml.replace(new RegExp(`\\$${parameterKey}`, 'g'), parameterValue);
}

This is a naive global string replace on the raw YAML text — values are not shell-quoted, not YAML-escaped, and not validated against the declared parameter type. This is an injection risk and is treated as a gap to close (see Design §7), not the target behavior.

Template system (Handlebars)

Kubernetes manifests are rendered from Handlebars templates under services/k8s-root-actor/src/templates/ (compiled on every render in src/utils/template-renderer.ts — there is no template cache). The templates that actually exist:

  • slurm-job: slurm-job/script.sh.hbs, slurm-job/k8s-job.yaml.hbs
  • k8s-deployment: k8s-deployment/app.yaml.hbs, plus shared k8s-common/prepare.sh.hbs, k8s-common/PersistentVolumeClaim.yaml.hbs, k8s-common/Service.yaml.hbs, k8s-common/VirtualService.yaml.hbs, k8s-common/Gateway.yaml.hbs
  • k8s-inferenceservice: k8s-inferenceservice/app.yaml.hbs, plus the same k8s-common/* shared templates

The registered helpers (in template-renderer.ts) are:

HelperPurpose
joinJoin an array with a separator
shqShell-quote a value ('…' with embedded single-quotes escaped)
yamlqYAML/JSON-quote a scalar (JSON.stringify)
capitalizeCapitalize the first character
toUpperCaseUppercase the string

There is no base64/toLower helper; base64 encoding is done in TypeScript (Buffer.from(...).toString('base64')) before the value is passed into the template. shq exists but is not applied to user parameters — closing that loop is part of the security target.

Reference resolution

Server-side references are resolved by populateRefs() in services/k8s-root-actor/src/utils/actions-yaml-references.ts. Reference files live under services/k8s-root-actor/src/actions-yaml-references/ (.yaml, .yml, .json); one ships today: gradio-space.yaml. Path traversal is blocked by validatePathInDirectory() (resolved path must stay within the references dir). Nested references are resolved (the merged object is re-processed recursively), but there is no cycle detection.

SLURM specifics

The slurm-job k8s Job SSHes to a single external SLURM cluster using the Kubernetes secret themelio-hpc-ssh (keys: id_ed25519, known_hosts, ssh_user, ssh_host, remote_script_path), mounted read-only at /ssh. The remote script is sbatch-submitted inside a screen session. Both the cluster and secret name are hardcoded in slurm-job/k8s-job.yaml.hbs. This single-cluster/single-account model is a deliberate temporary solution; a future per-project SLURM-account binding is anticipated (see Design §4.1).

GitLab token scopes (as-built)

Implemented (2026-06-07): the web service mints a short-lived GitLab PAT named after the action with scopes ['read_repository', 'write_repository'] only (action-executor.ts). The broad api scope was dropped — actions only clone/push repository content. (Originally as-built this also included api.)

Overview

This document specifies how Themelio turns a declarative actions.yaml file at the root of a GitLab repository into running compute. The web service discovers actions, collects parameters, performs substitution, persists a job record, and proxies execution to k8s-root-actor; k8s-root-actor resolves references, renders Handlebars templates, and applies Kubernetes/SLURM resources for one of three action types (slurm-job, k8s-deployment, k8s-inferenceservice). It documents the system as built today, the target design it should converge to (secure substitution, ADR-0012-backed persistence and destroy, typed parameter UI, UI-guided generation), and the gap between them so an implementer knows exactly what remains.

Implementation status (reconciled 2026-06-07)

Most of the "target / gap" items below have since shipped. The sections retain their as-built→target framing for history, but the current state is:

Target (section)Status
Parameter validation + shq/yamlq escaping (§7)Implemented (web/src/utils/actions/action-executor.ts)
Drop api GitLab PAT scopeImplemented — minted with read_repository/write_repository only
Reference depth-3 limit + cycle detection (§6)Implemented (MAX_REFERENCE_DEPTH, CircularReferenceError, ReferenceDepthError)
Typed parameter input UI (§11)Implemented (ActionParametersModal.tsx)
UI-guided actions.yaml creation (§10)Implemented (RepositoryForm.tsx + api_createActionsFile); a huggingface-inference.yaml reference now ships alongside gradio-space.yaml
Destroy — deployments (§9)Implemented (DeploymentManager.tsx → DELETE …/deployments/{id})
Destroy — jobs UI (§9)Implemented (JobManager.tsx Delete button → DELETE …/jobs/{id})
env consumed by all three action typesImplemented — slurm-job and k8s-inferenceservice now honor env (previously k8s-deployment only)
ActionConfig Zod validator (§2)Not implemented — still an open target; structure is js-yaml-cast (per-value validation exists, structural does not)

The formal Status stays Draft because ADR-0013 is still Proposed.

Goals and Non-Goals

Goals

  • Provide an accurate, implementer-facing specification of the three action types and their end-to-end execution path, naming the actual files and symbols.
  • Close the parameter-handling gap: validate parameter values against their declared type and quote them safely (using the existing shq/yamlq helpers) so user input cannot break out of the script or manifest.
  • Render every declared parameter type (string, number, boolean, enum, file) with an appropriate input control in the web UI.
  • Create the ADR-0012 record at action execution and surface per-resource destroy in the UI.
  • Auto-generate actions.yaml during model and space creation only, driven by the web UI: a huggingface-format model → k8s-inferenceservice, a gradio space → k8s-deployment.

Non-Goals

  • Re-specifying ADR-0012's persistence schema. The jobs/deployments/*_logs tables, secret model, and CRUD/destroy endpoints are defined in DD-0012; this DD only describes how actions integrate with them.
  • Multi-step workflows or dependencies between actions.
  • Multiple SLURM clusters or dynamic cluster/account selection (single hardcoded cluster + single shared SLURM account via the themelio-hpc-ssh secret). This is a deliberate temporary stance — binding projects/namespaces to specific SLURM accounts is future work.
  • Per-action quota enforcement. Resource quotas are owned by an external quota mechanism managed by a Waldur site agent at the namespace/SLURM-account level; this system does not cap or duplicate cpus/ram/gpus enforcement per action.
  • Container image caching across executions.

Design

1. Architecture & responsibilities

Two services collaborate; the boundary is that the web service pre-resolves the action config and only sends the single selected action to k8s-root-actor.

ConcernWeb service (services/web)k8s-root-actor (services/k8s-root-actor)
Fetch actions.yamlYes — GitLab raw file APINo
Parameter substitutionYes (action-executor.ts)No
Parse + select actionYes (js-yaml, actions[actionName])Receives resolved actionConfig
Reference resolution (#/references/)NoYes (populateRefs)
Template renderingNoYes (renderTemplate)
kubectl/SLURM applyNoYes (kubectlCommands, SSH)
Job/record trackingDelegates to the ADR-0012 layerOwns ADR-0012 records

2. actions.yaml format & parsing

Two parse paths exist, both using js-yaml:

  1. Listing for the UI — loadActionsYaml() (services/web/src/utils/actions/action-loader.ts) fetches …/repository/files/actions.yaml/raw?ref={branch} and projects each entry to a CustomAction (name, title, label, parameters). Missing/invalid files degrade gracefully to []. This is invoked from the resource-detail GET routes (e.g. services/web/src/app/api/models/[namespace]/[repo]/route.ts), so the action list rides along on the model/space payload — there is no dedicated /actions/[branch] endpoint.
  2. Execution — executeAction() (action-executor.ts) re-fetches the raw YAML at the target branch, substitutes parameters, parses, and selects actions[actionName].

Target: introduce a shared schema validator (Zod) for ActionConfig used by executeAction before dispatch, returning a 400 with field-level errors instead of casting js-yaml output.

3. Action execution flow (as-built)

User clicks action (ResourceActions.tsx) ─ optional ActionParametersModal
  ↓ POST /api/{models|datasets|spaces|repos}/{ns}/{repo}/action/{branch}
     body: { action, parameters }
Web: handleActionRoute → executeAction (action-executor.ts)
  ↓ fetch actions.yaml(raw, ref=branch) → substitute $params → parse → select action
  ↓ guard: getRepoJobs({ idPrefix, onlyRunning }) → 409 if already running
  ↓ createJobEntry(...) + pushJobLog("INITIALIZED")   [as-built: always a "job"; target: branch job vs deployment per type — see §8]
  ↓ createPersonalAccessToken(scopes: read_repository, write_repository)   [target: drop `api`]
  ↓ kubeRequest POST {K8S_ROOT_ACTOR_URL}/{repoType}/{ns}/{repoId}/{branch}/action
     body: { actionName, actionJobName, actionConfig, gitlabToken }
k8s-root-actor (index.ts):
  ↓ populateRefs(actionConfig)        → 400 traversal / 404 missing ref / 500 parse
  ↓ namespace = sanitizeK8sName(namespace)
  ↓ switch(resolvedConfig.type):
       slurm-job             → actionSlurmJob
       k8s-deployment        → actionK8sDeployment
       k8s-inferenceservice  → actionK8sInferenceService
  ↓ renderTemplate(...) → kubectl{Create|Apply}Template / Service / VirtualService / Gateway / PVC
  ↓ return { status: "pending" | "conflict"(409) | "error"(400/404/500), ... }
Web: pushJobLog("K8S_JOB_SUBMITTED" | "K8S_JOB_FAILED") → return to UI → redirect to #jobs

4. Action types

All three handlers receive the resolved Action (src/types/action.ts) and base64-encode rendered scripts before embedding them in manifests.

4.1 slurm-job — src/lib/action-slurm-job.ts

Renders slurm-job/script.sh (with repos, pre, post, env as export NAME='value' lines via formatEnvExports, script, and cpus/ram/gpus defaulting to 1/4/0), base64-encodes it, and creates a Kubernetes batch/v1 Job from slurm-job/k8s-job.yaml. The Job container scps the script to the SLURM head node via the themelio-hpc-ssh secret and launches it under screen + sbatch. No PVC, Service, or external URL. Maps to ADR-0012's jobs table.

A single SLURM cluster and a single shared SLURM account back all slurm-job actions, with the cluster and themelio-hpc-ssh secret hardcoded in the template. This is an explicitly temporary arrangement: there is no per-project or per-action cluster/account selection today. The intended future direction is to bind each Themelio project/namespace to a specific SLURM account, so the substitution and secret-resolution paths should be written with that eventual per-project mapping in mind rather than hardcoding assumptions that would be costly to unwind.

4.2 k8s-deployment — src/lib/action-k8s-deployment.ts

Computes a stable name via buildStableActionName([repoType, namespace, repoId, branchName, actionName]) (sanitized prefix + 8-char SHA1), then:

  1. kubectlResourceExists(...) → throws K8sResourceAlreadyExistsError (surfaced as 409 conflict) if the deployment exists.
  2. Creates a 10Gi PVC ({name}-workspace).
  3. Renders k8s-common/prepare.sh as an init script: always clones the main repo (the one holding actions.yaml, at {THEMELIO_URL}/{repoType}/{namespace}/{repoId}.git) with oauth2:{gitlabToken}@ injected, then clones each entry in repos — injecting oauth2:{gitlabToken}@ for Themelio-hosted URLs and cloning others as-is — and finally runs pre. (sync_or_clone_repo re-syncs an existing checkout instead of re-cloning.)
  4. Applies k8s-deployment/app.yaml (init container + main container running the base64 script, env from parseActionEnv, cpus/ram/gpus/port).
  5. Applies Service, Istio VirtualService, and Gateway (service-gateway) for an external URL.

Returns { deploymentName, pvcName, url, internalURL }. Maps to ADR-0012's deployments table.

4.3 k8s-inferenceservice — src/lib/action-k8s-inferenceservice.ts

Same stable-name + init-script approach, but applies a KServe InferenceService (k8s-inferenceservice/app.yaml, vLLM serving) with a PVC mounted at /mnt/models, env from parseActionEnv appended to the model-server env block, plus VirtualService and Gateway. Maps to ADR-0012's deployments table.

5. Template system

Templates are read and compiled on every call to loadTemplate (no caching — edits to .hbs files take effect immediately) and rendered with renderTemplate(name, context). Helpers available: join, shq, yamlq, capitalize, toUpperCase (see notes above). Scripts are base64-encoded in TypeScript before templating; manifests are piped to kubectl via stdin by src/utils/kubectlCommands.ts.

Target / gap: route every user-supplied parameter through shq (for shell context) or yamlq (for manifest scalars) at substitution time, so quoting is structural rather than relying on the author of actions.yaml. (Implemented 2026-06-07 — see §7.)

6. Reference resolution

populateRefs() recursively walks the config; any object with a ref key is replaced by the merged content of the referenced file (#/references/<name> or a relative path under src/actions-yaml-references/), with the action's inline keys overriding the reference. Path traversal is rejected (PathTraversalError → 400); a missing file is ReferenceNotFoundError → 404; a parse failure is ReferenceParseError → 500. Nested references resolve. Implemented (2026-06-07): nesting is bounded at a maximum depth of 3 — populateRefs tracks the resolution depth and the visited-path set, throwing ReferenceDepthError past three levels and CircularReferenceError on a revisited path. Shipped references: gradio-space.yaml (a k8s-deployment that bootstraps uv, installs requirements.txt, and runs app.py) and huggingface-inference.yaml (a k8s-inferenceservice for vLLM serving).

7. Parameter substitution & security (gap)

Update (2026-06-07): implemented. action-executor.ts now validates each value against its definition and applies shq (shell) / yamlq (YAML) escaping. The "As-built" text below is the original pre-hardening state, kept for history.

As-built: raw-text regex replace, no quoting, no validation (notes above). A parameter value like "; rm -rf / #" or one containing YAML control characters can break out of the intended context.

Target:

  1. Validate each provided value against its parameter's JSON Schema definition (type, enum membership, required); reject unknown parameter keys.
  2. Apply context-appropriate escaping when substituting — shq for values landing in shell script/pre/post, yamlq for values landing in manifest scalars (options, env).
  3. Perform validation, escaping, and parameter resolution entirely in the web service (executeAction). k8s-root-actor is never responsible for parameter handling — it receives an already-resolved actionConfig and the request contract between the two services is unchanged.

8. Persistence integration (ADR-0012)

Per ADR-0012, the three action types collapse into two ADR-0012 resource kinds:

Action typeADR-0012 kindTable
slurm-jobjob (short-running batch task)jobs
k8s-deploymentdeployment (long-running service)deployments
k8s-inferenceservicedeployment (long-running service)deployments

For now slurm-job is the only job type; both Kubernetes action types are deployments. Action execution is the creation point for the corresponding ADR-0012 record. The record schema, secret model, status enums, gitlab_project_id keying, and the read/update/list/destroy endpoints are all defined in DD-0012 and are not restated here.

Update (2026-06-07): implemented (the split moved server-side). The legacy Valkey helpers (createJobEntry / pushJobLog / getRepoJobs) were removed. executeAction no longer creates any record itself — it proxies the resolved action to k8s-root-actor, which owns the id+secret, branches on actionConfig.type (slurm-job → jobs, k8s-deployment/k8s-inferenceservice → deployments), and persists the correct ADR-0012 record. The "As-built gap" text below predates this.

As-built gap: services/web/src/utils/actions/action-executor.ts predates the ADR-0012 job/deployment split — it unconditionally calls createJobEntry / pushJobLog / getRepoJobs (Valkey-backed "job" records) for every action type, so a k8s-deployment or k8s-inferenceservice is wrongly tracked as a job. Target: executeAction must branch on actionConfig.type and create a job record for slurm-job versus a deployment record for k8s-deployment/k8s-inferenceservice, routing each through the matching ADR-0012 layer rather than the legacy Valkey job helpers.

9. Destroy lifecycle

Update (2026-06-07): implemented for both kinds. k8s-root-actor exposes DELETE /jobs/{id} and DELETE /deployments/{id} (DD-0012) with per-type resource teardown; DeploymentManager.tsx and JobManager.tsx both render working Delete buttons. The "As-built" text below predates this.

As-built: no destroy. DeploymentManager.tsx renders a hardcoded disabled delete button; k8s-root-actor exposes no DELETE.

Target: destroy is per-resource (not a separate action type) and runs through DD-0012's destroy flow and DELETE endpoints. The actions-specific obligation is teardown of the exact resources each action type created: for k8s-deployment the Deployment + Service + VirtualService + Gateway + PVC, for k8s-inferenceservice the InferenceService + VirtualService + Gateway + PVC, for slurm-job the Kubernetes Job — deleted by their stored stable names.

10. UI-guided creation

Automatic actions.yaml generation is scoped to two repo types only — model and space — and is driven entirely by the web UI during repository creation. Datasets and generic repos never get an auto-generated actions.yaml.

Update (2026-06-07): implemented. RepositoryForm.tsx offers the type/format selector and api_createActionsFile commits the pre-filled actions.yaml on create (gradio → k8s-deployment, huggingface → k8s-inferenceservice). The "As-built" text below predates this.

As-built: the model/space create flows (services/web/src/app/(auth)/{models,spaces}/new/page.tsx) only create the repo and a README; no actions.yaml is generated.

Target: the create-model and create-space UIs offer a type/format selector and commit a pre-filled actions.yaml based on the choice. The selectable values are intentionally minimal for now:

Repo typeUI selection (only option today)Generated action
spacespace type gradioa k8s-deployment action, reusing gradio-space.yaml as the reference
modelmodel format huggingfacea k8s-inferenceservice action (vLLM serving the Hugging Face model)

Each is a single-choice selector today (gradio for spaces, huggingface for models) but is modelled as an enumeration so additional space types / model formats can be added later without reworking the flow. The committed actions.yaml is immediately runnable, and the user can edit it afterward.

11. Parameter input UI

Update (2026-06-07): implemented. ActionParametersModal.tsx now renders dedicated controls per type (text, number, checkbox, select, file upload) with default pre-fill and client-side required/enum validation. The "As-built" text below predates this.

As-built: ActionParametersModal.tsx renders type={param.type === 'string' ? 'text' : param.type}, so only string (and partially number) produce valid inputs; boolean, enum, and file emit invalid HTML type attributes. No required-field enforcement or default pre-fill.

Target: dedicated controls derived from each parameter's JSON Schema — text (string), number input (number/integer), toggle/checkbox (boolean), select for a constrained enum, file upload (file) — with default pre-fill and client-side required/enum validation mirroring the server-side validation from §7.

Data Model

Current ActionConfig (k8s-root-actor)

type ActionConfig = {
  type: 'slurm-job' | 'k8s-deployment' | 'k8s-inferenceservice';
  ref?: string;
  repos?: { url: string; dir?: string }[];
  env?: string[];                 // "KEY=value" entries
  pre?: string[];
  post?: string[];
  script: string;
  options?: { cpus?: number; ram?: number; gpus?: number; gpuLabel?: string; port?: number; modelName?: string };
};

The web-side CustomAction/ActionParameter shapes (services/web/src/utils/actions/action-types.ts) carry only the UI-relevant fields (name, title, label, parameters).

Target: a validated schema (Zod) sharing these field names, additionally typing parameters[*] as JSON Schema parameter definitions (type, title, default, required, enum) so both the UI and executeAction validate against one source of truth.

Persistent records (jobs, deployments, job_logs, deployment_logs) and their status enums are owned by ADR-0012 — see DD-0012 § Data Model.

API Design

Web service

Execute an action

POST /api/{models|datasets|spaces|repos}/[namespace]/[repo]/action/[branchName]

Request:

{ "action": "train-model", "parameters": { "learning_rate": "0.001", "epochs": "10" } }

Response (proxied from k8s-root-actor):

{ "status": "pending", "message": "Kubernetes job applied for Slurm action", "actionName": "train-model" }

Errors: 401 (missing PRIVATE-TOKEN), 409 (A job for this action is already running), 500 (action not found / file not found / k8s failure).

Listing actions is not a standalone endpoint — actions are embedded in the resource-detail GET payload (GET /api/models/[namespace]/[repo], etc.) via loadActionsYaml.

k8s-root-actor

POST /:repoType/:namespace/:repoId/:branchName/action

Request:

{
  "actionName": "train-model",
  "actionJobName": "ns-repo-main-train-model-1717459200000",
  "actionConfig": { "type": "slurm-job", "script": "…", "options": { "gpus": 1 } },
  "gitlabToken": "glpat-…"
}

Responses:

CodeBody statusWhen
200pendingResource applied
409conflictK8sResourceAlreadyExistsError (deployment exists)
400errorPath traversal in ref, or unknown action type
404errorReferenced file not found
500errorReference parse failure or apply failure

Target additions: the action endpoint also creates the persisted record, and destroy is served by dedicated DELETE endpoints — both contracts are defined in DD-0012.

Testing Strategy

Unit

  • populateRefs: #/references/ and relative resolution; nested refs up to depth 3 (and rejection past it, which also covers cycles); PathTraversalError on ../ escapes; ReferenceNotFoundError / ReferenceParseError.
  • Parameter validation + escaping (target §7): values are checked against their JSON Schema (enum membership rejected when out of range, required enforced, type coerced/validated); shell/YAML metacharacters in values are neutralized by shq/yamlq.
  • buildStableActionName / sanitizeK8sName: deterministic, DNS-safe, ≤ name limits.
  • Template rendering: each *.hbs renders valid YAML for representative contexts.

Integration (local Minikube)

  • Each action type end-to-end: slurm-job creates a batch/v1 Job; k8s-deployment creates PVC + Deployment + Service + VirtualService + Gateway and serves at the external URL; k8s-inferenceservice creates a KServe InferenceService.
  • Re-running a live k8s-deployment returns 409.
  • Target: action execution creates the expected ADR-0012 record, and destroy removes exactly the resources the action created (per §9). Record/status assertions belong to DD-0012's test suite.

Manual acceptance criteria

  • A repo with actions.yaml shows its actions in ResourceActions; a parameterized action opens the modal with correct controls per type; submitting runs the action and the UI redirects to #jobs where status updates appear.
  • A malicious parameter value cannot alter the executed command or manifest (target).

Deployment & Rollout

Phased, ordered by risk and by dependency on ADR-0012 landing first:

  1. Security hardening (no new infra): add ActionConfig/parameter schema validation and safe shq/yamlq substitution in the web service; drop the GitLab PAT api scope (mint with read_repository/write_repository only); enforce the depth-3 reference nesting limit.
  2. Persistence + destroy (requires ADR-0012): adopt the ADR-0012 record layer for action jobs/deployments; implement per-action resource teardown on destroy; enable the destroy button.
  3. Typed parameter UI: ship boolean/enum/file controls and client-side validation in ActionParametersModal.
  4. UI-guided creation: generate actions.yaml during model and space creation only — a huggingface model → k8s-inferenceservice, a gradio space → k8s-deployment.

Each phase is independently shippable; phase 2 must not start before ADR-0012's schema and endpoints exist.

References

Page source

Source snapshot · Revision f928834c8.