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 succeededValidationStatus.WARNING— the check ran and reported a warningValidationStatus.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:
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:
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:
Report fields include:
fileprovider_aliastotal_validatorsfailedwarningsdisabledentries
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.