(CAT-1698) Slim .rubocop.yml to the NewCops: enable deep-merge model and validate clean scaffolds#647
(CAT-1698) Slim .rubocop.yml to the NewCops: enable deep-merge model and validate clean scaffolds#647david22swan wants to merge 13 commits into
.rubocop.yml to the NewCops: enable deep-merge model and validate clean scaffolds#647Conversation
.rubocop.yml to NewCops: enable deep-merge model (behaviour change)
68a3598 to
6e95d7c
Compare
Replace the enumeration-based RuboCop configuration with the slim model: - Reduce the profiles to `'on'` (canonical; NewCops: enable) and `'off'` (DisabledByDefault: true, NewCops: disable), with `cleanups_only`, `strict`, and `hardcore` kept as backward-compatible aliases that resolve to `'on'`. - Remove the cleanup_cops anchor and the long per-cop `Enabled: true/false` enumeration; cops are now governed implicitly by AllCops.NewCops: enable. - Keep the `default_configs` anchor of deliberate EnforcedStyle tunings, reused by the `'on'` profile. - Add `cop_overrides` as the authoritative per-module override key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete the per-cop enumeration and profile-builder tooling that the slim NewCops: enable deep-merge model replaces (CLEANUP-01): - rubocop/defaults-1.50.0.yml - rubocop/cleanup_cops.yml - rubocop/profile_builder.rb - rubocop/README.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render the final .rubocop.yml by layering configuration with deep_merge in the order `defaults -> profile['configs'] -> cop_overrides`, replacing the old enumerated render path. Resolve the active profile from `selected_profile` and guard the lookup against invalid or boolean-coerced values (e.g. an unquoted `on`/`off` coerced to true/false in YAML) so a malformed selection falls back safely instead of raising. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `/.planning/` to .gitignore so the local GSD planning documents are kept local and not tracked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the documentation in line with the slim model: - Rewrite the README "Manage Rubocop rules" section around cop_overrides .sync.yml patterns; refresh the config-key table (add cop_overrides and TargetRubyVersion, drop cleanup_cops, update selected_profile/profiles/ default_configs) and attribute Puppet 9 security cops to AllCops.NewCops. - Rewrite the CLAUDE.md RuboCop Profile System section for the deep_merge model and remove the stale rubocop/ directory references. - Fix doc accuracy findings (CR-01, WR-01): default_configs is a config_defaults.yml YAML anchor consumed by the 'on' profile, not a render-time @configs key; and config_defaults.yml ships `selected_profile: strict`, not 'on'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.ci/test_script.sh asserted the old enumerated model: that the default-rendered .rubocop.yml writes `Performance/CaseWhenSplat: Enabled: true`, and that a `.sync.yml` override under `default_configs` flips it to false. Both are stale: - The slim template no longer enumerates per-cop `Enabled: true` entries, so the "template applied" check now asserts `NewCops` is present instead. - `default_configs` is a config_defaults.yml YAML anchor, not a render-time override surface; the fixture now uses `cop_overrides`, the authoritative per-module override key, which still renders `Performance/CaseWhenSplat: Enabled: false` after `pdk update`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add nine cop tunings to the config_defaults.yml `default_configs` anchor (reused
by the 'on' profile) so a freshly-scaffolded module's generated specs and
boilerplate validate clean under the slim `AllCops.NewCops: enable` model,
without re-introducing per-cop enumeration:
- Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: comma (PDK emits
trailing commas; matches the existing Arguments/ArrayLiteral convention)
- RSpec/DescribeClass: Enabled: false (rspec-puppet describes subjects by string;
the only sanctioned wholesale disable)
- RSpec/MessageSpies, RSpec/ExampleLength, RSpec/MultipleExpectations,
RSpec/VerifiedDoubleReference: scoped via Exclude to the provider/transport
spec paths only, where resource-API specs use string doubles and multi-line
examples
- RSpec/ContextWording: add the 'on' prefix (single-quoted to avoid YAML
coercing it to boolean true) for rspec-puppet-facts `context "on #{os}"`
- Style/MixinUsage: Exclude spec/spec_helper.rb
- Lint/EmptyClass: Exclude the network_device namespace shim
These are genuine PDK / rspec-puppet conventions we do not control, so they are
tuned in config rather than fought in the templates. Each entry carries an inline
Description: rationale so the intent is not lost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…type templates Fix the Ruby emitted by the provider_type and transport_type object templates so a freshly-scaffolded module validates clean, fixing the template rather than silencing the cop: - Convert the plain `<<-EOS` heredocs to squiggly heredocs with meaningful delimiter names (`<<~DOCS` in provider_type, `<<~DESC` in transport_type) and re-indent the bodies. Clears Layout/HeredocIndentation, Layout/ClosingHeredocIndentation, and Naming/HeredocDelimiterNaming. - Drop the aligned-value padding from the transport_type `password` hash so it matches the single-space style of its sibling hashes. Clears Layout/HashAlignment. Rendered output is behavior-preserving: the heredoc string contents are byte-identical and only the delimiter token and indentation change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…4 function template Fix two style offences in the Ruby emitted by the v4 function template: - Rename the illustrative parameter `x` to `value`. Clears Naming/MethodParameterName. - Emit the dispatch symbol as `:'<%= name %>'` instead of `name.to_sym.inspect`. For a namespaced name like `foo::bar` this now renders as a single-quoted `:'foo::bar'` rather than a double-quoted `:"foo::bar"`, clearing Style/QuotedSymbols. The `dispatch_name` line is left unchanged: it never carries a `::`, so it always renders as a bare unquoted symbol. Behavior of the generated function is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SimpleProvider `get` method in the provider template returned two example resources (`foo`, `bar`), tripping Metrics/MethodLength (11/10) on a fresh scaffold. Trim it to a single example resource to clear the offence, and update the provider spec template to expect the single resource so `pdk test unit` stays green (51 examples, 0 failures). Keeping the two templates in lockstep avoids the regression where the provider returns one resource while the generated spec still asserts two. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inline `rubocop:disable` in the transport device template referenced `Style/ClassAndModuleCamelCase`, which RuboCop has moved to the `Naming` department. Under NewCops: enable this emitted a "has the wrong namespace" deprecation warning. Update the directive to `Naming/ClassAndModuleCamelCase`. The directive remains conditional on the transport name containing an underscore, where the capitalised constant genuinely raises the cop, so it does not become a redundant disable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The internal `defaults -> profile -> cop_overrides` deep-merge in moduleroot/.rubocop.yml.erb used `knockout_prefix: "--"`, while CLAUDE.md tells maintainers to use `---` to remove a cop_override. A maintainer following the docs got a silent no-op. Align the template to `---` so the documented and active knockout prefix match. Only the removal-marker string changes; merge semantics for non-knockout values are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the RuboCop Profile System section of CLAUDE.md to match the Phase 4 behaviour: - Note that the `defaults -> profile -> cop_overrides` deep-merge and PDK's `.sync.yml` overlay now share one `---` knockout prefix everywhere. - Note that `default_configs` also tunes several PDK / rspec-puppet-convention cops so freshly-scaffolded modules validate clean under NewCops: enable, each carrying an inline `Description:` rationale that must not be removed. - Add a future-improvements note that object types not exercised by `.ci/test_script.sh` (plan, legacy V1 function) were intentionally left untouched, as candidates for future template-validation coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ad25564 to
0639643
Compare
.rubocop.yml to NewCops: enable deep-merge model (behaviour change).rubocop.yml to the NewCops: enable deep-merge model and validate clean scaffolds (behaviour change)
| enabled_cops = cop_configs.keys | ||
| else | ||
| enabled_cops = [] | ||
| if @configs['cop_overrides'] && !@configs['cop_overrides'].empty? |
There was a problem hiding this comment.
cop_overrides type guard is incomplete — should confirm it is a Hash.
The guard @configs['cop_overrides'] && !@configs['cop_overrides'].empty? checks presence but not type. A misconfigured .sync.yml like cop_overrides: "not a hash" passes the guard and causes DeepMerge::deep_merge! to raise a cryptic library error with no hint that the problem is in the module's .sync.yml.
Suggestion:
if @configs['cop_overrides'].is_a?(Hash) && !@configs['cop_overrides'].empty?Or raise explicitly before the guard:
raise "cop_overrides must be a Hash, got #{@configs['cop_overrides'].class}" unless @configs['cop_overrides'].nil? || @configs['cop_overrides'].is_a?(Hash)| # YAML 1.1 may coerce an unquoted on/off profile name to true/false; map it back. | ||
| selected = { true => 'on', false => 'off' }.fetch(selected, selected) | ||
| profile = @configs['profiles'] && @configs['profiles'][selected] | ||
| raise "Unknown rubocop selected_profile #{selected.inspect}; valid: #{(@configs['profiles'] || {}).keys.join(', ')}" unless profile.is_a?(Hash) && profile['configs'] |
There was a problem hiding this comment.
profile['configs'] is checked for truthiness but not confirmed to be a Hash.
The raise guard unless profile.is_a?(Hash) && profile['configs'] checks that profile['configs'] is truthy, but not that it is itself a Hash. A custom profile with configs: "not a hash" would pass this guard and cause DeepMerge::deep_merge! to fail opaquely on the next line.
Suggestion:
raise "..." unless profile.is_a?(Hash) && profile['configs'].is_a?(Hash)| selected = { true => 'on', false => 'off' }.fetch(selected, selected) | ||
| profile = @configs['profiles'] && @configs['profiles'][selected] | ||
| raise "Unknown rubocop selected_profile #{selected.inspect}; valid: #{(@configs['profiles'] || {}).keys.join(', ')}" unless profile.is_a?(Hash) && profile['configs'] | ||
| configs = defaults.dup |
There was a problem hiding this comment.
defaults.dup produces a shallow copy — nested hashes are shared.
Hash#dup copies the top-level hash but not nested objects. After dup, configs['AllCops'] and defaults['AllCops'] point to the same object. DeepMerge::deep_merge! mutates the nested hash in place, so defaults is also mutated alongside configs. No current bug (the template renders once per Ruby process invocation), but this is a latent hazard if the template is ever invoked more than once in one process.
Suggestion: use Marshal.load(Marshal.dump(defaults)) for a deep copy, or add a comment documenting the single-render assumption so future maintainers know why dup is safe here.
| grep template < metadata.json | ||
| cp "$TEMPLATE_PR_DIR/.ci/fixtures/new_provider_sync.yml" ./.sync.yml | ||
| grep -A 1 "Performance/CaseWhenSplat" ./.rubocop.yml | grep -q "true" # Ensure that the template is applied | ||
| grep -A 5 "AllCops" ./.rubocop.yml | grep -q "NewCops" # Ensure the slim template is applied (NewCops governs cops, not per-cop enumeration) |
There was a problem hiding this comment.
Pre-update CI assertion is weaker than it looks.
grep -A 5 "AllCops" ./.rubocop.yml | grep -q "NewCops" would pass even if the profile deep-merge were completely broken, because NewCops is hardcoded in the defaults hash unconditionally — it appears in the rendered file regardless of whether profile['configs'] contributed anything.
The old assertion (grep -A 1 "Performance/CaseWhenSplat" | grep -q "true") at least verified that the profile merge contributed a specific cop setting. Consider adding an assertion that checks a tuning from default_configs actually appears — e.g. checking that Style/TrailingCommaInHashLiteral or RSpec/DescribeClass appears in the rendered file with the expected value.
| Performance/Squeeze: | ||
| Performance/StringInclude: | ||
| Performance/Sum: | ||
| RSpec/MessageSpies: |
There was a problem hiding this comment.
RSpec/MessageSpies EnforcedStyle: receive was silently dropped when strict was collapsed into an alias for on.
The old strict profile explicitly set:
RSpec/MessageSpies:
EnforcedStyle: receiveThe new default_configs block scopes RSpec/MessageSpies to an Exclude list for provider/transport spec paths but does not carry forward the EnforcedStyle: receive setting for all other spec files. RuboCop's default for this cop is have_received (spy style), which differs from the receive (expectation style) that modules were previously validated against under strict.
Existing modules using expectation-style doubles outside the excluded paths may start failing pdk validate after updating. Worth verifying against a real module before merge, and documenting the behaviour change in the PR description if the drop is intentional.
.rubocop.yml to the NewCops: enable deep-merge model and validate clean scaffolds (behaviour change).rubocop.yml to the NewCops: enable deep-merge model and validate clean scaffolds
Summary
This PR replaces the pdk-templates RuboCop template with a slim deep-merge model
and makes a freshly-scaffolded module validate clean under it.
The generated
.rubocop.ymlnow usesAllCops.NewCops: enableto govern whichcops run, instead of an enumerated per-cop
Enabled: true/falselist. Onlyintentionally-styled cops (EnforcedStyle and option tunings) are written out. The
result is a shorter, self-maintaining file that stays current as RuboCop adds new
cops.
On top of the slim model, this PR tunes the genuine PDK / rspec-puppet convention
cops and fixes the object templates so a module scaffolded from these templates
passes
pdk validatewith zero offences underNewCops: enable— taking theempirical offence count on a fresh full-object-type scaffold from 44 down to 0,
with CI green end-to-end.
What changed
config_defaults.ymlto a two-profile model(
'on'=NewCops: enable,'off'=DisabledByDefault/NewCops: disable,with
cleanups_only/strict/hardcoreas backward-compatible aliases of'on'), drop the per-cop enumeration and thecleanup_copsanchor, and addcop_overridesas the authoritative per-module override key.rubocop/defaults-1.50.0.yml,cleanup_cops.yml,profile_builder.rb, andrubocop/README.md.moduleroot/.rubocop.yml.erbnow layersconfig via
deep_merge(defaults -> profile configs -> cop_overrides), guardsthe
selected_profilelookup against invalid/boolean-coerced values, and uses asingle
---knockout prefix consistent with the docs.default_configsanchor (trailing-comma style, rspec-puppet describe/context/double idioms, etc.), each with an inline
Description:rationale.templates (squiggly heredocs and aligned hashes in the resource-API type
templates, idiomatic param name and single-quoted symbol in the v4 function
template, a single-resource SimpleProvider
getwith a matching spec, and thecorrected
Naming/ClassAndModuleCamelCasenamespace in the transport devicedisable directive), with unchanged runtime behaviour.
README.mdandCLAUDE.mdfor the deep-merge model andthe convention tunings, and update
.ci/test_script.sh(and its fixture) toassert the slim model.
Verification
Fresh-scaffold gate (PDK 3.7.0, Ruby 3.2.11, Puppet 8.18.0). A module
scaffolded from these templates covering every CI object type (class, defined
type, fact, native + v4 function, provider, task, transport):
pdk validate— exit 0, 0 RuboCop offences (down from 44), no.rubocop_todo.ymlgenerated or relied on.pdk test unit— 51 examples, 0 failures..ci/test_script.sh(new-module flow + both convert flows) runsgreen end-to-end, with the CI script and fixture left unchanged.
The PR's CI pipeline is green, confirming the above on CI infrastructure.
Migration impact on existing modules was verified against two live puppetlabs
modules:
puppetlabs-apache(main):pdk validateexit 2 (one pre-existing puppet-lintwarning, 0 RuboCop offences; apache's up-to-date
.rubocop_todo.ymlsuppressesall current findings).
puppetlabs-motd(main):pdk validateexit 1 (11 RuboCop CONVENTION offencessurfaced by
NewCops: enableacrossRSpec/DescribeClass,RSpec/MultipleExpectations,Style/MixinUsage, andStyle/TrailingCommaInHashLiteral).Both modules converted and validated without a hard process failure.
Migration / breaking-class change
Three concrete migration points for module maintainers:
1.
selected_profile: 'off'mechanism changedPreviously,
'off'emitted an enumerated list of per-copEnabled: falseentriessourced from
rubocop/defaults-1.50.0.yml. That file has been removed.The new
'off'profile uses RuboCop's native switch:This achieves the same effect (all cops disabled) through a single idiomatic
mechanism. Modules using
selected_profile: 'off'will get this new form onpdk update— no action required unless you relied on the enumerated list fornon-PDK tooling.
2.
NewCops: enablesurfaces more offences in modules with older.rubocop_todo.ymlfilesSwitching from the old curated enumeration to
NewCops: enableactivates cops thatwere either not in the previous
strictenable-list or not covered by an older.rubocop_todo.yml. This can surface new CONVENTION-level RuboCop offences onpdk validatefor modules whose.rubocop_todo.ymlwas generated against an olderRuboCop version (for example, motd's
.rubocop_todo.ymlfrom RuboCop 1.48.1 in 2023did not suppress the 11 offences that appeared in the verification run).
To triage new offences, module maintainers should:
include_todos: truein.sync.yml, then runpdk updateto ensure.rubocop.ymlinherits from.rubocop_todo.yml..rubocop_todo.ymlusingbundle exec rubocop --auto-gen-configorpdk validate --auto-correctto baseline the new findings.cop_overridesin.sync.yml.3.
cleanups_only,strict, andhardcoreare backward-compatible aliases for'on'All three legacy profile names resolve to the same output as
selected_profile: 'on'(i.e.
AllCops.NewCops: enablewith the basedefault_configsEnforcedStyle tunings).Existing
.sync.ymlfiles that set any of these names continue to work with zeromigration required.
Durable reference
See the updated
README.md"Manage Rubocop rules" section and the.rubocop.ymlconfig-key table for the authoritative key reference (
selected_profile,profiles,cop_overrides,include_todos,TargetRubyVersion,default_configs) and usageexamples under the new model.
CLAUDE.mddocuments the deep-merge model, theconvention tunings (and their inline
Description:rationale), and the---knockout prefix.
Out of scope
Release cut, version bump, and tag order are not part of this PR.
🤖 Generated with Claude Code