From a9e5c9efc4b6afe1fb4ecf8bbab800ae4661ac75 Mon Sep 17 00:00:00 2001 From: op-ct Date: Wed, 14 Jul 2021 08:33:16 -0400 Subject: [PATCH] (SIMP-10264) GHA: Add `rpm_release` workflow (#62) --- .github/workflows/release_rpms.yml | 272 +++++++++++++++++++++++++++++ .github/workflows/tag_deploy.yml | 45 +++-- 2 files changed, 301 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/release_rpms.yml diff --git a/.github/workflows/release_rpms.yml b/.github/workflows/release_rpms.yml new file mode 100644 index 0000000..a268a82 --- /dev/null +++ b/.github/workflows/release_rpms.yml @@ -0,0 +1,272 @@ +# 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` +# 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 + dry_run: + description: "Dry run (Test-build RPMs)" + required: false + default: 'no' + +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 + runs-on: ubuntu-20.04 + steps: + - name: "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" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-(rc|alpha|beta|pre)?([0-9]+)?)?$ ]]; then + printf '::error ::Release Tag format is not SemVer or SemVer-ish RPM: %s\n' "$RELEASE_TAG" + exit 88 + fi + + - name: > + Query info for ${{ env.TARGET_REPO }} + release ${{ github.event.inputs.release_tag }} + (autocreate_release = '${{ github.event.inputs.autocreate_release }}') + id: release-api + env: + AUTOCREATE_RELEASE: ${{ github.event.inputs.autocreate_release }} + uses: actions/github-script@v4 + 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') + var release_id + const owner_data = { owner: owner, repo: repo } + const release_data = Object.assign( {tag: tag}, owner_data ) + const create_release_data = Object.assign( {tag_name: tag}, 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`) + + // Must already have a tag + github.request( 'GET /repos/{owner}/{repo}/git/matching-refs/{ref}', tag_data ).then ( + result => { + if (result.data.length == 0) { throw `Can't find tag ${tag} in repo ${owner}/${repo}` } + } + ).then( + result => { + github.request( 'POST /repos/{owner}/{repo}/releases', create_release_data).then( + result=>{ + release_id = id_from_release(result.data) + }, + post_err =>{ + core.error('Error auto-creating release') + throw post_err + } + ) + } + ).finally(()=>{ + return(release_id) + }) + } + + github.request('GET /repos/{owner}/{repo}/releases/tags/{tag}', release_data ).then( + result => { release_id = id_from_release(result.data) }, + err => { release_id = autocreate_release_if_appropriate(err) } + ).catch( e => { throw e } ).then( + result => { + if (!release_id){ + throw `Could not get release for ${tag} for repo ${owner}:${repo}` + } + core.setOutput('id', release_id) + } + ) + + - name: Checkout code + uses: actions/checkout@v2 + with: + repository: ${{ env.TARGET_REPO }} + ref: ${{ env.RELEASE_TAG }} + clean: true + fetch-depth: 0 + + - name: 'Build & Sign RPMs for ${{ github.event.inputs.release_tag }} Release' + 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' + + - 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@v4 + 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.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.repos.deleteReleaseAsset({ owner, repo, asset_id }) + }) + + - name: 'Upload RPM file(s) to GitHub Release (github-script)' + if: ${{ github.event.inputs.dry_run != 'yes' }} + uses: actions/github-script@v4 + 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.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.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.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 index 67d9227..e115e87 100644 --- a/.github/workflows/tag_deploy.yml +++ b/.github/workflows/tag_deploy.yml @@ -74,15 +74,6 @@ jobs: clean: true fetch-depth: 0 - - name: Build Release RPM - uses: simp/github-action-build-and-sign-pkg-single-rpm@v1 - 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 }} - - name: Get tag & annotation info (${{github.ref}}) id: tag-check run: | @@ -115,15 +106,37 @@ jobs: draft: false prerelease: false - - name: Upload RPM file to Release - uses: actions/upload-release-asset@v1 + build-and-attach-rpms: + name: Trigger RPM release + needs: [ create-github-release ] + if: github.repository_owner == 'simp' + runs-on: ubuntu-18.04 + env: + TARGET_REPO: ${{ github.repository }} + steps: + - name: Get tag & annotation info (${{github.ref}}) + id: tag-check + run: echo "::set-output name=tag::${GITHUB_REF/refs\/tags\//}" + - name: Trigger RPM release workflow + uses: actions/github-script@v4 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + TARGET_TAG: ${{ steps.tag-check.outputs.tag }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ${{ steps.build-and-sign-rpm.outputs.rpm_file_path }} - asset_name: ${{ steps.build-and-sign-rpm.outputs.rpm_file_basename }} - asset_content_type: application/octet-stream + github-token: ${{ secrets.SIMP_AUTO_GITHUB_TOKEN__REPO_SCOPE }} + script: | + 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 + } + }).then( (result) => { + console.log( `== Submitted workflow dispatch: status ${result.status}` ) + }) deploy-to-puppet-forge: name: Deploy PuppetForge Release