Puppet Core 9 Compatibility #4
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Puppet Core 9 Compatibility | |
| # Thin trigger workflow for the Puppet 9 major. Deliberately a near-copy of | |
| # compatibility-runner-puppet8.yml rather than a shared reusable workflow: | |
| # defining the matrix jobs behind a workflow_call boundary breaks the Actions | |
| # run page's left-sidebar job names (see docs/puppet-core-9-dual-major-support.md | |
| # §4, §12). DRY lives at the *step* level instead, via the same three composite | |
| # actions both callers share: | |
| # - .github/actions/prepare-test-matrix (change detection + matrix build) | |
| # - .github/actions/run-module-test (the actual per-module test) | |
| # - .github/actions/publish-compatibility-results (ledger/dashboard write) | |
| # A diff against the Puppet 8 caller should show only the profile name, the | |
| # concurrency-group suffix, the caller-workflow-file path, the puppet-major | |
| # input, the cron hour, and this comment — anything more is drift to catch in | |
| # review. | |
| # | |
| # The file itself landed on main ahead of the rest of Step D, because GitHub | |
| # only exposes workflow_dispatch for workflow files present on the default | |
| # branch (the constraint hit in Step C). | |
| # | |
| # The cron is offset an hour after the Puppet 8 caller's so the two nightly | |
| # runs don't contend for runners; correctness doesn't depend on the offset — | |
| # the shared compat-ledger-* concurrency group on `publish` already serializes | |
| # the one step that would otherwise race (§4.2). | |
| on: | |
| schedule: | |
| - cron: "0 3 * * *" # Nightly at 03:00 UTC (an hour after the Puppet 8 caller) | |
| workflow_dispatch: | |
| inputs: | |
| profile: | |
| description: "Compatibility profile name" | |
| required: false | |
| default: "9-latest-maintained" | |
| metadata_mode: | |
| description: "Metadata handling mode (warn|fail)" | |
| required: false | |
| default: "warn" | |
| enable_debug: | |
| description: "Enable verbose debug output (captures Docker logs, acceptance output)" | |
| required: false | |
| default: 'false' | |
| type: choice | |
| options: | |
| - 'false' | |
| - 'true' | |
| lean: | |
| description: "Lean run: test only changed/not-green/stale modules (false = full run of all modules)" | |
| required: false | |
| default: 'true' | |
| type: choice | |
| options: | |
| - 'true' | |
| - 'false' | |
| modules_json: | |
| description: "Optional JSON array override: [{\"repo\":\"...\",\"ref\":\"main\",\"os\":\"windows-latest\"}]" | |
| required: false | |
| default: "" | |
| permissions: | |
| contents: read | |
| # Serializes only this major's runs on the same ref; the Puppet 9 caller uses | |
| # its own group so the two majors' test phases proceed in parallel (§4.2). | |
| concurrency: | |
| group: compat-9-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| prepare: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| unit_matrix: ${{ steps.prepare.outputs.unit-matrix }} | |
| acceptance_matrix: ${{ steps.prepare.outputs.acceptance-matrix }} | |
| has_unit: ${{ steps.prepare.outputs.has-unit }} | |
| has_acceptance: ${{ steps.prepare.outputs.has-acceptance }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Prepare test matrix | |
| id: prepare | |
| uses: ./.github/actions/prepare-test-matrix | |
| with: | |
| modules-json: ${{ github.event.inputs.modules_json }} | |
| event-name: ${{ github.event_name }} | |
| lean: ${{ github.event.inputs.lean || 'true' }} | |
| caller-workflow-file: .github/workflows/compatibility-runner-puppet9.yml | |
| puppet-major: '9' | |
| window-hours: ${{ vars.PUPPET_CHANGE_WINDOW_HOURS || '48' }} | |
| stale-days: ${{ vars.PUPPET_STALE_DAYS || '30' }} | |
| test_unit: | |
| name: test / ${{ matrix.module.id }} / unit | |
| runs-on: ${{ matrix.module.os }} | |
| timeout-minutes: ${{ fromJSON(vars.PUPPET_JOB_TIMEOUT_MINUTES || '360') }} | |
| needs: prepare | |
| # GitHub Actions errors on an empty matrix vector instead of producing zero | |
| # combinations, so gate the job rather than relying on an empty unit_matrix. | |
| if: needs.prepare.outputs.has_unit == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| module: ${{ fromJson(needs.prepare.outputs.unit_matrix) }} | |
| env: | |
| PUPPET_CORE_API_KEY: ${{ secrets.PUPPET_CORE_API_KEY }} | |
| PUPPET_CORE_SOURCE_URL: https://rubygems-puppetcore.puppet.com | |
| PUPPET_COMPAT_METADATA_MODE: ${{ github.event.inputs.metadata_mode || 'warn' }} | |
| PUPPET_COMPAT_BUNDLE_PATH: .b | |
| PUPPET_ENFORCE_PRIVATE_SOURCE: "true" | |
| PUPPET_ENFORCE_NO_OPENVOX: "false" | |
| PUPPET_ENFORCE_EXACT_PUPPET_VERSION: "true" | |
| PUPPET_SPLIT_SOURCES: "true" | |
| PUPPET_STAGE_TIMEOUT_SECONDS: ${{ vars.PUPPET_STAGE_TIMEOUT_SECONDS || '1800' }} | |
| PUPPET_ACCEPTANCE_DEBUG: ${{ github.event.inputs.enable_debug || 'false' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Run module compatibility test | |
| uses: ./.github/actions/run-module-test | |
| with: | |
| module-json: ${{ toJson(matrix.module) }} | |
| module-id: ${{ matrix.module.id }} | |
| profile: ${{ github.event.inputs.profile || '9-latest-maintained' }} | |
| metadata-mode: ${{ github.event.inputs.metadata_mode || 'warn' }} | |
| enable-debug: ${{ github.event.inputs.enable_debug || 'false' }} | |
| test-mode: unit | |
| output-dir: o/${{ matrix.module.id }} | |
| artifact-name: compatibility-${{ matrix.module.id }}-unit | |
| test-lane: unit | |
| acceptance-target: unit | |
| prereqs-json: ${{ toJson(matrix.module.prereqs) }} | |
| - name: Upload module artifacts (unit) | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: compatibility-${{ matrix.module.id }}-unit | |
| path: o/${{ matrix.module.id }} | |
| if-no-files-found: warn | |
| test_acceptance: | |
| name: test / ${{ matrix.module.id }} / acceptance / ${{ matrix.module.target_id }} | |
| runs-on: ${{ matrix.module.os }} | |
| timeout-minutes: ${{ fromJSON(vars.PUPPET_JOB_TIMEOUT_MINUTES || '360') }} | |
| needs: prepare | |
| if: needs.prepare.outputs.has_acceptance == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| module: ${{ fromJson(needs.prepare.outputs.acceptance_matrix) }} | |
| env: | |
| PUPPET_CORE_API_KEY: ${{ secrets.PUPPET_CORE_API_KEY }} | |
| PUPPET_CORE_SOURCE_URL: https://rubygems-puppetcore.puppet.com | |
| PUPPET_COMPAT_METADATA_MODE: ${{ github.event.inputs.metadata_mode || 'warn' }} | |
| PUPPET_COMPAT_BUNDLE_PATH: .b | |
| PUPPET_ENFORCE_PRIVATE_SOURCE: "true" | |
| PUPPET_ENFORCE_NO_OPENVOX: "false" | |
| PUPPET_ENFORCE_EXACT_PUPPET_VERSION: "true" | |
| PUPPET_SPLIT_SOURCES: "true" | |
| PUPPET_STAGE_TIMEOUT_SECONDS: ${{ vars.PUPPET_STAGE_TIMEOUT_SECONDS || '1800' }} | |
| PUPPET_ACCEPTANCE_DEBUG: ${{ github.event.inputs.enable_debug || 'false' }} | |
| steps: | |
| - name: Checkout (for Beaker host seeding) | |
| uses: actions/checkout@v6 | |
| - name: Seed Beaker host mappings | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .tmp | |
| setfile="config/beaker/setfiles/${{ matrix.module.setfile }}.yml" | |
| echo "Reading host entries from $setfile" | |
| ruby -ryaml -e " | |
| data = YAML.safe_load(File.read(ARGV[0]), permitted_classes: [Symbol]) | |
| hosts = data.fetch('HOSTS', {}) | |
| hosts.each do |name, cfg| | |
| next unless cfg.is_a?(Hash) | |
| ip = cfg['ip'].to_s.strip | |
| next if ip.empty? | |
| puts \"#{ip} #{name}\" | |
| end | |
| " "$setfile" > .tmp/beaker-host-mappings.txt | |
| if [ ! -s .tmp/beaker-host-mappings.txt ]; then | |
| echo "No explicit host mappings found in setfile" | |
| exit 0 | |
| fi | |
| while read -r ip host; do | |
| [ -n "$ip" ] || continue | |
| [ -n "$host" ] || continue | |
| if getent hosts "$host" > /dev/null; then | |
| echo "Host already resolvable: $host" | |
| else | |
| echo "Adding host mapping: $ip $host" | |
| echo "$ip $host" | sudo tee -a /etc/hosts > /dev/null | |
| fi | |
| done < .tmp/beaker-host-mappings.txt | |
| echo "---- Relevant /etc/hosts entries ----" | |
| while read -r _ host; do | |
| [ -n "$host" ] || continue | |
| getent hosts "$host" || true | |
| done < .tmp/beaker-host-mappings.txt | |
| - name: Run module compatibility test (acceptance) | |
| uses: ./.github/actions/run-module-test | |
| with: | |
| module-json: ${{ toJson(matrix.module) }} | |
| module-id: ${{ matrix.module.id }} | |
| profile: ${{ github.event.inputs.profile || '9-latest-maintained' }} | |
| metadata-mode: ${{ github.event.inputs.metadata_mode || 'warn' }} | |
| enable-debug: ${{ github.event.inputs.enable_debug || 'false' }} | |
| test-mode: acceptance | |
| allow-acceptance: 'true' | |
| beaker-setfile: config/beaker/setfiles/${{ matrix.module.setfile }}.yml | |
| output-dir: o/${{ matrix.module.id }}-${{ matrix.module.target_id }} | |
| artifact-name: compatibility-${{ matrix.module.id }}-acceptance-${{ matrix.module.target_id }} | |
| test-lane: acceptance | |
| acceptance-target: ${{ matrix.module.target }} | |
| prereqs-json: ${{ toJson(matrix.module.prereqs) }} | |
| docker-mode: ${{ matrix.module.docker_mode || 'sshd' }} | |
| install-puppetserver: ${{ matrix.module.install_puppetserver || 'false' }} | |
| setup-commands: ${{ toJson(matrix.module.setup_commands) }} | |
| pre-acceptance-commands: ${{ toJson(matrix.module.pre_acceptance_commands) }} | |
| - name: Upload module artifacts (acceptance) | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: compatibility-${{ matrix.module.id }}-acceptance-${{ matrix.module.target_id }} | |
| path: o/${{ matrix.module.id }}-${{ matrix.module.target_id }} | |
| if-no-files-found: warn | |
| - name: Upload acceptance fallback diagnostics | |
| if: ${{ always() && (failure() || cancelled()) }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: compatibility-${{ matrix.module.id }}-acceptance-${{ matrix.module.target_id }}-diagnostics | |
| path: | | |
| o/${{ matrix.module.id }}-${{ matrix.module.target_id }} | |
| .tmp/beaker-host-mappings.txt | |
| if-no-files-found: warn | |
| publish: | |
| runs-on: ubuntu-latest | |
| needs: | |
| - prepare | |
| - test_unit | |
| - test_acceptance | |
| # Runs even when both test jobs were skipped (no-op run): the ledger still | |
| # needs reconciling against modules.json / KNOWN_* and the summary still | |
| # needs to say why nothing ran. | |
| if: always() && needs.prepare.result == 'success' | |
| permissions: | |
| contents: write | |
| # Shared across every major's caller (see docs/puppet-core-9-dual-major-support.md | |
| # §4.2) — the test-phase jobs above run under each caller's own concurrency | |
| # group, so majors test in parallel; only this ledger-write-and-push step is | |
| # serialized across them. Works the same whether publish's step logic lives | |
| # in a reusable workflow or (as here) a composite action, since concurrency | |
| # groups key off the group name, not the job's origin. | |
| concurrency: | |
| group: compat-ledger-${{ github.ref }} | |
| cancel-in-progress: false | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.ref_name }} | |
| fetch-depth: 0 | |
| - name: Publish compatibility results | |
| uses: ./.github/actions/publish-compatibility-results | |
| with: | |
| modules-json: ${{ github.event.inputs.modules_json }} | |
| has-unit: ${{ needs.prepare.outputs.has_unit }} | |
| has-acceptance: ${{ needs.prepare.outputs.has_acceptance }} | |
| stale-days: ${{ vars.PUPPET_STALE_DAYS || '30' }} |