Skip to content

feat: include templates by file path and bind them to a profile name - #521

Open
berique wants to merge 1 commit into
raydak-labs:mainfrom
berique:feat/518-profile-includes
Open

berique wants to merge 1 commit into
raydak-labs:mainfrom
berique:feat/518-profile-includes

Conversation

@berique

@berique berique commented Sep 5, 2026

Copy link
Copy Markdown

Closes #518.

Problem

The issue asks to keep a quality profile in its own file and share it across instances, independent of base_url/api_key and of the profile name.

Today that isn't quite possible:

  • include: accepts a Recyclarr/local template name, a TRaSH trash_id, or an http(s) URL — never a filesystem path.
  • Local templates live in a single non-recursive localConfigTemplatesPath directory keyed by basename, so profiles/radarr/uhd.yml is unreachable, names are global across arr types, and collisions silently overwrite.
  • YAML anchors only work within one config.yml, so they can't share anything with a separate file.
  • The profile name still has to be hardcoded in the file. renameQualityProfiles/cloneQualityProfiles only patch it up afterwards — which is exactly the coupling the issue wants removed.

Changes

1. Include a template by file path

radarr:
  movies:
    include:
      - template: ./profiles/radarr/uhd.yml
      - template: /data/profiles/shared/audio.yml
      - template: ./profiles/radarr/trash-profile.json # TRaSH detected automatically

New src/file-template-importer.ts, modelled directly on the existing url-template-importer.ts (same shape, same "log and return null" contract, synchronous).

  • Relative paths resolve against the directory holding config.yml, never the working directory.
  • Recyclarr YAML and TRaSH JSON both accepted; a file with a trash_id is detected as TRaSH, so source: TRASH is optional (an improvement over URL includes, which still require it).
  • Backwards compatible by construction: a value is only treated as a path after it has been ruled out against the Recyclarr, local and TRaSH template maps, so anything that resolves today keeps resolving identically. A regression test pins this.
  • Processed last among include sources, since an explicit path is the most direct reference a user can write.

2. profiles: — bind a name-free template to a name

radarr:
  movies:
    profiles:
      - name: UHD
        includes: ./profiles/radarr/quality.yml
      - name: HD
        includes: ./profiles/radarr/quality.yml # same file, second name
# profiles/radarr/quality.yml — no profile name anywhere
custom_formats:
  - trash_ids: [496f355514737f7d83bf7aa4d24f8169] # TrueHD Atmos
    assign_scores_to:
      - score: 5000

Each entry is resolved in isolation through the existing include pipeline into a scratch buffer, then its names are bound and it is merged normally. Because it reuses that pipeline, includes accepts every include form — file, URL, Recyclarr name, TRaSH id — not just files.

Binding rules:

In the included template Result
No quality profile Only custom format scores are bound
Exactly one quality profile Renamed to name, whatever it was called
More than one quality profile Entry skipped with a warning
Custom format with no assign_scores_to An assignment to name is created
Any assign_scores_to entry Its name is set to name; scores preserved

Score assignments are rebound even when they already name a profile — that is what lets an unmodified upstream template be reused under a name of your choosing. It's unambiguous because an entry resolving to more than one quality profile is rejected, so there is never a second legitimate target.

Ordering: after include:, before the instance's own custom_formats/quality_profiles (so instance config still wins), and before renameQualityProfiles/cloneQualityProfiles (so a bound profile can still be renamed or cloned).

3. Drive-by fix

mergeAndReduceCustomFormats now drops a score assignment with no profile name instead of creating a quality profile literally named undefined. Template files bypass schema validation entirely (yaml.parse(...) as MappedTemplates), so this was already reachable before this PR.

Notes for review

  • Two decisions I'd particularly like a second opinion on: rebinding already-named score assignments (rationale above), and the key being includes: — it matches the issue text, but every other key in the codebase is singular include:. Happy to rename.
  • Nested include: inside a profile file still warns and is ignored, as before. Proper recursion needs cycle detection and a decision on what relative paths inside an included file resolve against — worth its own issue.
  • Telemetry gains file_templates and config_profiles flags.

Testing

pnpm build && pnpm test && pnpm lint && pnpm typecheck all pass — 449 tests, 35 new.

  • src/file-template-importer.test.ts (new, 17 tests): path detection, config-relative resolution, and every load failure mode.
  • src/config.test.ts: file includes (relative, absolute, TRaSH JSON, missing file, template-key-wins regression) and profiles: (nameless binding, synthesised assignments, rebinding, >1 profile skip, one file under two names, Recyclarr template in a profile, interaction with rename, no profiles leak into the merged config).

Also verified outside the test suite, running from a different working directory: one nameless file bound to two names produced two independent profiles with correct scores, confirming config-dir path resolution.

Docs updated in config-file.md (new "File Templates" and "Reusable Profiles" sections), general.md (merge order), the config sample, and a working example under examples/full/config/profiles/.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HVQLWA3hsfKjrX4YsaPk6C

Summary by Sourcery

Support reusable, name-independent profiles and direct template-file includes while preserving existing template resolution behavior.

New Features:

  • Add experimental support for including Recyclarr and TRaSH templates directly from config-relative or absolute file paths.
  • Add experimental reusable profiles that bind name-independent templates to configurable profile names and allow one template to be shared under multiple names.

Bug Fixes:

  • Prevent nameless score assignments from creating a quality profile named undefined during template merging.

Enhancements:

  • Reuse the existing include resolution pipeline for reusable profiles and preserve compatibility by prioritizing known template names over file-path interpretation.
  • Expose telemetry for file-template and reusable-profile usage.

Documentation:

  • Document file-template includes, reusable profiles, merge ordering, configuration examples, and a working reusable-profile example.

Tests:

  • Add coverage for file loading, format detection, config-relative resolution, failure handling, profile binding, score rebinding, multiple-name reuse, and merge-order interactions.

Closes raydak-labs#518.

Adds two related ways to keep a quality profile in its own file and share it:

1. `include:` now accepts a filesystem path, alongside template names and URLs:

       include:
         - template: ./profiles/radarr/uhd.yml

   Relative paths resolve against the directory holding config.yml (not the
   working directory). Recyclarr YAML and TRaSH JSON are both accepted, with
   TRaSH detected automatically from `trash_id`. A value that matches a known
   Recyclarr/local/TRaSH template key still resolves as that template, so no
   existing config changes meaning.

2. A new instance-level `profiles:` block binds a name-free template to a name:

       profiles:
         - name: UHD
           includes: ./profiles/radarr/quality.yml

   The included file names nothing; the profile name comes from the config, so
   the same file can be reused across instances under different names. Each
   entry is resolved in isolation and accepts every include form, then its
   quality profile and all custom format score assignments are bound to `name`.
   An entry resolving to more than one quality profile is skipped with a
   warning, since there would be no single name to bind.

Previously this was only possible via `localConfigTemplatesPath`, a single flat
directory keyed by basename, and the profile name still had to be hardcoded in
the file - which `renameQualityProfiles`/`cloneQualityProfiles` could only patch
up after the fact.

Also hardens `mergeAndReduceCustomFormats` against a score assignment with no
profile name, which template files could already produce (they bypass schema
validation) and which previously created a quality profile named "undefined".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVQLWA3hsfKjrX4YsaPk6C
@sourcery-ai

sourcery-ai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds config-relative file-path includes for Recyclarr and TRaSH templates, plus reusable profiles: definitions that resolve any include source in isolation and rebind its profile and score assignments to a chosen name; also hardens nameless assignment merging, telemetry, documentation, examples, and regression coverage.

Sequence diagram for config-relative template and reusable profile resolution

sequenceDiagram
    participant Config as Config merger
    participant Maps as Template maps
    participant File as File template importer
    participant Scratch as Profile scratch buffer
    participant Merge as Merged config

    Config->>Maps: Resolve include template keys
    alt Known template key
        Maps-->>Config: Return mapped template
    else File path
        Config->>File: loadTemplateFromFile(template)
        File-->>Config: Return Recyclarr or TRASH template
    end
    Config->>Merge: Apply include template

    opt profiles entry
        Config->>Scratch: Resolve profile.includes in isolation
        Scratch->>Maps: Resolve all include forms
        Maps-->>Scratch: Populate scratch templates
        Config->>Scratch: bindProfileName(name)
        Scratch-->>Config: Rebound profile and score assignments
        Config->>Merge: Merge bound profile
    end

    Config->>Merge: Apply instance configuration
Loading

Flow diagram for reusable profile binding

flowchart TD
    A[profiles entry: name + includes] --> B[Resolve includes in scratch buffer]
    B --> C{Quality profiles resolved}
    C -->|More than one| D[Warn and skip entry]
    C -->|Zero| E[Bind custom format scores only]
    C -->|Exactly one| F[Rename profile to configured name]
    E --> G[Rebind or create score assignments]
    F --> G
    G --> H[Merge into instance config]
    H --> I[Apply instance config and later rename or clone operations]
Loading

File-Level Changes

Change Details Files
Add config-relative filesystem template loading with format detection and backward-compatible include precedence.
  • Detect path-like include values only after known Recyclarr, local, and TRaSH keys are checked.
  • Load YAML/JSON files synchronously, auto-detect TRaSH documents by trash_id, and log-and-skip invalid or missing files.
  • Process file includes after existing include sources and document path behavior and precedence.
src/file-template-importer.ts
src/file-template-importer.test.ts
src/config.ts
src/config.test.ts
docs/docs/configuration/config-file.md
docs/docs/configuration/general.md
docs/docs/configuration/_include/config-file-sample.yml
Introduce reusable profiles entries that resolve includes independently and bind their contents to a configured profile name.
  • Normalize string, list, and full include-item shorthands under profiles[].includes.
  • Resolve each profile entry through the existing include pipeline using a scratch merge buffer.
  • Bind a single quality profile and all score assignments to the configured name, synthesize missing assignments, and skip ambiguous entries.
  • Merge reusable profiles after include: and before instance-level definitions, preserving later rename/clone behavior and preventing profiles from leaking into merged config.
src/config.ts
src/config.test.ts
src/types/config.types.ts
src/types/common.types.ts
docs/docs/configuration/config-file.md
docs/docs/configuration/general.md
docs/docs/configuration/_include/config-file-sample.yml
examples/full/config/config.yml
examples/full/config/profiles/radarr-audio.yml
Prevent nameless template score assignments from creating an undefined quality profile.
  • Ignore score assignments without a profile name during custom-format reduction and emit a warning.
  • Add regression coverage for orphan assignments from file templates.
src/config.ts
src/config.test.ts
Expose usage telemetry for filesystem templates and reusable config profiles.
  • Add file_templates and config_profiles telemetry fields and populate them from parsed instance configuration.
  • Update telemetry expectations.
src/telemetry.ts
src/telemetry.test.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#518 Allow configuration templates, including reusable profiles, to be stored in separate YAML/JSON files and included by relative or absolute filesystem path.
#518 Allow a name-independent profile template to be bound to an arbitrary profile name per instance, including reusing the same file under multiple names and preserving or creating custom-format score assignments.
#518 Document the new file-template and reusable-profile configuration syntax and behavior.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/config.ts" line_range="736-739" />
<code_context>
+    // managed elsewhere (or already present on the server).
+    logger.debug(`Profile '${name}': includes resolve to no quality profile. Binding custom format scores only.`);
+  } else {
+    if (qualityProfile.name !== name) {
+      logger.info(`Profile '${name}': bound included quality profile '${qualityProfile.name}' -> '${name}'.`);
+    }
+    qualityProfile.name = name;
+  }
+
</code_context>
<issue_to_address>
**issue (broader_impact):** Binding a profile that comes from a Recyclarr or local template mutates the objects stored in the shared template map. When two `profiles` entries reuse the same named template, binding the second entry overwrites the quality-profile name and score-assignment names already appended for the first entry, so both entries end up under the second name.

**Triggers:** When multiple `profiles` entries reuse the same Recyclarr or local template name under different configured names.

**Suggested fix:** Deep-clone the resolved template before placing it in the scratch buffer or before rebinding it.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and a path or profile-binding bug could apply the wrong quality profile or custom-format scores to the configured Radarr/Sonarr instance. Reverting prevents future application, but settings already written to those instances would remain and require a rerun or manual repair.

Blocking findings: src/config.ts:739


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/config.ts
Comment on lines +736 to +739
if (qualityProfile.name !== name) {
logger.info(`Profile '${name}': bound included quality profile '${qualityProfile.name}' -> '${name}'.`);
}
qualityProfile.name = name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (broader_impact): Binding a profile that comes from a Recyclarr or local template mutates the objects stored in the shared template map. When two profiles entries reuse the same named template, binding the second entry overwrites the quality-profile name and score-assignment names already appended for the first entry, so both entries end up under the second name.

Triggers: When multiple profiles entries reuse the same Recyclarr or local template name under different configured names.

Suggested fix: Deep-clone the resolved template before placing it in the scratch buffer or before rebinding it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Sugestion] Include profile

1 participant