Skip to content

Validators

Validators let a provider check project state after templates have been rendered and the files are in their final form. They are the natural place for rules like:

  • a generated file must include a header
  • a config file must contain a required key
  • the selected Python version matches the repo settings
  • a file is syntactically valid after template output is written

Use validators when the check is about the final file content, not the templating logic itself.

Where validators fit in the provider lifecycle

A validator runs after repolish has resolved the provider state and rendered the output for the current apply run. It does not replace create_file_mappings(); it complements it. A provider can render a file, validate it, and still keep the check separate from the write step.

The important idea is that validation is additive. Providers do not fight for the same destination path. If multiple providers contribute validators for the same file, each validator is executed and the results are shown together in the apply summary.

Registering validators

create_file_validators() is keyed by destination path and then by validator name:

from pathlib import Path

from repolish import BaseContext, BaseInputs, Provider
from repolish.providers.models import (
    FileValidatorOptions,
    FileValidatorSpec,
    ValidationResult,
    ValidationStatus,
)


class Ctx(BaseContext):
    pass


class MyProvider(Provider[Ctx, BaseInputs]):
    def create_context(self) -> Ctx:
        return Ctx()

    def create_file_validators(self, context: Ctx):
        def lint(context: Ctx, path: Path):
            text = path.read_text(encoding='utf-8')
            return ValidationResult(
                status=(
                    ValidationStatus.PASS
                    if text.startswith('generated by repolish')
                    else ValidationStatus.ERROR
                ),
                message='missing generated header',
                path=str(path),
                validator_name='lint',
            )

        def schema(context: Ctx, path: Path):
            text = path.read_text(encoding='utf-8')
            return ValidationResult(
                status=(
                    ValidationStatus.PASS
                    if 'version' in text
                    else ValidationStatus.ERROR
                ),
                message='missing version key',
                path=str(path),
                validator_name='schema',
            )

        return {
            'config.toml': {
                'lint': lint,
                'schema': FileValidatorSpec(
                    fn=schema,
                    options=FileValidatorOptions(enabled=True),
                ),
            }
        }

A validator may be:

  • a bare callable
  • a FileValidatorSpec(fn=..., options=...)

The callable receives the provider context and the resolved file path. It returns ValidationResult.

Validation statuses

The public contract is intentionally typed. ValidationResult.status is a ValidationStatus value, not a loose boolean:

  • ValidationStatus.PASS — the check succeeded
  • ValidationStatus.WARNING — the check ran and reported a warning
  • ValidationStatus.ERROR — the check failed and should block the apply

This allows the UI and the CLI to distinguish pass, warning, and failure clearly. In particular, repolish apply --fail-on-warnings turns warnings into hard failures without conflating them with actual errors.

Existing project files without a render mapping

A validator does not need a matching create_file_mappings() entry. A provider can validate an existing project file that is already present in the repo even if that file is not written by repolish this run.

This is especially useful for repository hygiene checks or project-local rules that are intentionally not generated by the provider.

The summary still shows the file, but it is marked as a non-write state such as:

Standalone
└── p@
    └── ◌ README.md  developer owned
        validators:
          - ✓ lint

This makes the file visible as provider-owned and validated without suggesting it was rendered and written this run.

Disabling validators via repolish.yaml

Project-level overrides are the final authority. A repo can disable a validator without deleting the provider declaration, which keeps the disabled state visible in the summary:

providers:
  my_provider:
    provider_root: ./my_provider
    overrides:
      validators:
        config.toml:
          lint: false
          schema: true

This is useful when a provider ships a default-on check but the project needs to temporarily opt out or quietly keep a check in the registry without executing it.

Paused files skip validation entirely

If a destination file is listed in paused_files, repolish skips that file before rendering or validation. In that mode the file is shown as paused, and the validator list is omitted because the validators never ran.

This is intentional: a paused file is explicitly out of scope for the current run, so its validator results are not meaningful until the file is unpaused.

Multiple providers on the same file

Validator registries are additive. Different providers may each contribute validators for the same destination file, and all of them are shown together in the summary:

return {
    'config.toml': {
        'header': header,
        'endline': endline,
    }
}

No provider overrides the other. They are co-equal checks, each independently reported.

Example summary output

When repolish apply runs, validators are rendered beneath the affected file as separate lines:

config.toml
  validators:
    - ✗ lint: missing generated header
    - ⚠ schema: configuration is deprecated
    - ✓ header

This keeps the signal explicit:

  • green check mark: validator passed
  • red cross: validator failed
  • yellow warning: validator produced a warning
  • yellow cross: validator exists but is disabled by config
  • hollow marker: the file is provider-owned but no file was staged for it

Validator reports

For each validated file, repolish writes a report artifact:

.repolish/_/validators/validators.<path-slug>.<provider-alias>.json

Report fields include:

  • file
  • provider_alias
  • total_validators
  • failed
  • warnings
  • disabled
  • entries

Every registered validator gets an entry — passes included — with its name, status, and message. When a validator raises an exception instead of returning a result, the entry also carries the traceback as list[str] (one line per entry) for easier reading in JSON. Validators disabled before running appear with kind: disabled.

The summary tree links each validators: block to its report via a [details] hyperlink (when the terminal supports them), so a failing run can be debugged from the report without re-running with a debugger attached.

These files are the detailed record behind the compact summary tree output.