Files
relspecgo/docs/JOB_FILES.md
T
SG CommandandClaude Sonnet 5 4d299fda98 feat(job): declarative YAML job files for named relspec workflows
Add `relspec job list` and `relspec job run <name>` driven by YAML job
manifests (relspec.yml / relspec.<name>.yml), so multi-file merge and
conversion workflows can be expressed declaratively instead of as long
shell command lines.

v1 contract (see docs/JOB_FILES.md):
- `command` is a closed allow-list (convert, merge, scripts-list); no
  field accepts a shell string or executable path.
- Deterministic discovery: default file first, then named files sorted
  lexically; all files merged into one namespace; duplicate job names
  across files are a hard error.
- Every path resolves relative to the job file's directory; absolute,
  home-relative and directory-escaping paths are rejected at validation.
- Database credentials referenced by env-var name via `conn_env:`;
  connection strings are never stored and are redacted from logs/plan.
- Full validation (version, unknown fields, command/format, per-command
  input/output shape, path traversal, depends_on targets, dependency
  cycles) runs before anything is read, written or executed; per-job
  pre-flight then checks input existence, script dirs, env vars and the
  output overwrite policy for the whole plan.
- `depends_on` closure runs in deterministic topological order;
  `--no-deps` runs only the named job.
- `--dry-run` (alias `--plan`) prints the resolved plan and exits 0
  without touching inputs, outputs or databases.
- A failing job propagates the underlying non-zero exit status, logs
  FAILED (never OK), and writes no success marker.

pkg/jobs is side-effect free (discovery/parse/validate/plan only);
execution adapters live in cmd/relspec/job.go. Includes unit tests for
discovery, validation, planning and path safety, plus CLI tests for
end-to-end convert/merge, scripts-list across multiple directories,
dry-run, dependency chains, exit-code propagation and log redaction.

Deferred: live `scripts execute` from jobs, split/inspect/diff/templ
commands, job-to-job output wiring, log rotation/retention.

Refs #20

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 00:44:38 +02:00

8.0 KiB

RelSpec Job Files

Job files let you declare named, repeatable RelSpec workflows in YAML and run them with relspec job run <name> instead of retyping long command lines.

relspec job list                     # deterministic list of discovered jobs
relspec job run build-schema --plan  # validate + print plan, execute nothing
relspec job run build-schema         # run the job (and its dependencies)

Design contract (first release)

This is the smallest coherent contract that is safe and useful end to end. Anything not listed under "Supported" is intentionally deferred.

Not a shell

command is a closed allow-list. There is no field anywhere that accepts a shell string, an executable path, or arbitrary arguments. Adding a new command means adding a vetted adapter in the RelSpec source.

command what it does
convert read one or more input schemas, additively merge them, write one output
merge like convert but requires ≥2 inputs and exposes skip_* merge options
scripts-list deterministically list SQL scripts across one or more directories

Deferred (documented, not implemented here): scripts execution against a live database, split, inspect, diff, templ, job-to-job output wiring, log rotation/retention. Live SQL execution already exists as relspec scripts execute; wiring it into the job runner is a follow-up because it needs live database credentials and cannot be covered by offline tests.

Discovery and precedence

relspec job (no --file) scans --dir (default .) for:

  1. relspec.yml / relspec.yaml (the default file), then
  2. relspec.<name>.yml / relspec.<name>.yaml (extra files),

each group sorted lexically. Order is stable across runs. Use --file <path> (repeatable) to load explicit files and skip discovery.

All discovered/selected files are merged into one job namespace. A job name defined by more than one file is a hard error naming both files. YAML maps already forbid duplicate keys within a single file.

Paths

  • Every path (inputs[].path, output.path, script_dirs[], logfile) is relative to the directory containing the job file that declared the job, not the process working directory.
  • Absolute paths, ~-relative paths and any path that resolves outside the job file directory (../, a/../../b, …) are rejected during validation — before anything runs.

Credentials

  • Database inputs (format: pgsql / mssql) and database execution outputs (format: pgsql with conn_env) reference an environment variable name via conn_env:. The connection string itself is never stored in the manifest.
  • A conn_env value that looks like a connection string (contains :, /, @, =, spaces) is rejected.
  • Missing/empty environment variables are reported during pre-flight, before execution.
  • Job logs and --plan output show env:<NAME>, never the value. Resolved secret values and anything matching a connection-string password are redacted (***) from the logfile and diagnostics.

Validation happens before execution

relspec job list and relspec job run both fully validate the selected set first. Nothing is read, written, connected to, or executed if validation fails. Checks include:

  • schema version (must be 1), unknown YAML fields rejected
  • duplicate job names across files
  • unknown / missing command
  • per-command input/output shape (convert/merge need inputs + output; scripts-list needs script_dirs and forbids inputs/output)
  • unknown input/output format
  • path traversal / absolute / home-relative paths
  • depends_on targets exist
  • dependency cycles (reported as a -> b -> c -> a)

Then, immediately before running, per-job pre-flight resolves paths and checks:

  • every input file exists and is a file
  • every script_dir exists and is a directory
  • every conn_env variable is set
  • output.path does not already exist unless output.overwrite: true

If any pre-flight check fails for any job in the plan, no job runs.

Execution and exit codes

  • relspec job run <name> runs the job's depends_on closure first, in topological order (deterministic), then the job. --no-deps runs only the named job.
  • --dry-run (alias --plan) prints the resolved plan and exits 0 without touching inputs, outputs or databases.
  • A failing job returns the underlying non-zero status (the process exits 1) and the error names the job. The logfile records FAILED: <error>; a successful job records OK. No separate success-marker file is written, so a failure can never leave a stale "success".

Schema reference

version: 1                       # required, must be 1
jobs:
  <job-name>:
    command: convert | merge | scripts-list   # required
    description: "free text"                   # optional, shown by `job list`
    depends_on: [other-job, ...]               # optional
    inputs:                                    # convert (≥1) / merge (≥2)
      - path: relative/file.dbml               # file inputs
        format: dbml
      - format: pgsql                          # live-connection inputs
        conn_env: SOURCE_DB_URL                # env var NAME
    script_dirs:                               # scripts-list (≥1)
      - migrations/core
      - migrations/tenant
    output:                                    # convert / merge (required)
      format: pgsql
      path: build/schema.sql                   # file output, OR:
      conn_env: TARGET_DB_URL                  # execute against DB (pgsql only)
      overwrite: false                         # default false
    options:
      flatten_schema: false
      schema: public
      package: models                          # for gorm/bun output
      continue_on_error: false                 # pgsql output
      skip_relations: false                    # merge only
      skip_enums: false
      skip_views: false
      skip_domains: false
      skip_sequences: false
    logfile: .relspec/log/<job-name>.log       # optional; appended to

Supported input formats

dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, sqlite (file, via path); pgsql, mssql (live, via conn_env).

Supported output formats

dbml, dctx, drawdb, graphql, json, yaml, gorm, bun, drizzle, prisma, typeorm, pgsql, mssql, sqlite (file, via path); pgsql also supports conn_env to execute the generated DDL against a live database.

Examples

Merge many schema files, emit PostgreSQL DDL

version: 1
jobs:
  build-schema:
    command: convert
    inputs:
      - { path: schema/core.dbml, format: dbml }
      - { path: schema/billing.dbml, format: dbml }
      - { path: schema/tenant.dbml, format: dbml }
    output:
      format: pgsql
      path: build/schema.sql
      overwrite: true
    logfile: .relspec/log/build-schema.log

Multiple script directories

version: 1
jobs:
  migration-order:
    command: scripts-list
    script_dirs:
      - migrations/core
      - migrations/tenant
      - migrations/reporting
    logfile: .relspec/log/migration-order.log

Job depending on another job

version: 1
jobs:
  build-schema:
    command: convert
    inputs:
      - { path: schema/core.dbml, format: dbml }
      - { path: schema/tenant.dbml, format: dbml }
    output: { format: json, path: build/schema.json, overwrite: true }
  build-docs:
    command: convert
    depends_on: [build-schema]
    inputs:
      - { path: schema/core.dbml, format: dbml }
    output: { format: yaml, path: build/schema.yaml, overwrite: true }

Reading from a remote database

version: 1
jobs:
  snapshot-prod:
    command: convert
    inputs:
      - format: pgsql
        conn_env: PROD_DB_URL      # export PROD_DB_URL=postgres://...
    output:
      format: dbml
      path: snapshots/prod.dbml
      overwrite: true