Problem
Renovate now manages version numbers inside files that puppetsync also manages (Gemfile gem pins, uses: actions/checkout@vN refs in GHA workflows, metadata.json dependencies). The profiles enforce those files with file { content => file(...)/epp(...) }, which overwrites unconditionally — so every puppetsync run clobbers Renovate's updates back to whatever version the template was written with.
Proposed design: split template management into two stages
Stage 1 — bootstrap (lay down complete file, only if missing)
Puppet's file resource already supports this via replace => false. Classify each managed file:
- Fully-managed (no version content —
.gitignore, .rspec, _puppet-lint.rc, …): keep overwriting as today.
- Bootstrap + merge (
Gemfile, GHA workflows, metadata.json, …): add replace => false so the template only seeds new repos.
Express the classification as a profile parameter (or a small profile::managed_file define) so Hiera can flip a file between modes per project type. No new pipeline stage needed — bootstrap stays inside apply_puppet_role.
Stage 2 — merge (update existing files in place, preserving Renovate-managed values and comments)
New merge_managed_files pipeline stage in plans/init.pp after apply_puppet_role, backed by one task per file family:
merge_yaml.rb — load the existing file with psych-pure (comment/format-preserving YAML), deep-merge the template's structure into it with a declared list of preserved paths (e.g. jobs.*.steps.*.uses), write back. Add psych-pure to gem.deps.rb and the install_gems stage.
merge_gemfile.rb — generalize the line-regexp editing already proven in tasks/configure_renovate.rb: ensure required gem lines/blocks exist, but never touch an existing version constraint.
- JSON (
metadata.json, renovate.json) needs no comment preservation; JSON.parse + pretty_generate as today.
Drive the merge specs from Hiera, keyed off the existing project_types hierarchy — e.g. data/project_types/pupmod.yaml gains a puppetsync::merge_rules hash mapping file path → {type: yaml, preserve: [...]}. The "what" stays in data, the "how" in tasks.
Prior art in this repo
profile::pupmod::gitlab_ci already implemented read-existing-file → extract repo-specific content → re-merge into template.
tasks/configure_renovate.rb already does line-oriented Gemfile edits and JSON merging.
Risks
- psych-pure is young: before trusting it in the pipeline, add a round-trip test that parses and re-emits every workflow template under
modules/profile/files/ and diffs the result. It must also install/run under Bolt's bundled Ruby.
- Workflow restructuring is the hard semantic: when a template changes a job's steps, "preserve existing
uses: refs" gets ambiguous. Proposed resolution: keep the existing ref when the same step still exists; take the template's ref for genuinely new steps.
Migration
Existing repos already have all the files, so bootstrap no-ops everywhere and the merge stage becomes the workhorse immediately.
Problem
Renovate now manages version numbers inside files that puppetsync also manages (
Gemfilegem pins,uses: actions/checkout@vNrefs in GHA workflows,metadata.jsondependencies). The profiles enforce those files withfile { content => file(...)/epp(...) }, which overwrites unconditionally — so every puppetsync run clobbers Renovate's updates back to whatever version the template was written with.Proposed design: split template management into two stages
Stage 1 — bootstrap (lay down complete file, only if missing)
Puppet's
fileresource already supports this viareplace => false. Classify each managed file:.gitignore,.rspec,_puppet-lint.rc, …): keep overwriting as today.Gemfile, GHA workflows,metadata.json, …): addreplace => falseso the template only seeds new repos.Express the classification as a profile parameter (or a small
profile::managed_filedefine) so Hiera can flip a file between modes per project type. No new pipeline stage needed — bootstrap stays insideapply_puppet_role.Stage 2 — merge (update existing files in place, preserving Renovate-managed values and comments)
New
merge_managed_filespipeline stage inplans/init.ppafterapply_puppet_role, backed by one task per file family:merge_yaml.rb— load the existing file with psych-pure (comment/format-preserving YAML), deep-merge the template's structure into it with a declared list of preserved paths (e.g.jobs.*.steps.*.uses), write back. Addpsych-puretogem.deps.rband theinstall_gemsstage.merge_gemfile.rb— generalize the line-regexp editing already proven intasks/configure_renovate.rb: ensure requiredgemlines/blocks exist, but never touch an existing version constraint.metadata.json,renovate.json) needs no comment preservation;JSON.parse+pretty_generateas today.Drive the merge specs from Hiera, keyed off the existing
project_typeshierarchy — e.g.data/project_types/pupmod.yamlgains apuppetsync::merge_ruleshash mapping file path →{type: yaml, preserve: [...]}. The "what" stays in data, the "how" in tasks.Prior art in this repo
profile::pupmod::gitlab_cialready implemented read-existing-file → extract repo-specific content → re-merge into template.tasks/configure_renovate.rbalready does line-oriented Gemfile edits and JSON merging.Risks
modules/profile/files/and diffs the result. It must also install/run under Bolt's bundled Ruby.uses:refs" gets ambiguous. Proposed resolution: keep the existing ref when the same step still exists; take the template's ref for genuinely new steps.Migration
Existing repos already have all the files, so bootstrap no-ops everywhere and the merge stage becomes the workhorse immediately.