Skip to content

feat(config): validate jobs, not just the global section - #778

Merged
CybotTM merged 1 commit into
mainfrom
feat/validate-jobs
Aug 2, 2026
Merged

feat(config): validate jobs, not just the global section#778
CybotTM merged 1 commit into
mainfrom
feat/validate-jobs

Conversation

@CybotTM

@CybotTM CybotTM commented Aug 2, 2026

Copy link
Copy Markdown
Member

Closes #773. Depends on nothing; complements #777, which fixed the daemon half.

The validator never saw a job

It walks structs and skipped maps — and every job lives in one. So an unparsable schedule passed validate with the strict flag on or off, and the only later sign was a warning while the daemon started and the job never fired.

Two things were needed to reach a job's fields:

  • Job sections are maps, so the walk descends into them.
  • Every job type embeds core.<Kind>Job, which embeds BareJob — where schedule and command live. Squashed structs were skipped too, so even after traversing the maps the schedule stayed invisible. They are now descended into only inside a job, so the global section keeps the behaviour it had.

Strict, but not the wrong kind of strict

Applying the existing rule to jobs would have rejected configurations that work today. "A field with no default tag is required" is a heuristic tuned to the global section; on a job it demands nearly every key a job can carry.

So that rule is suppressed inside jobs, and what a job needs is stated explicitly — taken from what the runtime already demands, not invented here:

section required source
job-exec container, command RunExec(ctx, j.Container, …)
job-run image or container core.RunJob.ValidateErrImageOrContainer
job-service-run image core.RunServiceJob.ValidateErrImageRequired
job-local command args.GetArgs(j.Command)
job-compose file, service the docker compose -f … run it builds
all schedule nothing to register the job under otherwise

Errors name the section and the job the user wrote:

config validation error for field 'job-exec "foo": container':
  is required (the container to exec in)

Impact measured, not assumed

Every config in the repo still validates clean: example/ofelia.ini, test/test-config.ini, test/run-job/ofelia.ini. There is an e2e test pinning the shipped example specifically, so adding job checks cannot start rejecting the file new users are handed.

One existing fixture had to change rather than the rule. TestValidateExecuteValidFile declared a job-exec with no container and asserted the config was valid. It is not — verified against the running daemon, that job fails on every tick:

error: job run: exec run: run_exec container "": invalid container name or ID: value is empty

The test was pinning a config that cannot work. This is precisely the class of failure the change exists to catch, so the fixture gained a container.

Note on the constants

command, image, container and schedule are now constants. They were literals in the format switch, which carried a comment arguing that extracting them only relocates duplication — true while they appeared once. The requirements table makes them appear in two independent lists with nothing tying them together, which is what goconst was pointing at.

Test plan

  • go test ./... — green
  • go test -race -tags=e2e ./e2e/... — green
  • golangci-lint run incl. --build-tags="e2e unix" — 0 issues
  • lefthook run pre-push — exit 0
  • All three repo configs validate clean, before and after
  • Each requirement verified to fire, and each valid form verified to pass (including job-run with only a container)

Closes #773.

The validator walked structs and skipped maps. Every job lives in one, so
no job was ever reachable by it: an unparsable schedule passed validate
with the strict flag on or off, and the only later sign was a warning
while the daemon started and the job never fired.

Two things were needed to reach a job's fields. Job sections are maps, so
the walk descends into them; and every job type embeds core.<Kind>Job
which embeds BareJob, where schedule and command live, so squashed
structs are descended into as well. The latter only inside a job — the
global section keeps the behavior it had, and the shipped example, the
run-job example and the test config all still validate clean.

Applying the existing rule to jobs would have been the wrong kind of
strict. "A field with no default tag is required" is tuned to the global
section; on a job it would demand nearly every key a job can carry and
reject configurations that work today. That rule is therefore suppressed
inside jobs, and what a job needs is stated explicitly, taken from what
the runtime already demands rather than invented here:

    job-exec         container, command
    job-run          image or container   (core.RunJob.Validate)
    job-service-run  image                (core.RunServiceJob.Validate)
    job-local        command
    job-compose      file, service
    all              schedule

Errors name the section and the job the user wrote, so "job-exec \"foo\":
container is required (the container to exec in)" points at the line to
fix rather than at a bare key.

One existing fixture had to change rather than the rule: TestValidate-
ExecuteValidFile declared a job-exec with no container and asserted the
config was valid. It is not — verified against the daemon, that job fails
on every single tick with `run_exec container "": invalid container name
or ID: value is empty`. The test was pinning a config that cannot work.

The keys named in both the format switch and the requirements table are
now constants; before the table they existed once, and duplicating them
with nothing tying the two lists together is what goconst was pointing at.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Copilot AI review requested due to automatic review settings August 2, 2026 11:52
@sonarqubecloud

sonarqubecloud Bot commented Aug 2, 2026

Copy link
Copy Markdown

@github-actions github-actions Bot added the tests label Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

✅ Mutation Testing Results

Mutation Score: 100.00% (threshold: 60%)

✨ Good job! Mutation score meets the threshold.

What is mutation testing?

Mutation testing measures test quality by introducing small changes (mutations) to the code and checking if tests detect them. A higher score means better test effectiveness.

  • Killed mutants: Tests caught the mutation (good!)
  • Survived mutants: Tests missed the mutation (needs improvement)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval for maintainer PR

All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.

@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.04110% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.29%. Comparing base (c743097) to head (ff2ee7b).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
config/validator.go 89.04% 4 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #778      +/-   ##
==========================================
- Coverage   89.31%   89.29%   -0.02%     
==========================================
  Files          88       88              
  Lines       12071    12128      +57     
==========================================
+ Hits        10781    10830      +49     
- Misses        995      999       +4     
- Partials      295      299       +4     
Flag Coverage Δ
integration 89.28% <89.04%> (-0.04%) ⬇️
unittests 88.72% <89.04%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a long-standing gap in ofelia validate by ensuring the config validator can actually reach and validate per-job fields (not just the global section). It extends the validator’s reflection walk to traverse job maps and (only within jobs) descend into mapstructure:",squash" embedded structs so that shared fields like schedule/command are validated, and it adds explicit per-job-kind required-field checks aligned with runtime requirements.

Changes:

  • Traverse job section maps during validation so job entries are reachable by the validator.
  • Within job entries only, descend into squashed embedded structs and suppress the global “no default tag ⇒ required” heuristic; instead enforce explicit per-kind job requirements.
  • Add unit + e2e coverage to pin schedule parsing failures and missing required job fields, while ensuring the shipped example config still validates.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
config/validator.go Extends the validator walk to descend into job maps and squashed embedded structs within jobs; adds explicit job requirements and qualified job error paths.
config/validator_jobs_test.go New unit tests covering job traversal, schedule parsing, per-kind requirements, and the “don’t require every field” rule inside jobs.
config/validator_mutation_test.go Updates tests for the new validateStringField signature using fieldCtx.
config/validator_boundary_test.go Updates boundary tests for the new validateStringField signature using fieldCtx.
e2e/config_validation_test.go Adds end-to-end validation tests for unparsable schedules, missing job requirements, and continued acceptance of example/ofelia.ini.
cli/validate_test.go Fixes the test fixture to use a runnable job-exec by adding the missing container field.

@CybotTM
CybotTM added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 54f8577 Aug 2, 2026
38 checks passed
@CybotTM
CybotTM deleted the feat/validate-jobs branch August 2, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

An unparsable schedule passes validate, and the daemon starts with a job that never runs

2 participants