Skip to content

Add bootstrap mode for managed baseline files (#50 slice 1) - #70

Merged
silug merged 2 commits into
simp:mainfrom
silug:issue-50-bootstrap-mode
Jul 31, 2026
Merged

Add bootstrap mode for managed baseline files (#50 slice 1)#70
silug merged 2 commits into
simp:mainfrom
silug:issue-50-bootstrap-mode

Conversation

@silug

@silug silug commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

First slice of #50 (Renovate-resilient templates): split baseline-file management into two modes so puppetsync stops clobbering values that Renovate maintains in place. Slices 2/3 (the in-place merge tasks for Gemfile and YAML) build on this.

Changes

  • New profile::managed_file define — wraps the baseline file pattern with a mode parameter:
    • enforce (default): content fully managed; local changes overwritten — the previous behavior for every file
    • bootstrap: lay down the complete file only when it doesn't exist (replace => false), for files whose values are maintained in place after creation
  • All file-managing profiles converted (gemfile, git_files, puppet_lint, pdkignore, rspec, github_actions), each exposing a Hiera-overridable mode parameter — so classification lives in data when it needs to vary per project type.
  • Classification in this slice:
    • Gemfilebootstrap (its pins are actively managed by Renovate via simp/renovate-config:ruby; this is the file where clobbering hurts today)
    • Everything else → enforce, including GHA workflows: puppetsync is still the delivery mechanism for workflow changes (e.g. Add a REFERENCE.md freshness check to pr_tests.yml (bundle exec rake validate:strings) #46), so they stay fully managed until the YAML merge stage lands — at which point they flip via Hiera. The trade-off (an enforce sync can revert Renovate's uses: bumps until then) is documented on the class.

No CHANGELOG.md update in this PR, per the new convention discussion (avoiding the every-PR conflict magnet).

Verification

End-to-end through the real apply_puppet_role stage against a local file:// pupmod fixture (temp config, not committed), with role::pupmod classified from Hiera via project_type:

  • Fixture pre-seeded with a custom Gemfile (standing in for Renovate-managed state) and a drifted .gitignore
  • After the sync: the Gemfile survived byte-for-byte untouched; .gitignore was re-baselined; all missing baseline files were created (5 workflows, .pdkignore, .puppet-lint.rc, .rspec, spec/spec_helper.rb); the sync commit contained 11 files — not the Gemfile
  • A second fixture without a Gemfile got the baseline Gemfile created (bootstrap still seeds new repos)

Also green: puppet parser validate over all changed manifests, bolt plan show, the list_pipeline_stages dry run, the CI idempotency e2e, and rspec (154 examples, 0 failures).

Refs #50

🤖 Generated with Claude Code

First slice of the Renovate-resilient template work: split file
management into two modes so puppetsync stops clobbering values that
Renovate maintains in place.

- New profile::managed_file define wrapping the baseline file pattern
  with a mode parameter: 'enforce' (fully managed content, the previous
  behavior) or 'bootstrap' (lay down the complete file only when it
  doesn't exist, via replace => false)
- All file-managing profiles (gemfile, git_files, puppet_lint,
  pdkignore, rspec, github_actions) now use the define and expose a
  Hiera-overridable mode parameter
- Classification: the Gemfile defaults to 'bootstrap' (its gem pins are
  actively Renovate-managed); everything else stays 'enforce'. GHA
  workflows stay 'enforce' for now since puppetsync is still the
  delivery mechanism for workflow changes — they flip to bootstrap
  when the in-place merge stage lands (documented in the class)

Verified end-to-end with the apply_puppet_role stage against a local
pupmod fixture: a pre-seeded custom Gemfile survives the sync
untouched while a drifted .gitignore is re-baselined and all missing
baseline files (workflows, lint configs, spec_helper) are created; a
fixture without a Gemfile gets the baseline one. Parser validation,
plan show, the stage-list dry run, the CI e2e, and rspec all green.

Refs simp#50

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nick-markowski

Copy link
Copy Markdown
Member

LGTM — clean, well-scoped slice 1. Approving.

Correctness (confirmed):

  • managed_file is right: content + no ensureensure => file inferred, and since file's replace defaults to true, every enforce-classified profile is a byte-for-byte no-op refactor — which is why the suite + e2e stay green.
  • Reference integrity preserved: the inner file { $path: } still creates File[$path], so existing File[...] autorequires/references resolve unchanged, with exactly one File[$path] per path.
  • Gemfile classification is consistent — bootstrap preserves an existing Gemfile while Gemfile.lock stays force-absent regardless.
  • Clean type boundary: managed_file wraps content+replace only; the ensure => absent/directory resources correctly stay plain file.

One consequence worth being explicit about (documented, not a blocker): with Gemfile now bootstrap, puppetsync can no longer push any Gemfile change to existing repos — structure included, not just Renovate pins — until the in-place merge stages (slices 2/3) land. New repos still get seeded. So if a baseline Gemfile change is needed before then, it'd require a temporary per-project_type Hiera flip back to enforce.

Suggestions:

  • Add a small unit spec for managed_file asserting replace => false for mode => 'bootstrap' and true otherwise. Right now the Renovate-safety guarantee is verified once, manually, against an uncommitted fixture — so nothing in CI guards it. Since bootstrap-vs-enforce is now the load-bearing contract for not clobbering Renovate, a two-example spec locks it in cheaply against a future refactor of the define. (Happy to push this to a follow-up if you'd rather keep the slice tight.)
  • Optional: consider renaming the mode param (e.g. sync_mode/strategy). In a file resource mode conventionally means POSIX perms; here it's the management strategy. No conflict today since the define doesn't expose perms, but it's a latent trip-hazard if perm support is ever added. Docs are clear, so purely a take-it-or-leave-it.

- Rename managed_file's 'mode' parameter (and the per-profile
  passthroughs) to 'strategy': on a file-shaped define, 'mode'
  conventionally means POSIX permissions, and the rename is free now
  but breaking later once Hiera data references it
- Add scripts/ci-managed-file-contract.sh, run by the CI bolt job:
  puppet-applies profile::managed_file both ways and asserts the
  load-bearing guarantee (bootstrap never overwrites an existing file;
  enforce and the default always do; both create missing files) so the
  Renovate-safety contract is CI-guarded instead of verified once
  manually

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@silug

silug commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @nick-markowski — both suggestions taken in cbff800, and agreed on the consequence:

  • The Gemfile-freeze window you called out is real and intentional: the in-place merge that restores puppetsync's ability to change existing Gemfiles is already up as Add merge_gemfile task and pipeline stage (#50 slice 2) #72 (merge_gemfile), so the window only lasts until that merges. The temporary Hiera flip back to enforce remains the documented escape hatch if something urgent lands in between.
  • Contract test: rather than reintroduce rspec-puppet for a two-example spec, the guarantee is now CI-guarded in this repo's testing style — scripts/ci-managed-file-contract.sh (run by the CI bolt job) puppet applys profile::managed_file both ways and asserts: bootstrap never overwrites an existing file, enforce and the default always do, and both create missing files. The load-bearing Renovate-safety contract now fails CI loudly if a future refactor of the define breaks it.
  • Rename: took it — modestrategy across the define and all six profiles (plus docs). You're right that it's free now and a trap later; mode on a file-shaped define invites the POSIX-perms misreading the moment anyone adds perm support.

🤖 Generated with Claude Code

@silug
silug merged commit 80c9b6e into simp:main Jul 31, 2026
2 checks passed
@silug
silug deleted the issue-50-bootstrap-mode branch July 31, 2026 14:55
silug added a commit that referenced this pull request Jul 31, 2026
* Add merge_gha_workflows task and pipeline stage (#50 slice 3)

Final slice of the Renovate-resilient template machinery: refresh GHA
workflow files from their baseline templates while preserving every
Renovate-managed value already in the repo.

Approach: the template text is canonical — structure, comments, and
formatting come from it byte-for-byte — and the Renovate-managed
scalar values are grafted back on using Psych's node line/column
coordinates. No YAML re-serialization happens at all, so comment/
format preservation holds by construction and no new gem (psych-pure)
is needed.

- preserve_keys (default: uses, image, container, ruby-version,
  runs-on; extensible per session config) selects which mapping keys'
  values are Renovate's
- Values pair by [mapping path, key, identity], where identity is the
  part Renovate never changes (action before @, image before :, none
  for bare versions) — so distinct versions per job survive, trailing
  comments (pinned-digest conventions) ride along, and steps new to
  the template take the template's value
- The merge_github_workflows stage merges only files that exist in
  BOTH the repo and the template chain (same per-module override
  resolution as profile::github_actions); creating/removing workflow
  files remains the profile's job, and repo-specific workflows with
  no template are never touched

Verified with 13 specs (several driven by the real pr_tests.yml
template, including byte-identity assertions) and an e2e run against
a fixture with Renovate-bumped checkout@v7 / ruby-version 3.4.9 /
ubuntu-24.04 plus structural drift: the merge restored template
structure, preserved all bumped values (including per-job ones), left
an untemplated custom workflow untouched, and a second run reported
"1 unchanged". Real-world cases from pupmod-simp-pupmod dd98340
(ruby-version) and 837fb5e (runs-on) are covered by the defaults.

After this and #70 merge, profile::github_actions can flip to
bootstrap mode per project_type in Hiera, completing #50.

Closes #50

* Address review feedback: debug output, ported registries, pair the flip

- Remove the `warn stdin` debug line (it echoed every workflow template
  to stderr on each run)
- identity() now handles ported registries (registry:5000/foo:8 pairs
  by everything before the tag colon), with a spec
- Ship the profile::github_actions::strategy: bootstrap Hiera flip for
  pupmod/pupmod_skeleton WITH this stage: while the profile enforces,
  the apply stage overwrites workflows with pure template before this
  stage runs, so enabling merge_github_workflows pre-flip protects
  nothing. The key is inert until the managed_file strategy support
  merges, and the stage comment now documents the coupling. rubygem and
  unknown_with_ci project types stay enforce, since the merge stage
  does not cover them

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants