diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e92ee6..7bdd0c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v7 - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@v1.334.0 with: # Match the Ruby bundled with openbolt, which runs the tasks in production ruby-version: '3.2.11' diff --git a/data/sync/configs/20261024-setup-ruby-release-tag.yaml b/data/sync/configs/20261024-setup-ruby-release-tag.yaml new file mode 100644 index 0000000..3608393 --- /dev/null +++ b/data/sync/configs/20261024-setup-ruby-release-tag.yaml @@ -0,0 +1,64 @@ +--- +# Reference ruby/setup-ruby by a release tag instead of `@v1`. +# +# In ruby/setup-ruby, `v1` is a branch, not a tag. Renovate can't resolve +# a branch to a commit, so `@v1` can be neither pinned to a digest nor held +# back by the shared preset's minimumReleaseAge. A release tag gives +# Renovate a version to pin, age and update; the shared ruby.json preset +# groups those updates with the Ruby patch updates. +# +# merge_github_workflows normally preserves each repo's `uses:` values, so +# `refresh_identities` makes the template's ruby/setup-ruby ref win for +# this session only. Every other Renovate-managed value is preserved as +# usual. +# +# Run this after simp/renovate-config#9 merges. Without it, every +# setup-ruby release opens its own immediate PR in every repo, instead of +# joining the grouped Ruby PR after a 2-day wait. +puppetsync::plan_config: + puppetsync: + permitted_project_types: + - pupmod + - pupmod_skeleton + plans: + sync: + github_api_delay_seconds: 1 + merge_github_workflows: + files: + - pr_tests.yml + - tag_deploy.yml + preserve_blocks: + - jobs.acceptance + refresh_identities: + - ruby/setup-ruby + stages: + - install_gems + - checkout_git_feature_branch_in_each_repo + - merge_github_workflows + - git_commit_changes + - ensure_github_fork + - ensure_git_remote + - git_push_to_remote + - ensure_github_pr + + approve_github_prs: + stages: + - approve_github_pr_for_each_repo + + merge_github_prs: + stages: + - merge_github_pr_for_each_repo + + git: + feature_branch: puppetsync/20261024-setup-ruby-release-tag + commit_message: | + [puppetsync] Reference ruby/setup-ruby by a release tag + + `ruby/setup-ruby@v1` follows a branch, which Renovate can't pin or + hold back with a minimum release age. Reference the v1.334.0 release + tag instead, so Renovate pins it and proposes setup-ruby updates + together with the Ruby updates that need them. + + github: + pr_user: silug # This should be the account that *submitted* the PRs + approval_message: ':+1: :ghost:' diff --git a/dist/puppetsync/plans/init.pp b/dist/puppetsync/plans/init.pp index 4b90ce4..c703a8f 100644 --- a/dist/puppetsync/plans/init.pp +++ b/dist/puppetsync/plans/init.pp @@ -344,10 +344,14 @@ default => Hash({ 'path' => "${wf_dir}/${f}", 'template' => file::read($template_path) }), } }.filter |$wf| { $wf =~ NotUndef } + # Optional: identities (e.g. 'ruby/setup-ruby') whose values come from + # the template this session, overriding the repo's preserved value. + # puppetsync.plans.sync.merge_github_workflows.refresh_identities Hash({ - 'workflows' => $workflows, - 'preserve_keys' => $opts.dig('merge_github_workflows', 'preserve_keys'), - 'preserve_blocks' => $opts.dig('merge_github_workflows', 'preserve_blocks'), + 'workflows' => $workflows, + 'preserve_keys' => $opts.dig('merge_github_workflows', 'preserve_keys'), + 'preserve_blocks' => $opts.dig('merge_github_workflows', 'preserve_blocks'), + 'refresh_identities' => $opts.dig('merge_github_workflows', 'refresh_identities'), }) } } diff --git a/dist/puppetsync/tasks/merge_gha_workflows.json b/dist/puppetsync/tasks/merge_gha_workflows.json index 15b558a..9c97da3 100644 --- a/dist/puppetsync/tasks/merge_gha_workflows.json +++ b/dist/puppetsync/tasks/merge_gha_workflows.json @@ -10,6 +10,10 @@ "description": "YAML mapping keys whose scalar values are Renovate-managed and must be preserved from the existing file (default: uses, image, container, ruby-version, runs-on)", "type": "Optional[Array[String[1]]]" }, + "refresh_identities": { + "description": "Identities whose values come from the template even when the repo has one (e.g. ['ruby/setup-ruby'] to move every ruby/setup-ruby ref to the template's): for a session that deliberately changes a preserved value without resetting the repo's other Renovate-managed values", + "type": "Optional[Array[String[1]]]" + }, "preserve_blocks": { "description": "Dot-paths of mapping blocks the EXISTING file is authoritative for (e.g. ['jobs.acceptance']): its block replaces the template's, is appended when the template lacks it, and the template's block is removed when the existing file has none", "type": "Optional[Array[String[1]]]" diff --git a/dist/puppetsync/tasks/merge_gha_workflows.rb b/dist/puppetsync/tasks/merge_gha_workflows.rb index 3d82bf2..d991a4b 100644 --- a/dist/puppetsync/tasks/merge_gha_workflows.rb +++ b/dist/puppetsync/tasks/merge_gha_workflows.rb @@ -32,6 +32,14 @@ # other job's `needs:`, since GitHub rejects a workflow whose `needs:` # names a job that doesn't exist (the template's aggregate check job # needs every job, acceptance included). +# +# `refresh_identities` names identities (e.g. 'ruby/setup-ruby') whose values +# come from the template even when the repo already has one, including inside +# `preserve_blocks`: the way a session deliberately changes a preserved value, +# such as moving an action from a branch ref to a release tag, without +# touching the repo's other Renovate-managed values. An occurrence the +# template has no counterpart for (e.g. in a repo-owned block) takes the +# template's first value for that identity. require 'fileutils' require 'json' @@ -184,7 +192,19 @@ def prune_needs(template, job_names) end end -def merge_workflow(template, existing_text, keys, blocks) +# The template's values for a refreshed identity at this [path, key, +# identity], or its first value for the identity anywhere when this context +# only exists in the repo (a preserved block the template lacks) +def template_refresh(template_values, id) + list = template_values.fetch(id, []) + return list unless list.empty? + + template_values.select { |(_path, _key, ident), _values| ident == id[2] }.values.flatten.first(1) +end + +def merge_workflow(template, existing_text, keys, blocks, refresh = []) + # Read before preserve_blocks splices repo content into the template + template_values = refresh.empty? ? {} : existing_values(template, keys) template, preserved_blocks, removed_blocks = preserve_existing_blocks(template, existing_text, blocks) removed_jobs = removed_blocks.filter_map { |path| path.delete_prefix('jobs.') if path.match?(/\Ajobs\.[^.]+\z/) } template = prune_needs(template, removed_jobs) @@ -197,12 +217,13 @@ def merge_workflow(template, existing_text, keys, blocks) # differences (distinct versions per job, trailing comments) survive # verbatim. Template entries with no counterpart in the existing file # (a genuinely new or restored step) keep the template's value. + # Refreshed identities take the template's values instead. counters = Hash.new(0) preserved_scalars(Psych.parse(template), keys).each do |path, key, node| next unless node.start_line == node.end_line id = [path, key, identity(node.value)] - list = existing[id] + list = refresh.include?(id[2]) ? template_refresh(template_values, id) : existing[id] value = list[counters[id]] || list.last counters[id] += 1 next if value.nil? @@ -217,7 +238,7 @@ def merge_workflow(template, existing_text, keys, blocks) [lines.join("\n"), updated, preserved_blocks] end -def merge_gha_workflows(workflows, keys, blocks) +def merge_gha_workflows(workflows, keys, blocks, refresh = []) results = {} workflows.each do |wf| path = wf.fetch('path') @@ -232,7 +253,7 @@ def merge_gha_workflows(workflows, keys, blocks) end existing_text = File.read(path) - merged, updated, preserved_blocks = merge_workflow(template, existing_text, keys, blocks) + merged, updated, preserved_blocks = merge_workflow(template, existing_text, keys, blocks, refresh) if merged == existing_text results[path] = { 'changed' => false } @@ -252,5 +273,6 @@ def merge_gha_workflows(workflows, keys, blocks) raise('No workflows given') unless workflows.is_a?(Array) keys = params.fetch('preserve_keys', nil) || DEFAULT_PRESERVE_KEYS blocks = params.fetch('preserve_blocks', nil) || [] +refresh = params.fetch('refresh_identities', nil) || [] -puts JSON.generate(merge_gha_workflows(workflows, keys, blocks)) +puts JSON.generate(merge_gha_workflows(workflows, keys, blocks, refresh)) diff --git a/modules/profile/files/_github/workflows/tag_deploy_rubygem.yml b/modules/profile/files/_github/workflows/tag_deploy_rubygem.yml index 517ad8b..91f9fe5 100644 --- a/modules/profile/files/_github/workflows/tag_deploy_rubygem.yml +++ b/modules/profile/files/_github/workflows/tag_deploy_rubygem.yml @@ -90,7 +90,7 @@ jobs: echo "build_command=$GEM_BUILD_COMMAND" | tee -a "$GITHUB_OUTPUT" echo "pkg_dir=$GEM_PKG_DIR" | tee -a "$GITHUB_OUTPUT" echo "release_command=$GEM_RELEASE_COMMAND" | tee -a "$GITHUB_OUTPUT" - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@v1.334.0 with: ruby-version: 2.7 bundler-cache: true @@ -182,7 +182,7 @@ jobs: with: ref: ${{ github.ref }} clean: true - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@v1.334.0 with: ruby-version: 2.7 bundler-cache: true diff --git a/modules/profile/files/_github/workflows/tag_deploy_rubygem__github-only.yml b/modules/profile/files/_github/workflows/tag_deploy_rubygem__github-only.yml index cb30155..4818154 100644 --- a/modules/profile/files/_github/workflows/tag_deploy_rubygem__github-only.yml +++ b/modules/profile/files/_github/workflows/tag_deploy_rubygem__github-only.yml @@ -77,7 +77,7 @@ jobs: echo "build_command=$GEM_BUILD_COMMAND" | tee -a "$GITHUB_OUTPUT" echo "pkg_dir=$GEM_PKG_DIR" | tee -a "$GITHUB_OUTPUT" echo "release_command=$GEM_RELEASE_COMMAND" | tee -a "$GITHUB_OUTPUT" - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@v1.334.0 with: ruby-version: 2.7 bundler-cache: true diff --git a/modules/profile/files/_github/workflows/tag_deploy_rubygem__github-rpms.yml b/modules/profile/files/_github/workflows/tag_deploy_rubygem__github-rpms.yml index a505820..b1afef2 100644 --- a/modules/profile/files/_github/workflows/tag_deploy_rubygem__github-rpms.yml +++ b/modules/profile/files/_github/workflows/tag_deploy_rubygem__github-rpms.yml @@ -91,7 +91,7 @@ jobs: echo "build_command=$GEM_BUILD_COMMAND" | tee -a "$GITHUB_OUTPUT" echo "pkg_dir=$GEM_PKG_DIR" | tee -a "$GITHUB_OUTPUT" echo "release_command=$GEM_RELEASE_COMMAND" | tee -a "$GITHUB_OUTPUT" - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@v1.334.0 with: ruby-version: 2.7 bundler-cache: true diff --git a/modules/profile/files/pupmod/_github/workflows/pr_tests.simp-simp.yml b/modules/profile/files/pupmod/_github/workflows/pr_tests.simp-simp.yml index 3102366..0204306 100644 --- a/modules/profile/files/pupmod/_github/workflows/pr_tests.simp-simp.yml +++ b/modules/profile/files/pupmod/_github/workflows/pr_tests.simp-simp.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 # ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -48,7 +48,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -62,7 +62,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -75,7 +75,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -88,7 +88,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -141,7 +141,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby ${{matrix.puppet.ruby_version}}' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: ${{matrix.puppet.ruby_version}} bundler-cache: true @@ -193,7 +193,7 @@ jobs: - name: checkout repo uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' - name: bundle install diff --git a/modules/profile/files/pupmod/_github/workflows/pr_tests.yml b/modules/profile/files/pupmod/_github/workflows/pr_tests.yml index 678173b..a005f5f 100644 --- a/modules/profile/files/pupmod/_github/workflows/pr_tests.yml +++ b/modules/profile/files/pupmod/_github/workflows/pr_tests.yml @@ -23,7 +23,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 # ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -48,7 +48,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -62,7 +62,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -75,7 +75,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -88,7 +88,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -126,7 +126,7 @@ jobs: steps: - uses: actions/checkout@v7 - name: 'Install Ruby ${{matrix.puppet.ruby_version}}' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: ${{matrix.puppet.ruby_version}} bundler-cache: true @@ -173,7 +173,7 @@ jobs: - name: checkout repo uses: actions/checkout@v7 - name: 'Install Ruby' - uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' - name: bundle install diff --git a/modules/profile/files/pupmod/_github/workflows/tag_deploy.yml b/modules/profile/files/pupmod/_github/workflows/tag_deploy.yml index 5d883b7..27d6778 100644 --- a/modules/profile/files/pupmod/_github/workflows/tag_deploy.yml +++ b/modules/profile/files/pupmod/_github/workflows/tag_deploy.yml @@ -53,7 +53,7 @@ jobs: with: ref: ${{ github.ref }} clean: true - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true @@ -174,7 +174,7 @@ jobs: with: ref: ${{ github.ref }} clean: true - - uses: ruby/setup-ruby@v1 + - uses: ruby/setup-ruby@v1.334.0 with: ruby-version: '4.0.9' bundler-cache: true diff --git a/renovate.json b/renovate.json index f40b4af..40e99b7 100644 --- a/renovate.json +++ b/renovate.json @@ -2,6 +2,7 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", + "github>simp/renovate-config", "github>simp/renovate-config:ruby.json" ], "github-actions": { diff --git a/spec/tasks/merge_gha_workflows_spec.rb b/spec/tasks/merge_gha_workflows_spec.rb index 869f11f..cf1389d 100644 --- a/spec/tasks/merge_gha_workflows_spec.rb +++ b/spec/tasks/merge_gha_workflows_spec.rb @@ -13,9 +13,10 @@ end end - def run_merge(workflows, preserve_blocks: nil) + def run_merge(workflows, preserve_blocks: nil, refresh_identities: nil) params = { 'workflows' => workflows } params['preserve_blocks'] = preserve_blocks if preserve_blocks + params['refresh_identities'] = refresh_identities if refresh_identities run_task('merge_gha_workflows.rb', params) end @@ -251,6 +252,112 @@ def run_merge(workflows, preserve_blocks: nil) expect(File.read(@wf)).to eq(first_pass) end + context 'with refresh_identities' do + let(:template) do + <<~YAML + jobs: + build: + steps: + - uses: actions/checkout@v7 + - uses: ruby/setup-ruby@v1.334.0 + test: + steps: + - uses: ruby/setup-ruby@v1.334.0 + with: + ruby-version: '4.0.9' + YAML + end + + let(:existing) do + <<~YAML + jobs: + build: + steps: + - uses: actions/checkout@3d3c42e0f0d4c8a1e9f6c2b5a7d8e9f0a1b2c3d4 # v7 + - uses: ruby/setup-ruby@v1 # ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 + test: + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '4.0.8' + YAML + end + + it "takes the template's value for every occurrence of a refreshed identity" do + File.write(@wf, existing) + + stdout, stderr, status = run_merge([{ 'path' => @wf, 'template' => template }], refresh_identities: ['ruby/setup-ruby']) + + expect(status).to be_success, stderr + expect(JSON.parse(stdout)['files'][@wf]['changed']).to be true + merged = File.read(@wf) + expect(merged.scan('uses: ruby/setup-ruby@v1.334.0').size).to eq(2) + expect(merged).not_to include('ec106b438a') # the repo's trailing comment goes with its value + end + + it "keeps the repo's other Renovate-managed values" do + File.write(@wf, existing) + + _stdout, stderr, status = run_merge([{ 'path' => @wf, 'template' => template }], refresh_identities: ['ruby/setup-ruby']) + + expect(status).to be_success, stderr + merged = File.read(@wf) + expect(merged).to include('actions/checkout@3d3c42e0f0d4c8a1e9f6c2b5a7d8e9f0a1b2c3d4 # v7') + expect(merged).to include("ruby-version: '4.0.8'") + end + + it "keeps the repo's value for that identity without the option" do + File.write(@wf, existing) + + _stdout, stderr, status = run_merge([{ 'path' => @wf, 'template' => template }]) + + expect(status).to be_success, stderr + expect(File.read(@wf)).to eq(existing) + end + + it 'refreshes the identity inside preserved blocks too' do + template_with_acceptance = template + <<~YAML.gsub(/^/, ' ') + acceptance: + steps: + - uses: ruby/setup-ruby@v1.334.0 + YAML + repo_with_blocks = existing + <<~YAML.gsub(/^/, ' ') + acceptance: + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2.11' + repo_only: + steps: + - uses: ruby/setup-ruby@v1 + YAML + File.write(@wf, repo_with_blocks) + + _stdout, stderr, status = run_merge( + [{ 'path' => @wf, 'template' => template_with_acceptance }], + preserve_blocks: ['jobs.acceptance', 'jobs.repo_only'], + refresh_identities: ['ruby/setup-ruby'], + ) + + expect(status).to be_success, stderr + merged = File.read(@wf) + expect(merged).not_to match(%r{ruby/setup-ruby@v1$}) + expect(merged.scan('uses: ruby/setup-ruby@v1.334.0').size).to eq(4) + expect(merged).to include("ruby-version: '3.2.11'") # the rest of the preserved block is untouched + end + + it 'moves the fleet off the setup-ruby branch ref against the real template' do + fleet = real_template.gsub(/ruby\/setup-ruby@v1\.334\.0/, 'ruby/setup-ruby@v1') + raise 'munge failed' if fleet == real_template + File.write(@wf, fleet) + + _stdout, stderr, status = run_merge([{ 'path' => @wf, 'template' => real_template }], refresh_identities: ['ruby/setup-ruby']) + + expect(status).to be_success, stderr + expect(File.read(@wf)).to eq(real_template) + end + end + context 'with preserve_blocks' do let(:template) do <<~YAML