Files
glint/CLAUDE.md
T
2026-06-07 20:13:03 +02:00

8.1 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

gitlab-sim is a CLI tool that validates and lints .gitlab-ci.yml pipelines locally, without a GitLab server. It resolves extends: inheritance, fetches remote include: project: templates and include: component: catalog entries, then runs a set of lint rules over the fully-merged pipeline.

Goals: catch misconfigured pipelines early in a developer's workflow, before pushing to GitLab. Eventual goal: produce a visual graph of the pipeline DAG.

Non-goals: full GitLab CI emulation, job execution, secret resolution, or runner management.


Architecture

Data flow

model.Parse(path)               // two-pass YAML → *Pipeline
  └─ resolver.ResolveIncludes   // fetch project: / component: includes, merge
       └─ resolver.Resolve       // resolve extends: inheritance chains
            └─ linter.Lint       // run all lint rules → []Finding

This order is intentional and must be preserved: includes must be merged before extends: resolution so that jobs defined in remote templates are available as base templates; extends: must be resolved before linting so that derived jobs carry their full merged definition.

Package responsibilities

Package Role
internal/model Data structures (Pipeline, Job, Rule, …) and YAML parser
internal/linter Lint rules; each rule returns []Finding
internal/resolver extends: resolution and include: merging
internal/fetcher GitLab REST API client (token auth, file fetch)
internal/graph Mermaid graph generators (include dependencies, pipeline jobs)
cmd/gitlab-sim CLI entrypoint, flag parsing, output formatting

Two-pass YAML parser (internal/model/parser.go)

ParseBytes runs two yaml.Unmarshal passes over the same input:

  1. Raw pass — into map[string]yaml.Node to collect every top-level key without type assumptions.
  2. Typed pass — into *Pipeline to decode reserved keys (stages, variables, default, include, workflow, spec) into typed structs.

Keys that survive the raw pass and are not in ReservedKeys are decoded individually as Job structs. This approach handles the open-ended job-name namespace without requiring a catch-all map in the typed struct.

When adding a new top-level reserved key: add it to model.ReservedKeys and add the corresponding typed field to Pipeline.

any fields in Job

Many Job fields (e.g. Artifacts, Cache, Image, Trigger) are typed as any because GitLab CI allows both scalar and map forms. The linter type-asserts these at check time. Follow the existing pattern in internal/linter/keywords.go:

switch v := job.Artifacts.(type) {
case map[string]any:
    // map form
case nil:
    // not set
}

Never change an any field to a concrete struct type unless the GitLab CI spec guarantees only one form — doing so will silently drop the other form during YAML decode.


Adding a New Lint Rule

  1. Add the check function to the appropriate file in internal/linter/:

    • Per-job keyword constraintkeywords.go, called from checkJobKeywords
    • Cross-job graph rule (needs, deps, extends) → dedicated file (needs.go, dependencies.go)
    • Pipeline-level rulelinter.go, called from Lint
  2. Return []Finding with the correct Severity (Error or Warning) and a Job field set when the finding is job-scoped, empty for pipeline-level.

  3. Add a testdata fixture that triggers the new rule, and add it to the validate task in Taskfile.yml.

  4. Document the rule in the lint rules table in README.md.

Finding severity guide

Severity Use when
Error The pipeline will definitely fail or behave incorrectly
Warning Deprecated usage, best-practice deviation, or a condition that may be wrong

Adding a New Model Field

  1. Add the field to model.Job with the correct yaml: tag.
  2. If the field has constrained values, add a validity map to internal/linter/keywords.go (e.g. validJobWhen).
  3. Add the check to checkJobKeywords and call it from there.
  4. Add both a valid and an invalid fixture case to testdata/keywords_valid.yml / testdata/keywords_invalid.yml.

Go Code Style

  • Format: gofmt (enforced via go vet in the ci task). Never commit unformatted code.
  • Imports: group stdlib / external / internal with a blank line between groups.
  • Errors: always wrap with context using fmt.Errorf("doing X: %w", err). Use %w (not %s) to preserve the error chain.
  • No panics in library code (internal/). Panics are acceptable only in main() for truly unrecoverable startup failures — prefer returning errors.
  • No global mutable state. All configuration flows through function parameters or structs (see fetcher.GitLabConfig).
  • Exported symbols must have a doc comment. Unexported helpers do not need one unless the logic is non-obvious.
  • Table-driven tests are the default style for unit tests. Group cases in a []struct{ name, input, want } slice and range over them.
  • Dependencies: prefer the standard library. The only current external dependency is gopkg.in/yaml.v3; keep it that way unless there is a compelling reason.
  • go.sum must always be committed alongside go.mod.

Commit Message Guidelines

IMPORTANT: This project uses Conventional Commits format.

All commit messages must follow this format:

<type>(<scope>): <description>

[optional body]

[optional footer(s)]

Types:

  • feat: A new feature
  • fix: A bug fix
  • docs: Documentation only changes
  • refactor: Code change that neither fixes a bug nor adds a feature
  • test: Adding missing tests or correcting existing tests
  • chore: Changes to build process or auxiliary tools
  • perf: Performance improvements
  • style: Code style changes (formatting, missing semicolons, etc.)

Scopes:

  • linter: changes to lint rules (internal/linter/)
  • model: data structures or YAML parser (internal/model/)
  • resolver: extends/include resolution (internal/resolver/)
  • fetcher: GitLab API client (internal/fetcher/)
  • graph: Mermaid graph generators (internal/graph/)
  • cli: CLI entrypoint (cmd/gitlab-sim/)
  • testdata: fixture files only
  • docs: README, CHANGELOG, or other documentation
  • build: Taskfile, go.mod, go.sum, CI config
  • claude: CLAUDE.md changes

Breaking Changes: Add ! after type/scope for breaking changes (e.g. changed CLI flags, removed output fields):

feat(cli)!: rename --token to --api-token

Note: Always include a scope in parentheses, even for documentation changes.


Development Workflow

task ci          # full check: vet → test → build → validate (run before every commit)
task build       # compile the binary
task test        # run Go unit tests
task lint-go     # go vet only
task validate    # run the binary against all testdata fixtures
task clean       # remove build artifacts

task ci must pass before any commit is created.

Testdata fixtures

Every fixture in testdata/ is run by task validate. Files whose expected behaviour is clean (exit 0) are listed with ignore_error: false; files that are expected to produce errors (exit 1) use ignore_error: true. Keep both categories; do not use ignore_error: true for a fixture that is supposed to be clean.


Environment Variables (for manual testing)

Variable Purpose
GITLAB_TOKEN Personal access token (read_api scope)
CI_JOB_TOKEN CI/CD job token (when running inside a pipeline)
GITLAB_PRIVATE_TOKEN Legacy PAT name (lowest priority)
CI_SERVER_URL GitLab instance URL (takes precedence over GITLAB_URL)
GITLAB_URL GitLab instance URL fallback

Token resolution order (first non-empty wins): GITLAB_TOKENCI_JOB_TOKENGITLAB_PRIVATE_TOKEN. URL resolution order: --gitlab-url flag → CI_SERVER_URLGITLAB_URLhttps://gitlab.com.