rrt doctor
rrt doctor
Section titled “rrt doctor”Validate the core automation health of the resolved rrt configuration.
Overview
Section titled “Overview”rrt doctor is the basics-first repository health check. It focuses on the
shared automation wiring around the resolved configuration — local hooks, CI
workflows, and guidance to the feature-specific checks that own deeper policy
validation.
What it checks
Section titled “What it checks”The command checks the automation surfaces that tell you whether repository basics are wired correctly:
.pre-commit-config.yamlwhen presentlefthook.ymlwhen present.husky/*hook scripts when present.github/workflows/*.yml/.yamlwhen present- CI config for cross-pipeline artifact fetches, cross-checked against
[tool.rrt.artifact_protection]— see Artifact protection below
The checks are intentionally light-touch: they verify presence, readability, and whether the file appears to reference repo-release-tools policy checks. They do not replace the deeper feature validators.
Artifact protection
Section titled “Artifact protection”Some CI jobs fetch a build artifact produced by a different pipeline run — for example, a docs job that pulls a report built by an earlier release pipeline. That dependency lives only in the consuming job’s config, never on the artifact itself, so nothing on the forge side (GitLab, GitHub) can infer it exists — a storage cleanup or retention policy has no way to know the artifact is still needed. When the artifact expires, the consuming job breaks. A consumer that instead degrades gracefully — skips the missing artifact and carries on rather than failing loudly — is not the safer outcome: it hides the same broken dependency instead of surfacing it, so graceful degradation and silent breakage are the same failure underneath, just harder to notice.
rrt doctor closes this gap by scanning CI config for cross-pipeline
artifact fetches — steps that download a build artifact produced by a
different pipeline run — and cross-checking each one against an optional
[tool.rrt.artifact_protection] declaration, where consumed_by records the
CI file that performs the fetch. (This is unrelated to rrt artifacts, which
tracks content-addressed integrity of generated files you keep in the repo —
a different feature that happens to share a name fragment.)
The scanner recognizes two forms:
- GitLab
.../jobs/artifacts/<ref>/raw/<path>?job=<name>URLs, matched in every CI file the scanner reads —.gitlab-ci.yml, any.gitlab/**/*.yml/*.yamlfile (scanned recursively, since GitLab’sinclude: local:accepts any filename in any subdirectory), and also.github/workflows/*.yml/*.yaml. This is deliberate: a job running on GitHub Actions can stillcurlan artifact out of a GitLab instance, and scoping the check to.gitlab*files would make that fetch invisible. A single physical line can carry more than one fetch (for example chained shell commands,curl ... && curl ...) — every fetch on the line is found, not just the first. - GitHub Actions
actions/download-artifact@…steps that carryrun-id:in.github/workflows/*.yml/*.yaml(same-pipeline downloads withoutrun-idare ignored — they are not cross-pipeline). A step that carriesrun-id:but noname:— standard GitHub usage meaning “download every artifact from this run”, common in publish/release jobs — is still recorded, not dropped: see “Download-all fetches” below. GitLab’s same-pipeline equivalent — a job declaringneeds: [other_job]withartifacts: trueto pull that job’s output within the same pipeline — is out of scope for the same reason: those artifacts come from the same pipeline run and are never at risk from a retention cleanup of other pipelines, so the scanner does not look forneeds:at all.
Declare what each fetch is allowed to depend on:
[tool.rrt.artifact_protection]protected_refs = ["main"] # reserved: parsed but not yet enforced, see below
[[tool.rrt.artifact_protection.consumed]]job = "build:report_html"ref = "main"artifacts = ["public/report.html"]consumed_by = [".gitlab/65-docs.yml"]reason = "Docs pipeline embeds the report built by this job."protected_refs above is shown for illustration only — it is parsed and
validated but not read by any check yet (see “protected_refs is reserved,
not yet enforced” below); do not rely on it to protect main. Each
consumed entry: job and ref identify the fetch (see “Matching” below
for exactly how); artifacts lists the paths the fetch may pull;
consumed_by names the CI file(s) that perform the fetch; reason records
why the dependency exists. consumed_by and reason are documentation
only — nothing in the check validates or enforces them; matching uses only
job, ref, and (when known) path.
The check fails (error) when a scanned fetch has no matching consumed
entry — the failure message names the CI file and line and prints a
ready-to-paste [[tool.rrt.artifact_protection.consumed]] block. It warns
(without failing) when a consumed entry matches no scanned fetch, since a
stale declaration that protects nothing real trains people to ignore the
check; both directions can be reported together in the same run. When no
cross-pipeline fetches are found in CI config and no
[tool.rrt.artifact_protection] block is configured, the check is a soft
warning rather than a failure — there is nothing to protect yet.
Matching: (job, ref), plus path when known
Section titled “Matching: (job, ref), plus path when known”A fetch is declared only when a consumed entry matches on both job
and ref — never job alone. Matching on job alone would let one
correctly-filled entry mask an unrelated, undeclared fetch anywhere else in
CI config that merely happens to share its job string (generic names like
build, report, dist, coverage are common, and GitLab’s job holds a
job name while GitHub’s holds an artifact name — two namespaces sharing one
match field). Neither job nor ref is ever split, lowercased, or otherwise
normalized — job names may legitimately contain colons (for example
build:report_html:bundle, which is a distinct job from build:report_html).
When the fetch’s path is known — GitLab always sets it — the matched
entry’s artifacts list must additionally contain that exact path, or the
fetch still counts as undeclared. Declaring “job X at ref Y protects
artifacts [a, b]” must not silently cover a fetch of a different artifact
c from that same job and ref. GitHub Actions download-artifact exposes no
per-file path (path is always None), so for GitHub fetches the check
necessarily degrades to (job, ref) only — the best available for that
provider.
GitHub vs. GitLab: job means different things
Section titled “GitHub vs. GitLab: job means different things”job is part of the join key, but what it identifies depends on the
provider:
- GitLab — the real job name, taken from the fetch URL’s
?job=parameter. - GitHub Actions —
actions/download-artifactexposes no job identifier at all, so the scanner records the artifact’sname:field instead.
When you write a consumed entry for a GitHub Actions fetch, put the
artifact name in job, not a job or workflow name — otherwise the entry
will never match, and rrt doctor will keep reporting the fetch as
undeclared no matter what you write in job.
Download-all fetches (no name:)
Section titled “Download-all fetches (no name:)”actions/download-artifact with run-id: but no name: downloads every
artifact produced by that run — a common pattern in publish/release jobs. The
scanner cannot name a single artifact for it, so it records the fetch with
job set to the literal marker "*" (GitHub forbids ", :, <, >,
|, *, ?, CR, and LF in real artifact names, so "*" can never collide
with one). Declare it the same way as any other GitHub fetch, using the
marker verbatim:
[[tool.rrt.artifact_protection.consumed]]job = "*"ref = "12345"artifacts = ["TODO: list the artifact-relative path(s) this job consumes"]consumed_by = [".github/workflows/publish.yml"]reason = "Publish job downloads every artifact from the referenced run."Unedited TODO placeholders mean fictional protection
Section titled “Unedited TODO placeholders mean fictional protection”When rrt doctor finds an undeclared fetch, it prints a ready-to-paste
[[tool.rrt.artifact_protection.consumed]] block. For GitHub Actions
fetches, the artifact-relative path cannot be inferred from
download-artifact alone, so the suggested artifacts value is a
placeholder:
artifacts = ["TODO: list the artifact-relative path(s) this job consumes"]This block loads successfully as written. Nothing in config loading
requires artifacts entries to point at real paths. For GitLab fetches the
check verifies the declared path appears in artifacts (see the matching
rules above), so a GitLab TODO placeholder is caught immediately. For GitHub
fetches path is always unknown, so the check cannot verify it — an
unedited GitHub TODO placeholder loads cleanly and the very next rrt doctor
run will report that fetch as satisfied, even though nothing is actually
protected. Always replace both the artifacts TODO and the reason TODO
with real values before trusting a green result — an unedited placeholder
is fictional protection, not real protection.
protected_refs is reserved, not yet enforced
Section titled “protected_refs is reserved, not yet enforced”protected_refs is parsed, validated, and schema-checked, but no check in
rrt doctor currently reads it. Setting protected_refs = ["main"] records
your intent but enforces nothing today — it does not, by itself, block
anything from being deleted or flag any ref as protected. Treat it as
reserved for a future check; do not rely on it to harden anything yet.
Output and severity
Section titled “Output and severity”The command prints one grouped report for the core automation surfaces and an overall status at the end.
- unreadable automation files are errors
- missing hook-manager surfaces are obsolete when another hook manager is active
- missing optional integration surfaces are warnings when no equivalent surface is active
- surfaces that exist but do not appear to reference repo-release-tools are warnings
- readable, recognized surfaces are reported as OK
- undeclared cross-pipeline artifact fetches are errors; stale
consumedentries are warnings (see Artifact protection)
At the end, rrt doctor also points you to the feature-specific commands that
own deeper validation, such as rrt release check, rrt docs check, and
rrt eol.
Config discovery behavior
Section titled “Config discovery behavior”If no config file can be found, the command prints repository guidance and exits with an error.
If a config is auto-detected, the command emits a notice on stderr before the main report so you can tell that rrt did not use an explicitly selected file.
Examples
Section titled “Examples”rrt doctorCaveats
Section titled “Caveats”- The command reports core automation health for the resolved configuration, not just the visible file in the current directory.
- Feature-specific checks belong to their own surfaces:
rrt release check,rrt docs check, andrrt eol. - A warning does not fail the command; only error-level findings do.
Related docs
Section titled “Related docs”Badge families are intentionally complete for the current icon registry across platform, registry, and language labels; see src/repo_release_tools/tools/platform.py if you think one is missing.
Chat is powered by Context7, a third-party service with its own terms and privacy policy.