Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ jobs:
- name: Idempotency end-to-end test (local fixture repo)
run: ./scripts/ci-e2e-idempotency.sh

- name: managed_file bootstrap/enforce contract test
run: ./scripts/ci-managed-file-contract.sh

- name: List pipeline stages (plan dry run)
env:
# The plan requires these to be set, but nothing calls the APIs in
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Three layers, all data-driven via Hiera (`hiera.yaml` defines two separate hiera

2. **The baseline — `modules/role/` and `modules/profile/`**
- One stage (`apply_puppet_role`) applies a `role::*` class to each cloned repo's working tree. The role is chosen per repo via Hiera's `classes` key, keyed off the `project_type` fact (see `data/project_types/*.yaml`).
- `role::*` classes are thin lists of `profile::*` includes. Each profile manages a specific slice of the baseline (Gemfile, git files, GitHub Actions workflows, lint configs, ...), writing into `$::repo_path`.
- `role::*` classes are thin lists of `profile::*` includes. Each profile manages a specific slice of the baseline (Gemfile, git files, GitHub Actions workflows, lint configs, ...), writing into `$::repo_path` via the `profile::managed_file` define. Files have two management strategies (Hiera-overridable `strategy` param per profile): `enforce` (content fully managed, the default) and `bootstrap` (complete file laid down only when missing — used for files whose values Renovate maintains in place, like the Gemfile).
- Static file sources live in `modules/profile/files/`, templates in `modules/profile/templates/`. Dotfiles are stored with a leading underscore (`_gitignore` → `.gitignore`). Profiles use fallback lookup chains so a repo-specific override (`_gitignore.<repo_name>`, or `_github/workflows/<action>.<repo_name>.yml`) beats the generic file.

3. **Data — `data/`**
Expand Down
11 changes: 10 additions & 1 deletion modules/profile/manifests/github_actions.pp
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@
#
# files/pupmod/_github/workflows/{workflow_name}.{repo_name}.yml
#
# @param strategy
# `enforce` (default): workflow files are fully managed — puppetsync is
# still the delivery mechanism for workflow changes. NOTE: the `uses:`
# action refs in these files are Renovate-managed, so an `enforce` sync
# can revert Renovate's bumps until the in-place merge stage exists
# (simp/puppetsync#50); switch to `bootstrap` per project_type in Hiera
# once that lands.
class profile::github_actions(
Stdlib::Absolutepath $target_github_actions_dir = "${::repo_path}/.github/workflows",
Optional[String[1]] $target_repo_name = $facts.dig('module_metadata','name'),
Array[String] $present_action_files = [],
Array[String] $absent_action_files = [
'pr_glci.yml', 'pr_glci_cleanup.yml', 'pr_glci_manual.yml',
],
Enum['enforce','bootstrap'] $strategy = 'enforce',
){
$project_type = $facts.dig('project_type').lest || {'unknown'}
$project_type2 = $project_type == 'pupmod_skeleton' ? {
Expand All @@ -29,7 +37,8 @@

$present_action_files.each |$action_file| {
$action = basename( $action_file, '.yml' )
file{ "${target_github_actions_dir}/${action_file}":
profile::managed_file{ "${target_github_actions_dir}/${action_file}":
strategy => $strategy,
content => file(
"${module_name}/${project_type}/_github/workflows/${action}.${target_repo_name}.yml",
"${module_name}/${project_type}/_github/workflows/${action}.yml",
Expand Down
28 changes: 28 additions & 0 deletions modules/profile/manifests/managed_file.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# @summary Manage a baseline file in a target repo
#
# @param content
# Full file content
#
# @param strategy
# How the file is managed:
#
# * `enforce` (default) ― content is fully managed; local changes are
# always overwritten by the next sync
# * `bootstrap` ― lay down the complete file only if it does not exist
# yet. Use this for files whose values are maintained in place after
# creation (e.g. version pins managed by Renovate), so a sync never
# clobbers them. (In-place structural updates are handled separately —
# see the merge stages planned in simp/puppetsync#50.)
#
# @param path
# Absolute path of the file to manage (defaults to the resource title)
define profile::managed_file (
String $content,
Enum['enforce','bootstrap'] $strategy = 'enforce',
Stdlib::Absolutepath $path = $title,
) {
file { $path:
content => $content,
replace => $strategy ? { 'bootstrap' => false, default => true },
}
}
18 changes: 13 additions & 5 deletions modules/profile/manifests/pupmod/gemfile.pp
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
# Static Gemfile for Puppet modules
# Baseline Gemfile for Puppet modules
#
# @param strategy
# `bootstrap` (default): the Gemfile is only laid down when it doesn't
# exist, so Renovate-managed gem pins in existing repos are never
# clobbered. Set to `enforce` (e.g. per project_type in Hiera) to
# overwrite existing files.
class profile::pupmod::gemfile(
Stdlib::Absolutepath $gemfile_path = "${::repo_path}/Gemfile",
Optional[String[1]] $target_module_name = $facts.dig('module_metadata','name'),
Stdlib::Absolutepath $gemfile_path = "${::repo_path}/Gemfile",
Optional[String[1]] $target_module_name = $facts.dig('module_metadata','name'),
Enum['enforce','bootstrap'] $strategy = 'bootstrap',
){
file{ $gemfile_path:
profile::managed_file{ $gemfile_path:
strategy => $strategy,
content => file(
"${module_name}/pupmod/Gemfile.${target_module_name}",
"${module_name}/pupmod/Gemfile",
)
),
}

file{ "${gemfile_path}.lock":
Expand Down
17 changes: 12 additions & 5 deletions modules/profile/manifests/pupmod/git_files.pp
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
# Manages .gitignore and .gitattributes
#
# @param strategy
# `enforce` (default): these files carry no externally-managed values, so
# their content is fully managed
class profile::pupmod::git_files(
Stdlib::Absolutepath $gitignore_path = "${::repo_path}/.gitignore",
Stdlib::Absolutepath $gitattributes_path = "${::repo_path}/.gitattributes",
Optional[String[1]] $target_module_name = $facts.dig('module_metadata','name'),
Stdlib::Absolutepath $gitignore_path = "${::repo_path}/.gitignore",
Stdlib::Absolutepath $gitattributes_path = "${::repo_path}/.gitattributes",
Optional[String[1]] $target_module_name = $facts.dig('module_metadata','name'),
Enum['enforce','bootstrap'] $strategy = 'enforce',
){
file{ $gitignore_path:
profile::managed_file{ $gitignore_path:
strategy => $strategy,
content => file(
"${module_name}/pupmod/_gitignore.${target_module_name}",
"${module_name}/pupmod/_gitignore",
"${module_name}/_gitignore",
),
}

file{ $gitattributes_path:
profile::managed_file{ $gitattributes_path:
strategy => $strategy,
content => file(
"${module_name}/pupmod/_gitattributes.${target_module_name}",
"${module_name}/pupmod/_gitattributes",
Expand Down
11 changes: 8 additions & 3 deletions modules/profile/manifests/pupmod/pdkignore.pp
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@
#
# files/pupmod/_pdkignore.pupmod-simp-name
#
# @param strategy
# `enforce` (default): this file carries no externally-managed values, so
# its content is fully managed
class profile::pupmod::pdkignore(
Stdlib::Absolutepath $target_pdkignore_path = "${::repo_path}/.pdkignore",
Optional[String[1]] $target_module_name = $facts.dig('module_metadata','name'),
Stdlib::Absolutepath $target_pdkignore_path = "${::repo_path}/.pdkignore",
Optional[String[1]] $target_module_name = $facts.dig('module_metadata','name'),
Enum['enforce','bootstrap'] $strategy = 'enforce',
){
file{ $target_pdkignore_path:
profile::managed_file{ $target_pdkignore_path:
strategy => $strategy,
content => file(
"${module_name}/pupmod/_pdkignore.${target_module_name}",
"${module_name}/pupmod/_pdkignore"
Expand Down
12 changes: 9 additions & 3 deletions modules/profile/manifests/pupmod/puppet_lint.pp
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# Manages .puppet-lint.rc
#
# @param strategy
# `enforce` (default): this file carries no externally-managed values, so
# its content is fully managed
class profile::pupmod::puppet_lint(
Stdlib::Absolutepath $puppet_lint_rc_path = "${::repo_path}/.puppet-lint.rc",
Optional[String[1]] $target_module_name = $facts.dig('module_metadata','name'),
Stdlib::Absolutepath $puppet_lint_rc_path = "${::repo_path}/.puppet-lint.rc",
Optional[String[1]] $target_module_name = $facts.dig('module_metadata','name'),
Enum['enforce','bootstrap'] $strategy = 'enforce',
){
file{ $puppet_lint_rc_path:
profile::managed_file{ $puppet_lint_rc_path:
strategy => $strategy,
content => file(
"${module_name}/pupmod/_puppet-lint.rc.${target_module_name}",
"${module_name}/pupmod/_puppet-lint.rc",
Expand Down
10 changes: 8 additions & 2 deletions modules/profile/manifests/pupmod/rspec.pp
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,27 @@
# @param rspec_path Path to .rspec
# @param spec_helper_path Path to spec_helper.rb
# @param target_module_name Target module name
# @param strategy
# `enforce` (default): these files carry no externally-managed values, so
# their content is fully managed
class profile::pupmod::rspec (
# lint:ignore:top_scope_facts
Stdlib::Absolutepath $rspec_path = "${::repo_path}/.rspec",
Stdlib::Absolutepath $spec_helper_path = "${::repo_path}/spec/spec_helper.rb",
# lint:endignore
Optional[String[1]] $target_module_name = $facts.dig('module_metadata','name'),
Enum['enforce','bootstrap'] $strategy = 'enforce',
) {
file { $rspec_path:
profile::managed_file { $rspec_path:
strategy => $strategy,
content => file(
"${module_name}/pupmod/_rspec.${target_module_name}",
"${module_name}/pupmod/_rspec",
),
}

file { $spec_helper_path:
profile::managed_file { $spec_helper_path:
strategy => $strategy,
content => epp(
"${module_name}/pupmod/spec/spec_helper.rb.epp",
),
Expand Down
40 changes: 40 additions & 0 deletions scripts/ci-managed-file-contract.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash
# Contract test for profile::managed_file (simp/puppetsync#50):
#
# - strategy => 'bootstrap' must NEVER overwrite an existing file
# (this is the guarantee that keeps puppetsync from clobbering
# Renovate-managed values)
# - strategy => 'enforce' (and the default) must always overwrite
# - both strategies must create a missing file with the given content
#
# Requires bolt (openbolt) and the project's modules (./Rakefile install).
set -euo pipefail
cd "$(dirname "$0")/.."

PUPPET="${PUPPET:-/opt/puppetlabs/bolt/bin/puppet}"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

fail() { echo "FAIL: $1" >&2; exit 1; }
apply() { "$PUPPET" apply --modulepath modules:.modules -e "$1" >/dev/null; }

printf 'custom\n' > "$WORK/bootstrap-existing.txt"
apply "profile::managed_file { '$WORK/bootstrap-existing.txt': content => \"baseline\n\", strategy => 'bootstrap' }"
[ "$(cat "$WORK/bootstrap-existing.txt")" = 'custom' ] \
|| fail "strategy => bootstrap overwrote an existing file"

printf 'custom\n' > "$WORK/enforce-existing.txt"
apply "profile::managed_file { '$WORK/enforce-existing.txt': content => \"baseline\n\", strategy => 'enforce' }"
[ "$(cat "$WORK/enforce-existing.txt")" = 'baseline' ] \
|| fail "strategy => enforce did not overwrite an existing file"

printf 'custom\n' > "$WORK/default-existing.txt"
apply "profile::managed_file { '$WORK/default-existing.txt': content => \"baseline\n\" }"
[ "$(cat "$WORK/default-existing.txt")" = 'baseline' ] \
|| fail "the default strategy is not enforce"

apply "profile::managed_file { '$WORK/bootstrap-missing.txt': content => \"baseline\n\", strategy => 'bootstrap' }"
[ "$(cat "$WORK/bootstrap-missing.txt")" = 'baseline' ] \
|| fail "strategy => bootstrap did not create a missing file"

echo 'PASS: managed_file contract'