diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0275e10 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is automatically updated as part of a puppet module baseline. +# The next baseline sync will overwrite any local changes made to this file. +# ------------------------------------------------------------------------------ +*.erb eol=lf +*.pp eol=lf +*.sh eol=lf +*.epp eol=lf +*.rb eol=lf diff --git a/.github/workflows/add_new_issue_to_triage_project.yml b/.github/workflows/add_new_issue_to_triage_project.yml new file mode 100644 index 0000000..f3c5b84 --- /dev/null +++ b/.github/workflows/add_new_issue_to_triage_project.yml @@ -0,0 +1,40 @@ +# Add new issues to triage project board (https://github.com/orgs/simp/projects/11) +# ------------------------------------------------------------------------------ +# +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# This pipeline uses the following GitHub Action Secrets: +# +# GitHub Secret variable Notes +# ------------------------------- --------------------------------------- +# AUTO_TRIAGE_TOKEN Token with appropriate permissions +# +# ------------------------------------------------------------------------------ +# +# +--- +name: Add new issues to triage project + +'on': + issues: + types: + - opened + - reopened + pull_request_target: + types: + - opened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.5.0 + with: + project-url: https://github.com/orgs/simp/projects/11 + github-token: ${{ secrets.AUTO_TRIAGE_TOKEN }} diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml new file mode 100644 index 0000000..9c0e683 --- /dev/null +++ b/.github/workflows/pr_tests.yml @@ -0,0 +1,142 @@ +# Run Puppet checks and test matrix on Pull Requests +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# +# The testing matrix considers ruby/puppet versions supported by SIMP and PE: +# ------------------------------------------------------------------------------ +# Release Puppet Ruby EOL +# PE 2021.Y 7.x 2.7 2025-02 (LTS) +# PE 2023.Y 8.x 3.2 Biannual updates +# +# https://puppet.com/docs/pe/latest/component_versions_in_recent_pe_releases.html +# https://puppet.com/misc/puppet-enterprise-lifecycle +# ============================================================================== +# +# https://docs.github.com/en/actions/reference/events-that-trigger-workflows +# +--- +name: PR Tests +'on': + pull_request: + types: [opened, reopened, synchronize] + +env: + PUPPET_VERSION: '~> 8' + +jobs: + puppet-syntax: + name: 'Puppet Syntax' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: "Install Ruby ${{matrix.puppet.ruby_version}}" + uses: ruby/setup-ruby@v1 # ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 + with: + ruby-version: 3.2 + bundler-cache: true + - run: "bundle exec rake syntax" + + puppet-style: + name: 'Puppet Style' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: "Install Ruby ${{matrix.puppet.ruby_version}}" + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - run: "bundle exec rake lint" + - run: "bundle exec rake metadata_lint" + + ruby-style: + if: false # TODO Modules will need: rubocop in Gemfile, .rubocop.yml + name: 'Ruby Style (experimental)' + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v3 + - name: "Install Ruby ${{matrix.puppet.ruby_version}}" + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - run: | + bundle show + bundle exec rake rubocop + + file-checks: + name: 'File checks' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: 'Install Ruby 3.2' + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - run: bundle exec rake check:dot_underscore + - run: bundle exec rake check:test_file + + releng-checks: + name: 'RELENG checks' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: 'Install Ruby ${{matrix.puppet.ruby_version}}' + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - name: 'Tags and changelogs' + run: | + bundle exec rake pkg:check_version + bundle exec rake pkg:compare_latest_tag[,true] + bundle exec rake pkg:create_tag_changelog + - name: 'Test-build the Puppet module' + run: 'bundle exec pdk build --force' + + spec-tests: + name: 'Puppet Spec' + needs: [puppet-syntax] + runs-on: ubuntu-latest + strategy: + matrix: + puppet: + - label: 'Puppet 7.x [SIMP 6.6/PE 2021.7]' + puppet_version: '~> 7.0' + ruby_version: '2.7' + experimental: false + - label: 'Puppet 8.x' + puppet_version: '~> 8.0' + ruby_version: '3.2' + experimental: false + fail-fast: false + env: + PUPPET_VERSION: ${{matrix.puppet.puppet_version}} + steps: + - uses: actions/checkout@v3 + - name: 'Install Ruby ${{matrix.puppet.ruby_version}}' + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{matrix.puppet.ruby_version}} + bundler-cache: true + - run: 'command -v rpm || if command -v apt-get; then sudo apt-get update; sudo apt-get install -y rpm; fi ||:' + - run: 'bundle exec rake spec' + continue-on-error: ${{matrix.puppet.experimental}} + +# dump_contexts: +# name: 'Examine Context contents' +# runs-on: ubuntu-latest +# steps: +# - name: Dump contexts +# env: +# GITHUB_CONTEXT: ${{ toJson(github) }} +# run: echo "$GITHUB_CONTEXT" +# diff --git a/.github/workflows/release_rpms.yml b/.github/workflows/release_rpms.yml new file mode 100644 index 0000000..04f1ef5 --- /dev/null +++ b/.github/workflows/release_rpms.yml @@ -0,0 +1,353 @@ +# Manual action to build, sign, and attach a release's RPMs +# ------------------------------------------------------------------------------ +# +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# This pipeline uses the following GitHub Action Secrets: +# +# GitHub Secret variable Notes +# ------------------------------- --------------------------------------- +# SIMP_CORE_REF_FOR_BUILDING_RPMS simp-core ref (tag) to use to build +# RPMs with `rake pkg:single` against +# `build/rpms/dependencies.yaml` +# SIMP_DEV_GPG_SIGNING_KEY GPG signing key's secret key +# SIMP_DEV_GPG_SIGNING_KEY_ID User ID (name) of signing key +# SIMP_DEV_GPG_SIGNING_KEY_PASSPHRASE Passphrase to use GPG signing key +# +# ------------------------------------------------------------------------------ +# +# * This is a workflow_dispatch action, which can be triggered manually or from +# other workflows/API. +# +# * If triggered by another workflow, it will be necessary to provide a GitHub +# access token via the the `target_repo_token` parameter +# +# +--- +name: 'RELENG: Build + attach RPMs to GitHub Release' + +'on': + workflow_dispatch: + inputs: + release_tag: + description: "Release tag" + required: true + clobber: + description: "Clobber identical assets?" + required: false + default: 'yes' + clean: + description: "Wipe all release assets first?" + required: false + default: 'no' + autocreate_release: + # A GitHub release is needed to upload artifacts to, and some repos + # (e.g., forked mirrors) only have tags. + description: "Create release if missing? (tag must exist)" + required: false + default: 'yes' + build_container_os: + description: "Build container OS" + required: true + default: 'centos8' + target_repo: + description: "Target repo (instead of this one)" + required: false + # WARNING: To avoid exposing secrets in the log, only use this token with + # action/script's `github-token` parameter, NEVER in `env:` vars + target_repo_token: + description: "API token for uploading to target repo" + required: false + path_to_build: + # Example: simp-core builds pupmod from . and simp* from src/assets/simp + description: "Subpath to alternative RPM project" + required: false + dry_run: + description: "Dry run (Test-build RPMs)" + required: false + default: 'no' + # verbose: + # description: 'Verbose RPM builds when "yes"' + # required: false + # default: 'no' + rebuild_number: + description: 'If this is an RPM rebuild, put the number of the rebuild here' + required: false + default: '' + + +env: + TARGET_REPO: ${{ (github.event.inputs.target_repo != null && format('{0}/{1}', github.repository_owner, github.event.inputs.target_repo)) || github.repository }} + RELEASE_TAG: ${{ github.event.inputs.release_tag }} + +jobs: + create-and-attach-rpms-to-github-release: + name: > + Build and attach RPMs to Release: + ${{ (github.event.inputs.target_repo != null && format('{0}/{1}', github.repository_owner, github.event.inputs.target_repo)) || github.repository }} + ${{ github.event.inputs.release_tag }} + (build os: ${{ github.event.inputs.build_container_os }}) + runs-on: ubuntu-20.04 + steps: + - name: "Validate inputs" + id: validate-inputs + run: | + if ! [[ "$TARGET_REPO" =~ ^[a-z0-9][a-z0-9-]+/[a-z0-9][a-z0-9_-]+$ ]]; then + printf '::error ::Target repository name has invalid format: %s\n' "$TARGET_REPO" + exit 88 + fi + + if [[ "$RELEASE_TAG" =~ ^(simp-|v)?([0-9]+\.[0-9]+\.[0-9]+)(-(rc|RC|[Aa]lpha|[Bb]eta|pre|post)?([0-9]+)?)?$ ]]; then + if [ -n "${BASH_REMATCH[5]}" ]; then + echo "{prebuild_number}={${BASH_REMATCH[5]#-}}" >> $GITHUB_OUTPUT + fi + if [ -n "${BASH_REMATCH[3]}" ]; then + echo "{prebuild_suffix}={${BASH_REMATCH[3]#-}}" >> $GITHUB_OUTPUT + fi + if [ -n "${BASH_REMATCH[2]}" ]; then + echo "{build_semver}={${BASH_REMATCH[2]}}" >> $GITHUB_OUTPUT + fi + else + printf '::error ::Release Tag format is not SemVer, X.Y.Z-R, X.Y.Z-: "%s"\n' "$RELEASE_TAG" + exit 88 + fi + + - name: > + Query info for ${{ env.TARGET_REPO }} + release ${{ github.event.inputs.release_tag }} ${{ steps.validate-inputs.outputs.prebuild_suffix }} + build os ${{ github.event.inputs.build_container_os }} + (autocreate_release = '${{ github.event.inputs.autocreate_release }}') + id: release-api + env: + AUTOCREATE_RELEASE: ${{ github.event.inputs.autocreate_release }} + PREBUILD_TAG: ${{ steps.validate-inputs.outputs.prebuild_suffix }} + uses: actions/github-script@v6 + with: + github-token: ${{ github.event.inputs.target_repo_token || secrets.GITHUB_TOKEN }} + script: | + const [owner, repo] = process.env.TARGET_REPO.split('/') + const tag = process.env.RELEASE_TAG + const autocreate_release = (process.env.AUTOCREATE_RELEASE == 'yes') + const owner_data = { owner: owner, repo: repo } + const release_data = Object.assign( {tag: tag}, owner_data ) + const prerelease = process.env.PREBUILD_TAG ? true : false + const create_release_data = Object.assign( {tag_name: tag, prerelease: prerelease}, owner_data ) + const tag_data = Object.assign( {ref: `tags/${tag}`}, owner_data ) + + function id_from_release(data) { + console.log( ` >> Release for ${owner}/${repo}, tag ${tag}` ) + console.log( ` >>>> release_id: ${data.id}` ) + return data.id + } + + function throw_error_unless_should_autocreate_release(err){ + if (!( err.name == 'HttpError' && err.status == 404 && autocreate_release )){ + core.error(`Error finding release for tag ${tag}: ${err.name}`) + throw err + } + } + + async function autocreate_release_if_appropriate(err){ + throw_error_unless_should_autocreate_release(err) + core.warning(`Can't find release for tag ${tag} and tag exists, auto-creating release`) + + return await github.request( 'GET /repos/{owner}/{repo}/git/matching-refs/{ref}', tag_data ).then ( + result => { + // Must already have a tag + if (result.data.length == 0) { throw `Can't find tag ${tag} in repo ${owner}/${repo}` } + return result + } + ).then( + async result => { + return await github.request( 'POST /repos/{owner}/{repo}/releases', create_release_data).then( + result=>{ + release_id = id_from_release(result.data) + console.log(` ++ created auto release ${release_id}` ) + return release_id + }, + post_err =>{ + core.error('Error auto-creating release') + throw post_err + } + ) + } + ) + } + + await github.request('GET /repos/{owner}/{repo}/releases/tags/{tag}', release_data ).then( + async result => { return await id_from_release(result.data) }, + async err => { return await autocreate_release_if_appropriate(err) } + ).then( + release_id => { + if (!release_id){ + throw `Could not get release for ${tag} for repo ${owner}:${repo}` + } + console.log( ` **** release_id: ${release_id}` ) + core.setOutput('id', release_id) + }, + err => { throw err } + ) + + - name: Checkout code + uses: actions/checkout@v3 + with: + repository: ${{ env.TARGET_REPO }} + ref: ${{ env.RELEASE_TAG }} + clean: true + fetch-depth: 0 + + - name: 'Customize RPM Release tag via build/rpm_metadata/release (pre-release only)' + if: steps.validate-inputs.outputs.prebuild_suffix + env: + BUILD_SEMVER: ${{ steps.validate-inputs.outputs.build_semver }} + PREBUILD_TAG: ${{ steps.validate-inputs.outputs.prebuild_suffix }} + PREBUILD_NUMBER: ${{ steps.validate-inputs.outputs.prebuild_number }} + # Note: To accomodate the capabilities of EL7's version of RPM, the + # release number is formatted according to the Fedora Packaging + # Guidelines' "Traditional versioning" conventions: + # + # - https://fedoraproject.org/en-US/packaging-guidelines/Versioning/ + # - https://fedoraproject.org/wiki/Package_Versioning_Examples + # + run: | + mkdir -p build/rpm_metadata + # Special case for simp-doc's unique data format in /release + if [[ "$TARGET_REPO" =~ ^simp\/simp-doc$ ]]; then + echo "version: $BUILD_SEMVER" > build/rpm_metadata/release + echo "release: 0.${PREBUILD_NUMBER:-$GITHUB_RUN_NUMBER}.${PREBUILD_TAG}" >> build/rpm_metadata/release + printf '::warning ::Added file build/rpm_metadata/release with content "%s"\n' "$(cat build/rpm_metadata/release)" + else + echo "0.${PREBUILD_NUMBER:-$GITHUB_RUN_NUMBER}.${PREBUILD_TAG}" > build/rpm_metadata/release + printf '::warning ::Added file build/rpm_metadata/release with content "%s"\n' "$(cat build/rpm_metadata/release)" + fi + + - name: 'Customize RPM Release tag via build/rpm_metadata/release (RPM rebuild)' + if: ${{ github.event.inputs.rebuild_number != '' }} + env: + BUILD_SEMVER: ${{ steps.validate-inputs.outputs.build_semver }} + REBUILD_NUMBER: ${{ github.event.inputs.rebuild_number }} + run: | + mkdir -p build/rpm_metadata + # simp-doc uses a unique data format in /release + if [[ "$TARGET_REPO" =~ ^simp\/simp-doc$ ]]; then + echo "version: $BUILD_SEMVER" > build/rpm_metadata/release + echo "release: $REBUILD_NUMBER" > build/rpm_metadata/release + else + echo "$REBUILD_NUMBER" > build/rpm_metadata/release + fi + printf '::warning ::Added file build/rpm_metadata/release with content "%s"\n' "$(cat build/rpm_metadata/release)" + + - name: > + Build & Sign RPMs for + ${{ github.event.inputs.release_tag }} + Release (${{ github.event.inputs.build_container_os }}) + uses: simp/github-action-build-and-sign-pkg-single-rpm@v2 + id: build-and-sign-rpm + with: + gpg_signing_key: ${{ secrets.SIMP_DEV_GPG_SIGNING_KEY }} + gpg_signing_key_id: ${{ secrets.SIMP_DEV_GPG_SIGNING_KEY_ID }} + gpg_signing_key_passphrase: ${{ secrets.SIMP_DEV_GPG_SIGNING_KEY_PASSPHRASE }} + simp_core_ref_for_building_rpms: ${{ secrets.SIMP_CORE_REF_FOR_BUILDING_RPMS }} + simp_builder_docker_image: 'docker.io/simpproject/simp_build_${{ github.event.inputs.build_container_os }}:latest' + path_to_build: "${{ (github.event.inputs.path_to_build != null && format('{0}/{1}', github.workspace, github.event.inputs.path_to_build)) || github.workspace }}" + verbose: 'no' # ${{ github.event.inputs.verbose }} + + - name: "Wipe all previous assets from GitHub Release (when clean == 'yes')" + if: ${{ github.event.inputs.clean == 'yes' && github.event.inputs.dry_run != 'yes' }} + uses: actions/github-script@v6 + env: + release_id: ${{ steps.release-api.outputs.id }} + with: + github-token: ${{ github.event.inputs.target_repo_token || secrets.GITHUB_TOKEN }} + script: | + const release_id = process.env.release_id + const [owner, repo] = process.env.TARGET_REPO.split('/') + const existingAssets = await github.rest.repos.listReleaseAssets({ owner, repo, release_id }) + + console.log( ` !! !! Wiping ALL uploaded assets for ${owner}/${repo} release (id: ${release_id})`) + existingAssets.data.forEach(async function(asset){ + asset_id = asset.id + console.log( ` !! !! !! Wiping existing asset for ${asset.name} (id: ${asset_id})`) + await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id }) + }) + + - name: "Upload RPM file(s) to GitHub Release (dry_run != 'yes')" + if: ${{ github.event.inputs.dry_run != 'yes' }} + uses: actions/github-script@v6 + env: + rpm_file_paths: ${{ steps.build-and-sign-rpm.outputs.rpm_file_paths }} + rpm_gpg_file: ${{ steps.build-and-sign-rpm.outputs.rpm_gpg_file }} + release_id: ${{ steps.release-api.outputs.id }} + clobber: ${{ github.event.inputs.clobber }} + clean: ${{ github.event.inputs.clean }} + dry_run: ${{ github.event.inputs.dry_run }} + with: + github-token: ${{ github.event.inputs.target_repo_token || secrets.GITHUB_TOKEN }} + script: | + const path = require('path') + const fs = require('fs') + + async function clobberAsset (name, owner, repo, release_id ){ + console.log( ` -- clobber asset ${name}: owner: ${owner} repo: ${repo} release_id: ${release_id}` ) + + const existingAssets = await github.rest.repos.listReleaseAssets({ owner, repo, release_id }) + const matchingAssets = existingAssets.data.filter(item => item.name == name); + if ( matchingAssets.length > 0 ){ + asset_id = matchingAssets[0].id + console.log( ` !! !! Clobbering existing asset for ${name} (id: ${asset_id})`) + await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id }) + return(true) + } + return(false) + } + + async function uploadAsset(owner, repo, release_id, file, assetContentType ){ + console.log( `\n\n -- uploadAsset: owner: ${owner} repo: ${repo} release_id: ${release_id}, file: ${file}\n` ) + const name = path.basename(file) + + const data = fs.readFileSync(file) + const contentLength = fs.statSync(file).size + const headers = { + 'content-type': assetContentType, + 'content-length': contentLength + }; + + console.log( ` == Uploading asset ${name}: ${assetContentType}` ) + const uploadAssetResponse = await github.rest.repos.uploadReleaseAsset({ + owner, repo, release_id, data, name, headers, + }) + return( uploadAssetResponse ); + } + + console.log('== start'); + const release_id = process.env.release_id + const [owner, repo] = process.env.TARGET_REPO.split('/') + const clobber = process.env.clobber == 'yes'; + const rpm_files = process.env.rpm_file_paths.split(/[\r\n]+/); + const rpm_gpg_file = process.env.rpm_gpg_file; + + let uploaded_files = rpm_files.concat(rpm_gpg_file).map(function(file){ + const name = path.basename(file) + var content_type = 'application/pgp-keys' + if( name.match(/\.rpm$/) ){ + content_type = 'application/octet-stream' + } + + let conditionalClobber = new Promise((resolve,reject) => { + if ( clobber ){ + resolve(clobberAsset( name, owner, repo, release_id )) + return + } + resolve( false ) + }) + + conditionalClobber.then((clobbered)=> { + uploadAsset(owner, repo, release_id, file, content_type ) + }).then(result => result ) + }) + console.log('== done') diff --git a/.github/workflows/tag_deploy.yml b/.github/workflows/tag_deploy.yml new file mode 100644 index 0000000..10a5d1c --- /dev/null +++ b/.github/workflows/tag_deploy.yml @@ -0,0 +1,196 @@ +# Build & Deploy Puppet module & GitHub release when a SemVer tag is pushed +# ------------------------------------------------------------------------------ +# +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a standardized asset baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# +# This pipeline uses the following GitHub Action Secrets: +# +# GitHub Secret variable Notes +# ------------------------------- --------------------------------------- +# PUPPETFORGE_API_TOKEN +# SIMP_CORE_REF_FOR_BUILDING_RPMS simp-core ref (tag) to use to build +# RPMs with `rake pkg:single` +# SIMP_DEV_GPG_SIGNING_KEY GPG signing key's secret key +# SIMP_DEV_GPG_SIGNING_KEY_ID User ID (name) of signing key +# SIMP_DEV_GPG_SIGNING_KEY_PASSPHRASE Passphrase to use GPG signing key +# +# ------------------------------------------------------------------------------ +# +# NOTES: +# +# * The CHANGELOG text is altered to remove RPM-style date headers, which don't +# render well as markdown on the GitHub release pages +# +--- +name: 'Tag: Release to GitHub w/RPMs + Puppet Forge' + +'on': + push: + tags: + # NOTE: These filter patterns aren't actually regexes: + # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet + - '[0-9]+\.[0-9]+\.[0-9]+' + - '[0-9]+\.[0-9]+\.[0-9]+\-[a-z]+[0-9]+' + +env: + PUPPET_VERSION: '~> 8' + +jobs: + releng-checks: + name: "RELENG checks" + if: github.repository_owner == 'simp' + runs-on: ubuntu-latest + steps: + - name: "Assert '${{ github.ref }}' is a tag" + run: '[[ "$GITHUB_REF" =~ ^refs/tags/ ]] || { echo "::error ::GITHUB_REF is not a tag: ${GITHUB_REF}"; exit 1 ; }' + - uses: actions/checkout@v3 + with: + ref: ${{ github.ref }} + clean: true + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - run: bundle exec rake pkg:check_version + - run: bundle exec rake pkg:compare_latest_tag + - run: bundle exec rake pkg:create_tag_changelog + - run: bundle exec rake metadata_lint + - name: "Test that Puppet module can build" + run: "bundle exec pdk build --force" + + + create-github-release: + name: Deploy GitHub Release + needs: + - releng-checks + if: github.repository_owner == 'simp' + runs-on: ubuntu-latest + outputs: + prerelease: ${{ steps.tag-check.outputs.prerelease }} + tag: ${{ steps.tag-check.outputs.tag }} + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.ref }} + clean: true + fetch-depth: 0 + + - name: Get tag & annotation info (${{github.ref}}) + id: tag-check + run: | + tag="${GITHUB_REF/refs\/tags\//}" + annotation="$(git for-each-ref "$GITHUB_REF" --format='%(contents)' --count=1)" + annotation_title="$(echo "$annotation" | head -1)" + + if [[ "$tag" =~ ^(simp-|v)?[0-9]+\.[0-9]+\.[0-9]+(-(rc|alpha|beta|pre|post)?([0-9]+)?)?$ ]]; then + if [ -n "${BASH_REMATCH[2]}" ]; then + prerelease=yes + annotation_title="Pre-release of ${tag}" + fi + else + printf '::error ::Release Tag format is not SemVer, X.Y.Z-R, X.Y.Z-: "%s"\n' "$RELEASE_TAG" + exit 88 + fi + + echo "tag=$tag" | tee -a "$GITHUB_OUTPUT" + echo "prerelease=$prerelease" | tee -a "$GITHUB_OUTPUT" + echo "TARGET_TAG=$tag" | tee -a "$GITHUB_ENV" + + # Prepare annotation body as a file for the next step + # + # * The GitHub Release renders the text in this file as markdown + # * The `perl -pe` removes RPM-style date headers from the CHANGELOG, + # because they don't render well as markdown on the Release page + echo "RELEASE_MESSAGE<> "$GITHUB_ENV" + printf '%s\n\n' "$annotation_title" >> "$GITHUB_ENV" + echo "$annotation" | tail -n +2 | \ + perl -pe 'BEGIN{undef $/;} s/\n\* (Mon|Tue|Wed|Thu|Fri|Sat|Sun) .*?\n//smg;' >> "$GITHUB_ENV" + echo "EOF$$" >> "$GITHUB_ENV" + + - name: Create Release + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IS_PRERELEASE: ${{ steps.tag-check.outputs.prerelease }} + run: | + echo "${RELEASE_MESSAGE}" > /tmp/.commit-msg.txt + args=(-F /tmp/.commit-msg.txt) + [[ "$IS_PRERELEASE" == yes ]] && args+=(--prerelease) + + gh release create ${args[@]} "$TARGET_TAG" + + build-and-attach-rpms: + name: Trigger RPM release + needs: + - create-github-release + if: github.repository_owner == 'simp' + runs-on: ubuntu-latest + env: + TARGET_REPO: ${{ github.repository }} + strategy: + matrix: + os: + - centos7 + - centos8 + steps: + - name: Trigger RPM release workflow (${{ matrix.os }}) + uses: actions/github-script@v6 + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + TARGET_TAG: ${{ needs.create-github-release.outputs.tag }} + with: + github-token: ${{ secrets.SIMP_AUTO_GITHUB_TOKEN__REPO_SCOPE }} + script: | + console.log( `== Building tag: '${ process.env.TARGET_TAG }' for os '${{ matrix.os}}'` ) + const [owner, repo] = process.env.TARGET_REPO.split('/') + await github.request('POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches', { + owner: owner, + repo: repo, + workflow_id: 'release_rpms.yml', + ref: process.env.DEFAULT_BRANCH, + inputs: { + release_tag: process.env.TARGET_TAG, + clean: 'no', + clobber: 'yes', + build_container_os: '${{ matrix.os }}' + } + }).then((result) => { + console.log( `== Submitted workflow dispatch to build RPMs from ${{ matrix.os }}: status ${result.status}` ) + }) + + deploy-to-puppet-forge: + name: 'Deploy PuppetForge Release' + needs: + - create-github-release + if: (github.repository_owner == 'simp') && (needs.create-github-release.outputs.prerelease != 'yes') + runs-on: ubuntu-latest + env: + PUPPETFORGE_API_TOKEN: ${{ secrets.PUPPETFORGE_API_TOKEN }} + FORGE_USER_AGENT: GitHubActions-ForgeReleng-Workflow/0.4.1 (Purpose/forge-ops-for-${{ github.event.repository.name }}) + FORGE_API_URL: https://forgeapi.puppet.com/v3/releases + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: ${{ github.ref }} + clean: true + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + - name: Build Puppet module (PDK) + run: bundle exec pdk build --force + - name: Deploy to Puppet Forge (skipped when prerelease) + run: | + curl -X POST --silent --show-error --fail \ + --user-agent "$FORGE_USER_AGENT" \ + --header "Authorization: Bearer ${PUPPETFORGE_API_TOKEN}" \ + --form "file=@$(find $PWD/pkg -name ''*.tar.gz'')" \ + "$FORGE_API_URL" diff --git a/.github/workflows/validate_tokens.yml b/.github/workflows/validate_tokens.yml new file mode 100644 index 0000000..cfbba7b --- /dev/null +++ b/.github/workflows/validate_tokens.yml @@ -0,0 +1,68 @@ +# Validate API tokens in GitHub Secrets against their respective services +# ------------------------------------------------------------------------------ +# +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to this file! +# +# ============================================================================== +# +# This pipeline uses the following GitHub Action Secrets: +# +# GitHub Secret variable Type Notes +# ------------------------ -------- ---------------------------------------- +# PUPPETFORGE_API_TOKEN Required +# NO_SCOPE_GITHUB_TOKEN Required GitHub token (should have no scopes) +# The secure vars will be filtered in GitHub Actions log output, and aren't +# provided to untrusted builds (i.e, triggered by PR from another repository) +# +--- +name: 'Manual: Validate API tokens' + +'on': + - workflow_dispatch + +jobs: + puppetforge: + name: 'Puppet Forge token authenticates with API' + runs-on: ubuntu-latest + env: + PUPPETFORGE_API_TOKEN: ${{ secrets.PUPPETFORGE_API_TOKEN }} + FORGE_USER_AGENT: GitHubActions-ForgeReleng-Workflow/0.4.0 (Purpose/forge-ops-for-${{ github.event.repository.name }}) + steps: + - run: | + curl -sS --fail --silent --show-error \ + --user-agent "$FORGE_USER_AGENT" \ + --header "Authorization: Bearer ${PUPPETFORGE_API_TOKEN:-default_content_to_cause_401_response}" \ + https://forgeapi.puppet.com/v3/users > /dev/null + + github-no-scope: + name: 'No-scope GitHub token has NO scopes' + runs-on: ubuntu-latest + env: + GITHUB_ORG: ${{ github.event.organization.login }} + NO_SCOPE_GITHUB_TOKEN: ${{secrets.NO_SCOPE_GITHUB_TOKEN}} + steps: + - name: Test token scopes with curl (expect no scopes) + run: | + if ! response="$(curl -I --http1.0 --fail --silent --show-error \ + --header 'Content-Type: application/json' \ + --header "Authorization: token ${NO_SCOPE_GITHUB_TOKEN:-default_content_to_cause_error}" \ + "https://api.github.com/users/${GITHUB_ORG}")" 2>/tmp/x.$$.err; then + echo "::error ::$(cat /tmp/x.$$.err)" + exit 1 + fi + + if ! scopes="$(echo "$response" | grep '^X-OAuth-Scopes:' )"; then + echo "::error ::No X-OAuth-Scopes in response headers!" + echo "::debug ::$response" + exit 1 + fi + scopes="$( echo "$scopes" | strings )" + if echo "$scopes" | awk -F: '{print $2}' | grep -E '\w' ; then + echo "::error ::The NO_SCOPE_GITHUB_TOKEN token has scopes! (${scopes})" + echo "::debug ::${scopes}" + exit 1 + fi diff --git a/.gitignore b/.gitignore index 3ee72d0..410b067 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,27 @@ +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is automatically updated as part of a puppet module baseline. +# The next baseline sync will overwrite any local changes made to this file. +# ------------------------------------------------------------------------------ .*.sw? .yardoc -dist/ -pkg/ -spec/fixtures/ -spec/rp_env/ -!/spec/hieradata/default.yaml -!/spec/fixtures/site.pp -.rspec_system -.vagrant/ -.bundle/ -Gemfile.lock -vendor/ -junit/ -log/ -doc/ +.idea/ +dist +/pkg +# Read everything in fixtures +/spec/fixtures/* +# Un-ignore hieradata +!/spec/fixtures/hieradata/* +# Except this one, which is auto-generated +/spec/fixtures/hieradata/hiera.yaml +/spec/rp_env +/.rspec_system +/.vagrant +/.bundle +/.vendor +/vendor +/junit +/log +/doc +/Gemfile.lock diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e9a33fd..25c663d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,35 +1,106 @@ +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is updated automatically as part of a puppet module baseline. +# +# The next baseline sync will overwrite any local changes to everything above +# the line "# Repo-specific content" +# ------------------------------------------------------------------------------ # The testing matrix considers ruby/puppet versions supported by SIMP and PE: # -# https://puppet.com/docs/pe/2019.0/component_versions_in_recent_pe_releases.html +# https://puppet.com/docs/pe/latest/component_versions_in_recent_pe_releases.html # https://puppet.com/misc/puppet-enterprise-lifecycle -# https://puppet.com/docs/pe/2018.1/overview/getting_support_for_pe.html # ------------------------------------------------------------------------------ -# Release Puppet Ruby EOL -# SIMP 6.3 5.5.10 2.4.5 TBD*** -# PE 2018.1 5.5.8 2.4.5 2020-05 (LTS)*** -# PE 2019.0 6.0 2.5.1 2019-08-31^^^ -# -# *** = Modules created for SIMP 6.3+ are not required to support Puppet < 5.5 +# Release Puppet Ruby EOL +# PE 2021.7 7.30 2.7.8 2025-02 (LTS) +# PE 2023.8 8.6 3.2.3 TBD --- + stages: - - 'sanity' - 'validation' - 'acceptance' - 'compliance' - 'deployment' variables: - PUPPET_VERSION: 'UNDEFINED' # <- Matrixed jobs MUST override this (or fail) - BUNDLER_VERSION: '1.17.1' + # PUPPET_VERSION is a canary variable! + # + # The value `UNDEFINED` will (intentionally) cause `bundler install|update` to + # fail. The intended value for PUPPET_VERSION is provided by the `pup_#` YAML + # anchors. If it is still `UNDEFINED`, all the other setting from the job's + # anchor are also missing. + PUPPET_VERSION: 'UNDEFINED' # <- Matrixed jobs MUST override this (or fail) + BUNDLER_VERSION: '2.4.22' + SIMP_MATRIX_LEVEL: '1' + SIMP_FORCE_RUN_MATRIX: 'no' # Force dependencies into a path the gitlab-runner user can write to. # (This avoids some failures on Runners with misconfigured ruby environments.) - GEM_HOME: .vendor/gem_install + GEM_HOME: .vendor/gem_install BUNDLE_CACHE_PATH: .vendor/bundle - BUNDLE_PATH: .vendor/bundle - BUNDLE_BIN: .vendor/gem_install/bin - BUNDLE_NO_PRUNE: 'true' + BUNDLE_PATH: .vendor/bundle + BUNDLE_BIN: .vendor/gem_install/bin + BUNDLE_NO_PRUNE: 'true' + +.snippets: + before_beaker_google: + # Logic for beaker-google environments + - echo -e "\e[0Ksection_start:`date +%s`:before_script05[collapsed=true]\r\e[0KGCP environment checks" + - "if [ \"$BEAKER_HYPERVISOR\" == google ]; then mkdir -p ~/.ssh; chmod 700 ~/.ssh; test -f ~/.ssh/google_compute_engine || ssh-keygen -f ~/.ssh/google_compute_engine < /dev/null; echo 'gem \"beaker-google\"' >> Gemfile.local ; fi" # yamllint disable rule:line-length + - echo -e "\e[0Ksection_end:`date +%s`:before_script05\r\e[0K" + before: + # Print important environment variables that may affect this job + - 'ruby -e "puts %(\n\n), %q(=)*80, %(\nSIMP-relevant Environment Variables:\n\n#{e=ENV.keys.grep(/^PUPPET|^SIMP|^BEAKER|MATRIX|GOOGLE/); pad=((e.map{|x| x.size}.max||0)+1); e.map{|v| %( * #{%(#{v}:).ljust(pad)} #{39.chr + ENV[v] + 39.chr}\n)}.join}\n), %q(=)*80, %(\n\n)" || :' # yamllint disable rule:line-length + + - echo -e "\e[0Ksection_start:`date +%s`:before_script10[collapsed=true]\r\e[0KDiagnostic ruby & gem information" + # Diagnostic ruby & gem information + - 'which ruby && ruby --version || :' + - "[[ $- == *i* ]] && echo 'Interactive shell session' || echo 'Non-interactive shell session'" + - "shopt -q login_shell && echo 'Login shell' || echo 'Not a login shell'" + - 'rvm ls || :' + - echo -e "\e[0Ksection_end:`date +%s`:before_script10\r\e[0K" + + # If RVM is available, make SURE it's using the right Ruby: + # * Source rvm (to run in non-login shells) + # * Use $MATRIX_RUBY_VERSION ruby, install if not present + - echo -e "\e[0Ksection_start:`date +%s`:before_script20[collapsed=true]\r\e[0KEnsure RVM & ruby is installed" + - "if command -v rvm; then if declare -p rvm_path &> /dev/null; then source \"${rvm_path}/scripts/rvm\"; else source \"$HOME/.rvm/scripts/rvm\" || source /etc/profile.d/rvm.sh; fi; fi" + - >- + if command -v rvm && ! grep rvm_install_on_use_flag=1 ~/.rvmrc; then + echo rvm_install_on_use_flag=1 >> ~/.rvmrc + || echo '== WARNING: ~/.rvmrc is missing rvm_install_on_use_flag=1 and I failed to add it'; fi + - "if command -v rvm; then rvm use \"$MATRIX_RUBY_VERSION\"; else echo \"rvm not detected; skipping 'rvm use'\"; fi" + - 'ruby --version || :' + - 'gem list sync || :' + - echo -e "\e[0Ksection_end:`date +%s`:before_script20\r\e[0K" + + # Bundle gems (preferring cached > local > downloaded resources) + # * Try to use cached and local resources before downloading dependencies + - echo -e "\e[0Ksection_start:`date +%s`:before_script30[collapsed=true]\r\e[0KBundle gems (preferring cached > local > downloaded resources)" + - 'declare GEM_BUNDLER_VER=(-v "~> ${BUNDLER_VERSION:-2.4.22}")' + - 'declare GEM_INSTALL_CMD=(gem install --no-document)' + - 'declare BUNDLER_INSTALL_CMD=(bundle install --no-binstubs --jobs $(nproc) "${FLAGS[@]}")' + - 'mkdir -p ${GEM_HOME} ${BUNDLER_BIN}' + - 'gem list -ie "${GEM_BUNDLER_VER[@]}" --silent bundler || "${GEM_INSTALL_CMD[@]}" --local "${GEM_BUNDLER_VER[@]}" bundler || "${GEM_INSTALL_CMD[@]}" "${GEM_BUNDLER_VER[@]}" bundler' + - 'rm -rf pkg/ || :' + - >- + bundle check + || rm -f Gemfile.lock + && ("${BUNDLER_INSTALL_CMD[@]}" --local + || "${BUNDLER_INSTALL_CMD[@]}" + || bundle pristine + || "${BUNDLER_INSTALL_CMD[@]}") + || { echo "PIPELINE: Bundler could not install everything (see log output above)" && exit 99 ; } + - echo -e "\e[0Ksection_end:`date +%s`:before_script30\r\e[0K" + + # Diagnostic bundler, ruby, and gem checks: + - echo -e "\e[0Ksection_start:`date +%s`:before_script40[collapsed=true]\r\e[0KDiagnostic bundler, ruby, and gem checks" + - 'bundle exec rvm ls || :' + - 'bundle exec which ruby || :' + - 'bundle show sync || :' + - 'bundle exec gem list sync || :' + - echo -e "\e[0Ksection_end:`date +%s`:before_script40\r\e[0K" # bundler dependencies and caching # @@ -38,53 +109,161 @@ variables: # -------------------------------------- .setup_bundler_env: &setup_bundler_env cache: - untracked: true key: "${CI_PROJECT_NAMESPACE}_ruby-${MATRIX_RUBY_VERSION}_bundler" paths: - '.vendor' before_script: - - 'ruby -e "puts %(\n\n), %q(=)*80, %(\nSIMP-relevant Environment Variables:\n\n#{e=ENV.keys.grep(/^PUPPET|^SIMP|^BEAKER|MATRIX/); pad=e.map{|x| x.size}.max+1; e.map{|v| %( * #{%(#{v}:).ljust(pad)} #{39.chr + ENV[v] + 39.chr}\n)}.join}\n), %q(=)*80, %(\n\n)"' - - 'declare GEM_BUNDLER_VER=(-v "~> ${BUNDLER_VERSION:-1.17.1}")' - - 'declare GEM_INSTALL_CMD=(gem install --no-document)' - - 'declare BUNDLER_INSTALL_CMD=(bundle install --no-binstubs --jobs $(nproc) "${FLAGS[@]}")' - - 'mkdir -p ${GEM_HOME} ${BUNDLER_BIN}' - - 'gem list -ie "${GEM_BUNDLER_VER[@]}" --silent bundler || "${GEM_INSTALL_CMD[@]}" --local "${GEM_BUNDLER_VER[@]}" bundler || "${GEM_INSTALL_CMD[@]}" "${GEM_BUNDLER_VER[@]}" bundler' - - 'rm -rf pkg/ || :' - - 'bundle check || rm -f Gemfile.lock && ("${BUNDLER_INSTALL_CMD[@]}" --local || "${BUNDLER_INSTALL_CMD[@]}" || bundle pristine || "${BUNDLER_INSTALL_CMD[@]}") || { echo "PIPELINE: Bundler could not install everything (see log output above)" && exit 99 ; }' + !reference [.snippets, before] + + +# Assign a matrix level when your test will run. Heavier jobs get higher numbers +# NOTE: To skip all jobs with a SIMP_MATRIX_LEVEL, set SIMP_MATRIX_LEVEL=0 + +.relevant_file_conditions_trigger_spec_tests: &relevant_file_conditions_trigger_spec_tests + changes: + - .gitlab-ci.yml + - .fixtures.yml + - "spec/spec_helper.rb" + - "spec/{classes,unit,defines,type_aliases,types,hosts,lib}/**/*.rb" + - "{SIMP,data,manifests,files,types,lib,functions}/**/*" + - "templates/**/*.{erb,epp}" + - "Gemfile" + exists: + - "spec/{classes,unit,defines,type_aliases,types,hosts}/**/*_spec.rb" + +.relevant_file_conditions_trigger_acceptance_tests: &relevant_file_conditions_trigger_acceptance_tests + changes: + - .gitlab-ci.yml + - .fixtures.yml + - "spec/spec_helper_acceptance.rb" + - "spec/{helpers,acceptance}/**/*" + - "spec/inspec_*/**/*" + - "{SIMP,data,manifests,files,types,lib,functions}/**/*" + - "templates/**/*.{erb,epp}" + - "Gemfile" + exists: + - "spec/acceptance/**/*_spec.rb" + +# For some reason, the rule regexes stopped matching line starts inside +# $CI_COMMIT_MESSAGE with carets /^/, so we're using /\n?/ as a workaround. +.skip_job_when_commit_message_says_to: &skip_job_when_commit_message_says_to + if: '$CI_COMMIT_MESSAGE =~ /\n?CI: (SKIP MATRIX|MATRIX LEVEL 0)/' + when: never + +.force_run_job_when_commit_message_lvl_1_or_above: &force_run_job_when_commit_mssage_lvl_1_or_above + if: '$CI_COMMIT_MESSAGE =~ /\n?CI: MATRIX LEVEL [123]/' + when: on_success + +.force_run_job_when_commit_message_lvl_2_or_above: &force_run_job_when_commit_mssage_lvl_2_or_above + if: '$CI_COMMIT_MESSAGE =~ /\n?CI: MATRIX LEVEL [23]/' + when: on_success + +.force_run_job_when_commit_message_lvl_3_or_above: &force_run_job_when_commit_mssage_lvl_3_or_above + if: '$CI_COMMIT_MESSAGE =~ /\n?CI: MATRIX LEVEL [3]/' + when: on_success + +# check for $CI_PIPELINE_SOURCE needed because this is combined w/when:changes +.run_job_when_level_1_or_above_w_changes: &run_job_when_level_1_or_above_w_changes + if: '$SIMP_MATRIX_LEVEL =~ /^[123]$/ && $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "push"' + when: on_success + +.run_job_when_level_2_or_above_w_changes: &run_job_when_level_2_or_above_w_changes + if: '$SIMP_MATRIX_LEVEL =~ /^[23]$/ && $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "push"' + when: on_success + +.run_job_when_level_3_or_above_w_changes: &run_job_when_level_3_or_above_w_changes + if: '$SIMP_MATRIX_LEVEL =~ /^[3]$/ && $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "push"' + when: on_success + +.force_run_job_when_var_and_lvl_1_or_above: &force_run_job_when_var_and_lvl_1_or_above + if: '$SIMP_FORCE_RUN_MATRIX == "yes" && $SIMP_MATRIX_LEVEL =~ /^[123]$/' + when: on_success + +.force_run_job_when_var_and_lvl_2_or_above: &force_run_job_when_var_and_lvl_2_or_above + if: '$SIMP_FORCE_RUN_MATRIX == "yes" && $SIMP_MATRIX_LEVEL =~ /^[23]$/' + when: on_success + +.force_run_job_when_var_and_lvl_3_or_above: &force_run_job_when_var_and_lvl_3_or_above + if: '$SIMP_FORCE_RUN_MATRIX == "yes" && $SIMP_MATRIX_LEVEL =~ /^[3]$/' + when: on_success + + +# SIMP_MATRIX_LEVEL=1: Intended to run every commit +.with_SIMP_ACCEPTANCE_MATRIX_LEVEL_1: &with_SIMP_ACCEPTANCE_MATRIX_LEVEL_1 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_var_and_lvl_1_or_above + - <<: *force_run_job_when_commit_mssage_lvl_1_or_above + - <<: *run_job_when_level_1_or_above_w_changes + <<: *relevant_file_conditions_trigger_acceptance_tests + - when: never + +.with_SIMP_SPEC_MATRIX_LEVEL_1: &with_SIMP_SPEC_MATRIX_LEVEL_1 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_commit_mssage_lvl_1_or_above + - <<: *force_run_job_when_var_and_lvl_1_or_above + - <<: *run_job_when_level_1_or_above_w_changes + <<: *relevant_file_conditions_trigger_spec_tests + - when: never + +# SIMP_MATRIX_LEVEL=2: Resource-heavy or redundant jobs +.with_SIMP_ACCEPTANCE_MATRIX_LEVEL_2: &with_SIMP_ACCEPTANCE_MATRIX_LEVEL_2 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_var_and_lvl_2_or_above + - <<: *force_run_job_when_commit_mssage_lvl_2_or_above + - <<: *run_job_when_level_2_or_above_w_changes + <<: *relevant_file_conditions_trigger_acceptance_tests + - when: never + +.with_SIMP_SPEC_MATRIX_LEVEL_2: &with_SIMP_SPEC_MATRIX_LEVEL_2 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_commit_mssage_lvl_2_or_above + - <<: *force_run_job_when_var_and_lvl_2_or_above + - <<: *run_job_when_level_2_or_above_w_changes + <<: *relevant_file_conditions_trigger_spec_tests + - when: never + +# SIMP_MATRIX_LEVEL=3: Reserved for FULL matrix testing +.with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3: &with_SIMP_ACCEPTANCE_MATRIX_LEVEL_3 + rules: + - <<: *skip_job_when_commit_message_says_to + - <<: *force_run_job_when_var_and_lvl_3_or_above + - <<: *force_run_job_when_commit_mssage_lvl_3_or_above + - <<: *run_job_when_level_3_or_above_w_changes + <<: *relevant_file_conditions_trigger_acceptance_tests + - when: never -# To avoid running a prohibitive number of tests every commit, -# don't set this env var in your gitlab instance -.only_with_SIMP_FULL_MATRIX: &only_with_SIMP_FULL_MATRIX - only: - variables: - - $SIMP_FULL_MATRIX == "yes" # Puppet Versions -#----------------------------------------------------------------------- +# ----------------------------------------------------------------------- -.pup_5: &pup_5 - image: 'ruby:2.4' +.pup_7_x: &pup_7_x + image: 'ruby:2.7' variables: - PUPPET_VERSION: '~> 5.0' - BEAKER_PUPPET_COLLECTION: 'puppet5' - MATRIX_RUBY_VERSION: '2.4' + PUPPET_VERSION: '~> 7.0' + BEAKER_PUPPET_COLLECTION: 'puppet7' + MATRIX_RUBY_VERSION: '2.7' -.pup_5_5_10: &pup_5_5_10 - image: 'ruby:2.4' +.pup_7_pe: &pup_7_pe + image: 'ruby:2.7' variables: - PUPPET_VERSION: '5.5.10' - BEAKER_PUPPET_COLLECTION: 'puppet5' - MATRIX_RUBY_VERSION: '2.4' + PUPPET_VERSION: '7.21.0' + BEAKER_PUPPET_COLLECTION: 'puppet7' + MATRIX_RUBY_VERSION: '2.7' -.pup_6: &pup_6 - image: 'ruby:2.5' +.pup_8_x: &pup_8_x + image: 'ruby:3.2' variables: - PUPPET_VERSION: '~> 6.0' - BEAKER_PUPPET_COLLECTION: 'puppet6' - MATRIX_RUBY_VERSION: '2.5' + PUPPET_VERSION: '~> 8.0' + BEAKER_PUPPET_COLLECTION: 'puppet8' + MATRIX_RUBY_VERSION: '3.2' + # Testing Environments -#----------------------------------------------------------------------- +# ----------------------------------------------------------------------- .lint_tests: &lint_tests stage: 'validation' @@ -99,66 +278,88 @@ variables: stage: 'validation' tags: ['docker'] <<: *setup_bundler_env + <<: *with_SIMP_SPEC_MATRIX_LEVEL_1 script: - 'bundle exec rake spec' +.beaker: &beaker + image: ruby:2.7.2 # must be 2.7.2 if running in GCP + tags: + - beaker + before_script: + - !reference [.snippets, before_beaker_google] + - !reference [.snippets, before] + + .acceptance_base: &acceptance_base stage: 'acceptance' - tags: ['beaker'] <<: *setup_bundler_env + <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_1 + <<: *beaker .compliance_base: &compliance_base stage: 'compliance' - tags: ['beaker'] <<: *setup_bundler_env + <<: *with_SIMP_ACCEPTANCE_MATRIX_LEVEL_1 + <<: *beaker # Pipeline / testing matrix -#======================================================================= +# ======================================================================= -sanity_checks: - <<: *pup_5 +releng_checks: + <<: *pup_7_x <<: *setup_bundler_env - stage: 'sanity' + stage: 'validation' tags: ['docker'] script: - - 'if `hash apt-get`; then apt-get update; fi' - - 'if `hash apt-get`; then apt-get install -y rpm; fi' + - 'command -v rpm || if command -v apt-get; then apt-get update; apt-get install -y rpm; fi ||:' - 'bundle exec rake check:dot_underscore' - 'bundle exec rake check:test_file' - 'bundle exec rake pkg:check_version' - - 'bundle exec rake pkg:compare_latest_tag' + - 'bundle exec rake pkg:compare_latest_tag[,true]' - 'bundle exec rake pkg:create_tag_changelog' - - 'bundle exec puppet module build' + - 'bundle exec pdk build --force --target-dir=dist' + # Linting -#----------------------------------------------------------------------- +# ----------------------------------------------------------------------- -pup5-lint: - <<: *pup_5 - <<: *lint_tests +# NOTE: Don't add more lint checks here. +# puppet-lint is a validator, not a parser; it includes its own lexer and +# doesn't use the Puppet gem at all. Running multiple lint tests against +# different Puppet versions won't accomplish anything. -pup6-lint: - <<: *pup_6 +pup-lint: + <<: *pup_7_x <<: *lint_tests # Unit Tests -#----------------------------------------------------------------------- +# ----------------------------------------------------------------------- -pup5-unit: - <<: *pup_5 +pup7.x-unit: + <<: *pup_7_x <<: *unit_tests + <<: *with_SIMP_SPEC_MATRIX_LEVEL_2 -pup5.5.10-unit: - <<: *pup_5_5_10 +pup7.pe-unit: + <<: *pup_7_pe <<: *unit_tests -pup6-unit: - <<: *pup_6 +pup8.x-unit: + <<: *pup_8_x <<: *unit_tests -# Acceptance tests +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# Everything above the "Repo-specific content" comment will be overwritten by +# the next puppetsync. +# ------------------------------------------------------------------------------ + +# Repo-specific content # ============================================================================== + # This module does not have acceptance tests yet. # # See: https://simp-project.atlassian.net/browse/SIMP-5632) diff --git a/.pdkignore b/.pdkignore new file mode 100644 index 0000000..84caa42 --- /dev/null +++ b/.pdkignore @@ -0,0 +1,57 @@ +# .pdkignore masks files from inclusion by `pdk build`. +# +# It is used by CI when building modules to publish to the Puppet Forge and to +# mask symlinks from the `pdk build` test in the module's RELENG checks. +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is automatically updated as part of a puppet module baseline. +# The next baseline sync will overwrite any local changes made to this file. +# ------------------------------------------------------------------------------ +.*.sw? +.git/ +.metadata +.yardoc +.yardwarns +*.iml +/.bundle/ +/.idea/ +/.vagrant/ +/coverage/ +/bin/ +/doc/ +/Gemfile.local +/Gemfile.lock +/junit/ +/log/ +/pkg/ +/dist/ +/tmp/ +/vendor/ +/.vendor/ +/convert_report.txt +/update_report.txt +.DS_Store +.project +.envrc +/inventory.yaml +/appveyor.yml +/.fixtures.yml +/Gemfile +/.gitattributes +/.gitignore +/.github/ +/.gitlab-ci.yml +/.pdkignore +/.puppet-lint.rc +/.sync.yml +/.pmtignore +/Rakefile +/rakelib/ +/.rspec +/.rubocop.yml +/.travis.yml +/.yardopts +/spec/ +/.vscode/ +/tests/ diff --git a/.puppet-lint.rc b/.puppet-lint.rc index 71ffc7b..eb56769 100644 --- a/.puppet-lint.rc +++ b/.puppet-lint.rc @@ -1,4 +1,15 @@ +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is automatically updated as part of a puppet module baseline. +# The next baseline sync will overwrite any local changes made to this file. +# ------------------------------------------------------------------------------ --log-format="%{path}:%{line}:%{check}:%{KIND}:%{message}" --relative +--no-class_inherits_from_params_class-check --no-140chars-check --no-trailing_comma-check +--no-params-empty-string-assignment-check +# This is here because the code can't handle lookups in parameters and SIMP +# modules have a LOT of those +--no-parameter_order-check diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index 79a6144..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.4.4 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 43ebe5a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,110 +0,0 @@ -# The testing matrix considers ruby/puppet versions supported by SIMP and PE: -# -# https://puppet.com/docs/pe/2018.1/component_versions_in_recent_pe_releases.html -# https://puppet.com/misc/puppet-enterprise-lifecycle -# https://puppet.com/docs/pe/2018.1/overview/getting_support_for_pe.html -# ------------------------------------------------------------------------------ -# Release Puppet Ruby EOL -# SIMP 6.2 4.10 2.1.9 TBD -# PE 2016.4 4.10 2.1.9 2018-12-31 (LTS) -# PE 2017.3 5.3 2.4.5 2018-12-31 -# SIMP 6.3 5.5 2.4.5 TBD*** -# PE 2018.1 5.5 2.4.5 2020-05 (LTS)*** -# PE 2019.0 6.0 2.5.1 2019-08-31^^^ -# -# *** = Modules created for SIMP 6.3+ are not required to support Puppet < 5.5 -# ^^^ = SIMP doesn't support 6 yet; tests are info-only and allowed to fail - ---- -language: ruby -cache: bundler -sudo: false - -stages: - - check - - spec - - name: deploy - if: 'fork = false AND tag = true' - -bundler_args: --without development system_tests --path .vendor - -notifications: - email: false - -addons: - apt: - packages: - - rpm - -before_install: - - rm -f Gemfile.lock - -global: - - STRICT_VARIABLES=yes - -jobs: - include: - - stage: check - name: 'Syntax, style, and validation checks' - rvm: 2.4.5 - env: PUPPET_VERSION="~> 5" - script: - - bundle exec rake check:dot_underscore - - bundle exec rake check:test_file - - bundle exec rake pkg:check_version - - bundle exec rake metadata_lint - - bundle exec rake pkg:compare_latest_tag - - bundle exec rake pkg:create_tag_changelog - - bundle exec rake lint - - bundle exec puppet module build - - - stage: spec - name: 'Puppet 5.3 (PE 2017.3)' - rvm: 2.4.5 - env: PUPPET_VERSION="~> 5.3.0" - script: - - bundle exec rake spec - - - stage: spec - rvm: 2.4.5 - name: 'Puppet 5.5 (SIMP 6.3, PE 2018.1)' - env: PUPPET_VERSION="~> 5.5.0" - script: - - bundle exec rake spec - - - stage: spec - name: 'Latest Puppet 5.x' - rvm: 2.4.5 - env: PUPPET_VERSION="~> 5.0" - script: - - bundle exec rake spec - - - stage: spec - name: 'Latest Puppet 6.x' - rvm: 2.5.1 - env: PUPPET_VERSION="~> 6.0" - script: - - bundle exec rake spec - - - stage: deploy - rvm: 2.4.5 - script: - - true - before_deploy: - - "export PUPMOD_METADATA_VERSION=`ruby -r json -e \"puts JSON.parse(File.read('metadata.json')).fetch('version')\"`" - - '[[ $TRAVIS_TAG =~ ^simp-${PUPMOD_METADATA_VERSION}$|^${PUPMOD_METADATA_VERSION}$ ]]' - deploy: - - provider: releases - api_key: - secure: "Fyd9HTLcrXXR8GV+dZRdNkUFM4iCX0doNTsMEIoM7Gy1Lw2ucFdSD0cyWT++iLDEFwtvFUEulvS7pzLDJq8q7NrBX8G5GueLOe55Iq4TY491HePWvji7Ug5xaMV4tnS6Y538oYciVCs9/nDQ6ONgGvNcU4lMekQGQAU4Q5bylGA3ZIuPXRG6KldSZQQ5DlLIaHbfDOpUUtZSvRLYp2VG4WLpEV3A5vmBp+A6H3xpSn87TdavDooDhcr5fiPRZiFWSorCwR6jF5Mz9XL6UxN2EfLUwh3SjcW3wvDtxXacA09HUPIGGqGaAcnZ4Kte5aWZXlu9k3qz35npuzWuFt1v1slLqnKSxKzZ1/BiwmqH1sneky0x84wxY1ZZjrbXabJh60bsVB5i5wfC86icoYPHd8kp3simvtSvCWw5R4L4G50H5Ph5K2c9XxWZPXXPPAu6ZUNxWBjTRDZdm+V+vArD7dYl6Cye9KtxLU9WVWzqQojQ+aK6i9RKrKjs2t2sTLPtsGz2PQoYmc/I9nAZ3LMxeWc6mbH1CKQC3nuQg+McbepYjkrEqUssqisiTJq7DtstsgLAx7qOoPSUQdnDpMYM/gzhDdjukZIxH6VpG87tlYGM3p0OYUqeyoRcnBppRf+lmHGH5Rith/IjSt2TmPHb7veKsd5++Vx5Vf1E7OlzGdM=" - skip_cleanup: true - on: - tags: true - condition: '($SKIP_FORGE_PUBLISH != true)' - - provider: puppetforge - user: simp - password: - secure: "Pwaif6lS+s3qapv9j69MRdmtdrSiZkBBkMfpPH/5BsAsNHwUis+i9h1+nWLqrk5I6q3NwVheO+IPdpvUXnWDqNz2JTd7Gt4frc9vsVbud1VmsNJzsvLbYqNAPxIYL6Rojl2soSK93Q/r83HzGTRBM/hFZeDE+r1+8QxGFkYikMYHbXF8+g9q2YVimCf80AmjC/679SuPHu8p3PhuFp5OKxMGa93T+2OdT2pL3xQ8CgjU0cO/2d2LfdLatdpxVSgBD+/TVsEPAzlGGCK446RxzmRmXTgdO0nzGYlbC1XcbJxz9PyjRlefZW0U+wpQqErZkemU3VdN2ChKkgFa8YxZwh1Xir8XDG16wJYDVPhKGu4ij+1v0319s37R4hJjWvOPvs1hAcGU+lYDGjYPknLfbiQodl29Qb5W2G614G0/0Ee4gEzDo2m/z4VA+1MpA4Qp4/jC/6qZRrFup53uVWrpK/6bagf1DOl6VLX98Q46d74KqtqgIxzuufF6q17CpJczMxXTkFl8G5YRUAgx45LpeSXWKj+TUfak5hVljIa8Kgxluf5MSQW69Qgc8zFvy+BrFDGqvQBzgXFwlPWuzOWUY8mMLR+BshJK6mVlYujrXYL8WqO9mniVvAL8PIjc6qjWS8c7XNyjsJz0x61F6laFY63aPPTL0Bb6nSYDTGtvTd4=" - on: - tags: true - condition: '($SKIP_FORGE_PUBLISH != true)' diff --git a/Gemfile b/Gemfile index f328fea..e74c3da 100644 --- a/Gemfile +++ b/Gemfile @@ -1,30 +1,55 @@ -gem_sources = ENV.fetch('GEM_SERVERS','https://rubygems.org').split(/[, ]+/) +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is automatically updated as part of a puppet module baseline. +# The next baseline sync will overwrite any local changes made to this file. +# ------------------------------------------------------------------------------ +gem_sources = ENV.fetch('GEM_SERVERS', 'https://rubygems.org').split(%r{[, ]+}) + +ENV['PDK_DISABLE_ANALYTICS'] ||= 'true' gem_sources.each { |gem_source| source gem_source } group :test do - gem 'rake' - gem 'puppet', ENV.fetch('PUPPET_VERSION', '~> 5.5') - gem 'rspec' - gem 'rspec-puppet' + puppet_version = ENV.fetch('PUPPET_VERSION', ['>= 7', '< 9']) + major_puppet_version = Array(puppet_version).first.scan(%r{(\d+)(?:\.|\Z)}).flatten.first.to_i gem 'hiera-puppet-helper' - gem 'puppetlabs_spec_helper' gem 'metadata-json-lint' + gem 'pathspec', '~> 0.2' if Gem::Requirement.create('< 2.6').satisfied_by?(Gem::Version.new(RUBY_VERSION.dup)) + gem('pdk', ENV.fetch('PDK_VERSION', ['>= 2.0', '< 4.0']), require: false) if major_puppet_version > 5 + gem 'puppet', puppet_version + gem 'puppetlabs_spec_helper' + gem 'puppet-lint-trailing_comma-check', require: false gem 'puppet-strings' - gem 'puppet-lint-empty_string-check', :require => false - gem 'puppet-lint-trailing_comma-check', :require => false - gem 'simp-rspec-puppet-facts', ENV.fetch('SIMP_RSPEC_PUPPET_FACTS_VERSION', '~> 2.2') - gem 'simp-rake-helpers', ENV.fetch('SIMP_RAKE_HELPERS_VERSION', '~> 5.6') - gem 'facterdb' + gem 'rake' + gem 'rspec' + gem 'rspec-puppet' + gem 'simp-rake-helpers', ENV.fetch('SIMP_RAKE_HELPERS_VERSION', ['>= 5.21.0', '< 6']) + gem 'simp-rspec-puppet-facts', ENV.fetch('SIMP_RSPEC_PUPPET_FACTS_VERSION', '~> 3.7') end group :development do gem 'pry' + gem 'pry-byebug' gem 'pry-doc' end group :system_tests do + gem 'bcrypt_pbkdf' gem 'beaker' gem 'beaker-rspec' - gem 'simp-beaker-helpers', ENV.fetch('SIMP_BEAKER_HELPERS_VERSION', '~> 1.12') + gem 'simp-beaker-helpers', ENV.fetch('SIMP_BEAKER_HELPERS_VERSION', ['>= 1.32.1', '< 2']) +end + +# Evaluate extra gemfiles if they exist +extra_gemfiles = [ + ENV.fetch('EXTRA_GEMFILE', ''), + "#{__FILE__}.project", + "#{__FILE__}.local", + File.join(Dir.home, '.gemfile'), +] +extra_gemfiles.each do |gemfile| + if File.file?(gemfile) && File.readable?(gemfile) + eval(File.read(gemfile), binding) # rubocop:disable Security/Eval + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 502c685..e076f23 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,13 @@ +# frozen_string_literal: true + +# +# ------------------------------------------------------------------------------ +# NOTICE: **This file is maintained with puppetsync** +# +# This file is automatically updated as part of a puppet module baseline. +# The next baseline sync will overwrite any local changes made to this file. +# ------------------------------------------------------------------------------ + require 'puppetlabs_spec_helper/module_spec_helper' require 'rspec-puppet' require 'simp/rspec-puppet-facts' @@ -7,47 +17,95 @@ # RSpec Material fixture_path = File.expand_path(File.join(__FILE__, '..', 'fixtures')) -module_name = File.basename(File.expand_path(File.join(__FILE__,'../..'))) +module_name = File.basename(File.expand_path(File.join(__FILE__, '../..'))) + +if ENV['PUPPET_DEBUG'] + Puppet::Util::Log.level = :debug + Puppet::Util::Log.newdestination(:console) +end -default_hiera_config =<<-EOM +default_hiera_config = <<~HIERA_CONFIG --- -:backends: - - "rspec" - - "yaml" -:yaml: - :datadir: "stub" -:hierarchy: - - "%{custom_hiera}" - - "%{spec_title}" - - "%{module_name}" - - "default" -EOM - - -['hieradata','modules'].each do |dir| - _dir = File.join(fixture_path,dir) - FileUtils.mkdir_p(_dir) unless File.directory?(_dir) +version: 5 +hierarchy: + - name: Custom Test Hiera + path: "%{custom_hiera}.yaml" + - name: "%{module_name}" + path: "%{module_name}.yaml" + - name: Common + path: default.yaml +defaults: + data_hash: yaml_data + datadir: "stub" +HIERA_CONFIG + +# This can be used from inside your spec tests to set the testable environment. +# You can use this to stub out an ENC. +# +# Example: +# +# context 'in the :foo environment' do +# let(:environment){:foo} +# ... +# end +# +def set_environment(environment = :production) + RSpec.configure { |c| c.default_facts['environment'] = environment.to_s } +end + +# This can be used from inside your spec tests to load custom hieradata within +# any context. +# +# Example: +# +# describe 'some::class' do +# context 'with version 10' do +# let(:hieradata){ "#{class_name}_v10" } +# ... +# end +# end +# +# Then, create a YAML file at spec/fixtures/hieradata/some__class_v10.yaml. +# +# Hiera will use this file as it's base of information stacked on top of +# 'default.yaml' and .yaml per the defaults above. +# +# Note: Any colons (:) are replaced with underscores (_) in the class name. +def set_hieradata(hieradata) + RSpec.configure { |c| c.default_facts['custom_hiera'] = hieradata } +end + +unless File.directory?(File.join(fixture_path, 'hieradata')) + FileUtils.mkdir_p(File.join(fixture_path, 'hieradata')) +end + +unless File.directory?(File.join(fixture_path, 'modules', module_name)) + FileUtils.mkdir_p(File.join(fixture_path, 'modules', module_name)) end RSpec.configure do |c| # If nothing else... c.default_facts = { - :production => { - #:fqdn => 'production.rspec.test.localdomain', - :path => '/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin', - :concat_basedir => '/tmp' + production: { + # :fqdn => 'production.rspec.test.localdomain', + path: '/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin', + concat_basedir: '/tmp' } } c.mock_framework = :rspec - c.mock_with :mocha + c.mock_with :rspec c.module_path = File.join(fixture_path, 'modules') - c.manifest_dir = File.join(fixture_path, 'manifests') - c.hiera_config = File.join(fixture_path,'hieradata','hiera.yaml') + c.manifest_dir = File.join(fixture_path, 'manifests') if c.respond_to?(:manifest_dir) + + c.hiera_config = File.join(fixture_path, 'hieradata', 'hiera.yaml') # Useless backtrace noise - backtrace_exclusion_patterns = [ /spec_helper/, /gems/ ] + backtrace_exclusion_patterns = [ + %r{spec_helper}, + %r{gems}, + ] if c.respond_to?(:backtrace_exclusion_patterns) c.backtrace_exclusion_patterns = backtrace_exclusion_patterns @@ -55,30 +113,50 @@ c.backtrace_clean_patterns = backtrace_exclusion_patterns end + # rubocop:disable RSpec/BeforeAfterAll c.before(:all) do - data = YAML.load(default_hiera_config) - data[:yaml][:datadir] = File.join(fixture_path, 'hieradata') + data = YAML.safe_load(default_hiera_config) + data.each_key do |key| + next unless data[key].is_a?(Hash) + + if data[key][:datadir] == 'stub' + data[key][:datadir] = File.join(fixture_path, 'hieradata') + elsif data[key]['datadir'] == 'stub' + data[key]['datadir'] = File.join(fixture_path, 'hieradata') + end + end File.open(c.hiera_config, 'w') do |f| f.write data.to_yaml end end + # rubocop:enable RSpec/BeforeAfterAll c.before(:each) do @spec_global_env_temp = Dir.mktmpdir('simpspec') if defined?(environment) - FileUtils.mkdir_p(File.join(@spec_global_env_temp,environment.to_s)) + set_environment(environment) + FileUtils.mkdir_p(File.join(@spec_global_env_temp, environment.to_s)) end # ensure the user running these tests has an accessible environmentpath + Puppet[:digest_algorithm] = 'sha256' Puppet[:environmentpath] = @spec_global_env_temp Puppet[:user] = Etc.getpwuid(Process.uid).name Puppet[:group] = Etc.getgrgid(Process.gid).name + + # sanitize hieradata + if defined?(hieradata) + set_hieradata(hieradata.tr(':', '_')) + elsif defined?(class_name) + set_hieradata(class_name.tr(':', '_')) + end end c.after(:each) do - FileUtils.rm_rf(@spec_global_env_temp) # clean up the mocked environmentpath + # clean up the mocked environmentpath + FileUtils.rm_rf(@spec_global_env_temp) @spec_global_env_temp = nil end end @@ -86,7 +164,7 @@ Dir.glob("#{RSpec.configuration.module_path}/*").each do |dir| begin Pathname.new(dir).realpath - rescue - fail "ERROR: The module '#{dir}' is not installed. Tests cannot continue." + rescue StandardError + raise "ERROR: The module '#{dir}' is not installed. Tests cannot continue." end end