CI Pipeline Configuration Linter
Build a command-line linter in Scala for CI pipeline configuration files, similar in spirit to GitLab CI's YAML format. It reads pipeline files that define stages and jobs, where each job belongs to a stage and can depend on other jobs through needs. It then reports mistakes before they reach the CI server: unknown stages, missing required fields, jobs depending on jobs that don't exist or run in a later stage, and circular dependencies (A needs B needs C needs A). Every error points to the file and line number. You'll learn to parse YAML with SnakeYAML while keeping line information, turn it into typed case classes, collect many errors instead of stopping at the first, and detect cycles in a graph with depth-first search.
- Estimate
- ~14.5h
- Steps
- 7
- Completed by
- 0
- Proposed by
- codeseed.app
sbt · snakeyaml
Project roadmap
- 01
Set up the project and fixtures
~1.5hCreate an sbt project with Scala 3, adding SnakeYAML and a test library (MUnit or ScalaTest). Define the format in the README: a top-level stages list, and jobs as top-level keys, each with stage (required), script (a required list of commands) and optional needs (a list of job names). Add example pipelines in fixtures/: one valid and several invalid ones. Done when: sbt compile works and the fixtures are in the repo.
- 02
Parse YAML with line numbers
~3hUse SnakeYAML's compose method, which returns a tree of Nodes, instead of load, so every value keeps its position through getStartMark (line and column). Convert the tree into case classes: Pipeline(stages, jobs) and Job(name, stage, script, needs, line), each field keeping its line. A file that isn't valid YAML becomes one error with SnakeYAML's position. Done when: the valid fixture parses into case classes with correct line numbers.
- 03
Validate structure and references
~2.5hWrite rules that each return a list of errors: every job has a stage and a non-empty script, each job's stage is listed in stages, each entry in needs names an existing job, and a job never needs a job in a later stage. Collect errors from all rules instead of stopping at the first, for example with Either and ValidatedNel-style accumulation or plain lists. Done when: each invalid fixture produces exactly the expected errors.
- 04
Detect circular dependencies
~2.5hBuild a graph from job names to their needs and run depth-first search, marking each node unvisited, in progress or done. Reaching a node that's in progress means a cycle. Report the full cycle path (for example: build -> test -> package -> build) with the line of the first job, and report each distinct cycle once. Done when: a fixture with two separate cycles reports both, and the valid fixture reports none.
- 05
Report errors clearly
~1.5hPrint errors sorted by file and line, like pipelines/ci.yml:14:3: error: job test needs unknown job biuld. Exit with code 1 if there are errors and 0 otherwise. Add --format json for machine-readable output. Done when: the output is sorted, readable, and the exit code matches.
- 06
Lint whole directories
~1.5hAccept any number of files and directories as arguments. Search directories for .yml and .yaml files, lint each one independently so an error in one file doesn't stop the others, and finish with a summary like 3 files checked, 2 with errors, 5 errors. Done when: running the linter on fixtures/ reports every invalid file and the summary is correct.
- 07
Write tests
~2hWrite tests for each rule, for cycle detection (no cycle, a self-dependency, a long cycle, two cycles), for line numbers in errors, and for invalid YAML. Run the whole CLI against the fixtures and compare its output with expected files. Done when: sbt test passes.
Resources
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack