From cc27b8d9520ffc44422f09df4155087916a20e88 Mon Sep 17 00:00:00 2001 From: op-ct Date: Wed, 12 Jan 2022 12:44:59 -0500 Subject: [PATCH] (SIMP-10392) GHA: Trigger `rpm_release` workflow (#436) This patch ensures that a GitHub release will trigger the `release_rpm` GHA workflow. The patch enforces a standardized asset baseline using simp/puppetsync, and may apply other updates to ensure conformity. [SIMP-10393] #close [SIMP-10392] #comment Add `release_rpms` to simp-doc --- .github/workflows/pr_glci.yml | 111 ++----- .github/workflows/pr_glci_cleanup.yml | 8 + .github/workflows/pr_glci_manual.yml | 142 +++----- .github/workflows/release_rpms.yml | 326 +++++++++++++++++++ .github/workflows/tag_deploy_github-rpms.yml | 129 ++++++++ .github/workflows/validate_tokens_asset.yml | 72 ++++ 6 files changed, 610 insertions(+), 178 deletions(-) create mode 100644 .github/workflows/release_rpms.yml create mode 100644 .github/workflows/tag_deploy_github-rpms.yml create mode 100644 .github/workflows/validate_tokens_asset.yml diff --git a/.github/workflows/pr_glci.yml b/.github/workflows/pr_glci.yml index 0a39b2c1..b37eec85 100644 --- a/.github/workflows/pr_glci.yml +++ b/.github/workflows/pr_glci.yml @@ -3,6 +3,14 @@ # 1. The .gitlab-ci.yaml file exists and validates # 2. The PR submitter has write access to the target repository. # +# ------------------------------------------------------------------------------ +# +# 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! +# # ============================================================================== # # GitHub Action Secrets variables available for this pipeline: @@ -57,47 +65,18 @@ jobs: name: '.gitlab-ci.yml Syntax' runs-on: ubuntu-latest outputs: - exists: ${{ steps.glci-file-exists.outputs.exists }} valid: ${{ steps.validate-glci-file.outputs.valid }} steps: - uses: actions/checkout@v2 with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} - - name: 'Does GLCI file exist?' - id: glci-file-exists - run: | - if [ -f .gitlab-ci.yml ]; then - echo '.gitlab-ci.yml exists' - echo '::set-output name=exists::true' - else - echo '::error ::The ".gitlab-ci.yml" file is missing!' - echo '::set-output name=exists::false' - false - fi - name: 'Validate GLCI file syntax' id: validate-glci-file - if: steps.glci-file-exists.outputs.exists == 'true' - env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} # https://gitlab.com/api/v4 - GITLAB_API_PRIVATE_TOKEN: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} - run: | - GITLAB_API_URL="${GITLAB_API_URL:-https://gitlab.com/api/v4}" - CURL_CMD=(curl --http1.1 --fail --silent --show-error --header 'Content-Type: application/json' --data @-) - [ -n "$GITLAB_API_PRIVATE_TOKEN" ] && CURL_CMD+=(--header "Authorization: Bearer $GITLAB_API_PRIVATE_TOKEN") - data="$(jq --null-input --arg yaml "$(<.gitlab-ci.yml)" '.content=$yaml' )" - response="$(echo "$data" | "${CURL_CMD[@]}" "${GITLAB_API_URL}/ci/lint?include_merged_yaml=true" | jq . )" - status=$( echo "$response" | jq .status ) - if [[ "$status" == '"valid"' ]]; then - echo '.gitlab-ci.yml is valid' - echo '::set-output name=valid::true' - else - echo '::set-output name=valid::false' - echo '::error::The .gitlab-ci.yml" file is invalid!' - echo "$response" | jq -r '.errors[] | . = "::error ::\(.)"' - printf "::debug ::.gitlab-ci.yml CI lint service response: %s\n" "$response" - false - fi + uses: simp/github-action-gitlab-ci-syntax-check@main + with: + gitlab_api_private_token: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + gitlab_api_url: ${{ secrets.GITLAB_API_URL }} # https://gitlab.com/api/v4 contributor-permissions: name: 'PR contributor check' @@ -145,14 +124,16 @@ jobs: # AND: # - [x] Newly-opened PRs: github.event.action == 'opened' # - [x] Re-opened PRs: github.event.action == 'reopened' - # - [x] Commites are added to PR: github.event.action == 'synchronize' + # - [x] Commits are added to PR: github.event.action == 'synchronize' # AND: # - [x] .gitlab-ci.yml exists/ok: needs.glci-syntax.outputs.valid == 'true' # - # It will NOT Trigger on: + # [Not implemented] It should NEVER trigger on: # # - [ ] Merged PRs: github.event.pull_request.merged == 'false' - # - (the downstream GitLab mirror will take care of that) + # - (the downstream GitLab mirror will take care of that) + # - Not implemented: For some reason, this conditional always fails + # - Unnecessary if on>pull_request_target>types doesn't include 'closed' if: github.event_name == 'pull_request_target' && ( github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize' ) && github.event.pull_request.merged != 'true' && needs.glci-syntax.outputs.valid == 'true' && needs.contributor-permissions.outputs.permitted == 'true' runs-on: ubuntu-18.04 steps: @@ -162,64 +143,26 @@ jobs: # - [ ] if there's no PR check on the main GitHub branch, make one (?) # - [x] Cancel any GLCI pipelines already pending/running for this branch # - "created|waiting_for_resource|preparing|pending|running" + # - Exception: don't cancel existing pipeline for our own commit # - [x] if PR: force-push branch to GitLab - uses: actions/checkout@v2 if: needs.contributor-permissions.outputs.permitted == 'true' with: + clean: true fetch-depth: 0 # Need full checkout to push to gitlab mirror repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.ref }} - name: Trigger CI when user has Repo Permissions if: needs.contributor-permissions.outputs.permitted == 'true' - env: - GITLAB_SERVER_URL: ${{ secrets.GITLAB_SERVER_URL }} # https://gitlab.com - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} # https://gitlab.com/api/v4 - GITLAB_ORG: 'simp' - GITLAB_API_PRIVATE_TOKEN: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} - GIT_BRANCH: ${{ github.event.pull_request.head.ref }} - run: | - GITLAB_SERVER_URL="${GITLAB_SERVER_URL:-https://gitlab.com}" - GITLAB_API_URL="${GITLAB_API_URL:-${GITLAB_SERVER_URL}/api/v4}" - GIT_BRANCH="${GIT_BRANCH:-GITHUB_HEAD_REF}" - GITXXB_REPO_NAME="${GITHUB_REPOSITORY/$GITHUB_REPOSITORY_OWNER\//}" - GITLAB_PROJECT_ID="${GITLAB_ORG}%2F${GITXXB_REPO_NAME}" - # --http1.0 avoids an HTTP/2 load balancing issue when run from GA - CURL_CMD=(curl --http1.0 --fail --silent --show-error \ - --header "Authorization: Bearer $GITLAB_API_PRIVATE_TOKEN" \ - --header "Content-Type: application/json" \ - --header "Accept: application/json" \ - ) - - # Cancel any active/pending GitLab CI pipelines for the same project+branch - active_pipeline_ids=() - for pipe_status in created waiting_for_resource preparing pending running; do - echo " ---- checking for CI pipelines with status '$pipe_status' for project '$GITLAB_PROJECT_ID', branch '$GIT_BRANCH'" - url="${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/pipelines?ref=${GIT_BRANCH}&status=${pipe_status}" - active_pipelines="$("${CURL_CMD[@]}" "$url" | jq -r '.[] | .id , .web_url')" - active_pipeline_ids+=($(echo "$active_pipelines" | grep -E '^[0-9]*$')) - printf "$active_pipelines\n\n" - done - if [ "${#active_pipeline_ids[@]}" -gt 0 ]; then - printf "\nFound %s active pipeline ids:\n" "${#active_pipeline_ids[@]}" - echo "${active_pipeline_ids[@]}" - for pipe_id in "${active_pipeline_ids[@]}"; do - printf "\n ------ Cancelling pipeline ID %s...\n" "$pipe_id" - "${CURL_CMD[@]}" --request POST "${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/pipelines/${pipe_id}/cancel" - done - else - echo No active pipelines found - fi - - echo "== Pushing $GIT_BRANCH to gitlab" - git remote add gitlab "https://oauth2:${GITLAB_API_PRIVATE_TOKEN}@${GITLAB_SERVER_URL#*://}/${GITLAB_ORG}/${GITXXB_REPO_NAME}.git" - #git branch "$GIT_BRANCH" HEAD - git log --color --graph --abbrev-commit -5 \ - --pretty=format:'%C(red)%h%C(reset) -%C(yellow)%d%Creset %s %Cgreen(%ci) %C(bold blue)<%an>%Creset' - git push gitlab ":${GIT_BRANCH}" -f || : # attempt to un-weird GLCI's `changed` tracking - git push gitlab "${GIT_BRANCH}" -f - echo "Pushed branch '${GIT_BRANCH}' to gitlab" - echo " A new pipeline should be at: https://${GITLAB_SERVER_URL#*://}/${GITLAB_ORG}/${GITXXB_REPO_NAME}/-/pipelines/" + uses: simp/github-action-gitlab-ci-pipeline-trigger@v1 + with: + git_branch: ${{ github.event.pull_request.head.ref }} # TODO check for/avoid protected branches? + git_hashref: ${{ github.event.pull_request.head.sha }} + gitlab_api_private_token: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + gitlab_group: ${{ github.event.organization.login }} + github_repository: ${{ github.repository }} + github_repository_owner: ${{ github.repository_owner }} - name: When user does NOT have Repo Permissions if: needs.contributor-permissions.outputs.permitted == 'false' diff --git a/.github/workflows/pr_glci_cleanup.yml b/.github/workflows/pr_glci_cleanup.yml index 3e75e560..7fb03811 100644 --- a/.github/workflows/pr_glci_cleanup.yml +++ b/.github/workflows/pr_glci_cleanup.yml @@ -3,6 +3,14 @@ # * Cancels all GLCI pipelines associated with the PR HEAD ref (branch) # * Removes the PR HEAD branch from the corresponding gitlab.com/org/ project # +# ------------------------------------------------------------------------------ +# +# 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! +# # ============================================================================== # # GitHub Action Secrets variables available for this pipeline: diff --git a/.github/workflows/pr_glci_manual.yml b/.github/workflows/pr_glci_manual.yml index ed45baa9..57835cc2 100644 --- a/.github/workflows/pr_glci_manual.yml +++ b/.github/workflows/pr_glci_manual.yml @@ -1,10 +1,36 @@ - +# Manually trigger GLCI pipelines for a PR +# ------------------------------------------------------------------------------ +# +# 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 Type Notes +# ------------------------ -------- ---------------------------------------- +# GITLAB_API_PRIVATE_TOKEN Required GitLab token (should have `api` scope) +# NO_SCOPE_GITHUB_TOKEN Required GitHub token (should have no scopes) +# GITLAB_SERVER_URL Optional Specify a GL server other than gitlab.com +# 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) +# +# ------------------------------------------------------------------------------ +# # NOTES: -# $secrets.GITHUB_AUTO is not set for workflow_dispatch events +# It is necessary to provide NO_SCOPE_GITHUB_TOKEN because $secrets.GITHUB_AUTO +# is NOT provide to manually-triggered (`workflow_dispatch`) events, in order +# to prevent recursive triggers between workflows +# +# Reference: # # https://docs.github.com/en/actions/reference/events-that-trigger-workflows#triggering-new-workflows-using-a-personal-access-token --- -name: 'Manual: GLCI for PR' +name: 'Manual: PR GLCI' on: workflow_dispatch: @@ -16,9 +42,8 @@ on: jobs: glci-syntax: name: '.gitlab-ci.yml Syntax' - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 outputs: - exists: ${{ steps.glci-file-exists.outputs.exists }} valid: ${{ steps.validate-glci-file.outputs.valid }} pr_head_ref: ${{ steps.get-pr.outputs.pr_head_ref }} pr_head_sha: ${{ steps.get-pr.outputs.pr_head_sha }} @@ -28,7 +53,7 @@ jobs: - uses: actions/github-script@v3 id: get-pr with: - github-token: ${{secrets.SIMP_AUTO_GITHUB_TOKEN__NO_SCOPE}} + github-token: ${{secrets.NO_SCOPE_GITHUB_TOKEN}} # See: # - https://octokit.github.io/rest.js/ script: | @@ -69,112 +94,42 @@ jobs: with: repository: ${{ steps.get-pr.outputs.pr_head_full_name }} ref: ${{ steps.get-pr.outputs.pr_head_sha }} - token: ${{secrets.SIMP_AUTO_GITHUB_TOKEN__NO_SCOPE}} + token: ${{secrets.NO_SCOPE_GITHUB_TOKEN}} clean: true - - name: 'Does GLCI file exist?' - id: glci-file-exists - run: | - if [ -f .gitlab-ci.yml ]; then - echo '.gitlab-ci.yml exists' - echo '::set-output name=exists::true' - else - echo '::error ::The ".gitlab-ci.yml" file is missing!' - echo '::set-output name=exists::false' - false - fi - name: 'Validate GLCI file syntax' id: validate-glci-file - if: steps.glci-file-exists.outputs.exists == 'true' - env: - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} # https://gitlab.com/api/v4 - GITLAB_API_PRIVATE_TOKEN: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} - run: | - GITLAB_API_URL="${GITLAB_API_URL:-https://gitlab.com/api/v4}" - CURL_CMD=(curl --http1.1 --fail --silent --show-error --header 'Content-Type: application/json' --data @-) - [ -n "$GITLAB_API_PRIVATE_TOKEN" ] && CURL_CMD+=(--header "Authorization: Bearer $GITLAB_API_PRIVATE_TOKEN") - data="$(jq --null-input --arg yaml "$(<.gitlab-ci.yml)" '.content=$yaml' )" - response="$(echo "$data" | "${CURL_CMD[@]}" "${GITLAB_API_URL}/ci/lint?include_merged_yaml=true" | jq . )" - status=$( echo "$response" | jq .status ) - if [[ "$status" == '"valid"' ]]; then - echo '.gitlab-ci.yml is valid' - echo '::set-output name=valid::true' - else - echo '::set-output name=valid::false' - echo '::error::The .gitlab-ci.yml" file is invalid!' - echo "$response" | jq -r '.errors[] | . = "::error ::\(.)"' - printf "::debug ::.gitlab-ci.yml CI lint service response: %s\n" "$response" - false - fi + uses: simp/github-action-gitlab-ci-syntax-check@main + with: + gitlab_api_private_token: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + gitlab_api_url: ${{ secrets.GITLAB_API_URL }} # https://gitlab.com/api/v4 trigger-when-user-has-repo-permissions: name: 'Trigger CI' needs: [ glci-syntax ] - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 with: repository: ${{ needs.glci-syntax.outputs.pr_head_full_name }} ref: ${{ needs.glci-syntax.outputs.pr_head_sha }} - token: ${{secrets.SIMP_AUTO_GITHUB_TOKEN__NO_SCOPE}} + token: ${{secrets.NO_SCOPE_GITHUB_TOKEN}} fetch-depth: 0 # Need full checkout to push to gitlab mirror clean: true - - name: Trigger CI - env: - GITLAB_SERVER_URL: ${{ secrets.GITLAB_SERVER_URL }} # https://gitlab.com - GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} # https://gitlab.com/api/v4 - GITLAB_ORG: 'simp' - GITLAB_API_PRIVATE_TOKEN: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} - GIT_BRANCH: ${{ needs.glci-syntax.outputs.pr_head_ref }} - run: | - GITLAB_SERVER_URL="${GITLAB_SERVER_URL:-https://gitlab.com}" - GITLAB_API_URL="${GITLAB_API_URL:-${GITLAB_SERVER_URL}/api/v4}" - GITXXB_REPO_NAME="${GITHUB_REPOSITORY/$GITHUB_REPOSITORY_OWNER\//}" - GITLAB_PROJECT_ID="${GITLAB_ORG}%2F${GITXXB_REPO_NAME}" - # --http1.0 avoids an HTTP/2 load balancing issue when run from GA - CURL_CMD=(curl --http1.0 --fail --silent --show-error \ - --header "Authorization: Bearer $GITLAB_API_PRIVATE_TOKEN" \ - --header "Content-Type: application/json" \ - --header "Accept: application/json" \ - ) - - # Cancel any active/pending GitLab CI pipelines for the same project+branch - active_pipeline_ids=() - for pipe_status in created waiting_for_resource preparing pending running; do - echo " ---- checking for CI pipelines with status '$pipe_status' for project '$GITLAB_PROJECT_ID', branch '$GIT_BRANCH'" - url="${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/pipelines?ref=${GIT_BRANCH}&status=${pipe_status}" - active_pipelines="$("${CURL_CMD[@]}" "$url" | jq -r '.[] | .id , .web_url')" - active_pipeline_ids+=($(echo "$active_pipelines" | grep -E '^[0-9]*$')) - printf "$active_pipelines\n\n" - done - if [ "${#active_pipeline_ids[@]}" -gt 0 ]; then - printf "\nFound %s active pipeline ids:\n" "${#active_pipeline_ids[@]}" - echo "${active_pipeline_ids[@]}" - for pipe_id in "${active_pipeline_ids[@]}"; do - printf "\n ------ Cancelling pipeline ID %s...\n" "$pipe_id" - "${CURL_CMD[@]}" --request POST "${GITLAB_API_URL}/projects/${GITLAB_PROJECT_ID}/pipelines/${pipe_id}/cancel" - done - else - echo No active pipelines found - fi - - # Should we protect against pushing default branches? - echo "== Pushing '$GIT_BRANCH' to gitlab" - git remote add gitlab "https://oauth2:${GITLAB_API_PRIVATE_TOKEN}@${GITLAB_SERVER_URL#*://}/${GITLAB_ORG}/${GITXXB_REPO_NAME}.git" - git branch "$GIT_BRANCH" HEAD || : - git branch -av - git log --color --graph --abbrev-commit -5 \ - --pretty=format:'%C(red)%h%C(reset) -%C(yellow)%d%Creset %s %Cgreen(%ci) %C(bold blue)<%an>%Creset' - git push gitlab ":${GIT_BRANCH}" -f || : # attempt to un-weird GLCI's `changed` tracking - echo "== git push --verbose gitlab ${GIT_BRANCH}" - git push --verbose gitlab "${GIT_BRANCH}" - echo "Pushed branch '${GIT_BRANCH}' to gitlab" - echo " A new pipeline should be at: https://${GITLAB_SERVER_URL#*://}/${GITLAB_ORG}/${GITXXB_REPO_NAME}/-/pipelines/" + - name: Trigger CI when user has Repo Permissions + uses: simp/github-action-gitlab-ci-pipeline-trigger@v1 + with: + git_hashref: ${{ needs.glci-syntax.outputs.pr_head_sha }} + git_branch: ${{ needs.glci-syntax.outputs.pr_head_ref }} + gitlab_api_private_token: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + gitlab_group: ${{ github.event.organization.login }} + github_repository: ${{ github.repository }} + github_repository_owner: ${{ github.repository_owner }} ### examine_contexts: ### needs: [ glci-syntax ] ### name: 'Examine Context contents' ### if: always() -### runs-on: ubuntu-latest +### runs-on: ubuntu-18.04 ### steps: ### - name: Dump contexts ### env: @@ -186,4 +141,3 @@ jobs: ### run: echo "$ENV_CONTEXT" ### - name: Dump env vars ### run: env | sort - diff --git a/.github/workflows/release_rpms.yml b/.github/workflows/release_rpms.yml new file mode 100644 index 00000000..31dfe616 --- /dev/null +++ b/.github/workflows/release_rpms.yml @@ -0,0 +1,326 @@ +# 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 + dry_run: + description: "Dry run (Test-build RPMs)" + required: false + default: 'no' + verbose: + description: 'Verbose RPM builds when "yes"' + 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: + ${{ (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|alpha|beta|pre)?([0-9]+)?)?$ ]]; then + if [ -n "${BASH_REMATCH[5]}" ]; then + echo "::set-output name=prebuild_number::${BASH_REMATCH[5]#-}" + fi + if [ -n "${BASH_REMATCH[3]}" ]; then + echo "::set-output name=prebuild_suffix::${BASH_REMATCH[3]#-}" + fi + if [ -n "${BASH_REMATCH[2]}" ]; then + echo "::set-output name=build_semver::${BASH_REMATCH[2]}" + fi + else + printf '::error ::Release Tag format is not SemVer, X.Y.Z-R, X.Y.Z-: "%s"\n' "$RELEASE_TAG" + echo 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@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') + 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@v2 + 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 + # simp-doc uses a different file format for /release (:facepalm:) + 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: > + 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' + verbose: "${{ 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@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_github-rpms.yml b/.github/workflows/tag_deploy_github-rpms.yml new file mode 100644 index 00000000..7cd2d8ee --- /dev/null +++ b/.github/workflows/tag_deploy_github-rpms.yml @@ -0,0 +1,129 @@ +# When a SemVer tag is pushed, create GitHub release & trigger RPM build +# ------------------------------------------------------------------------------ +# +# 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 +# ------------------------------- --------------------------------------- +# 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' + +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: '~> 6' + +jobs: + create-github-release: + name: Deploy GitHub Release + if: github.repository_owner == 'simp' + runs-on: ubuntu-18.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + 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)" + + echo "::set-output name=tag::${tag}" + echo "::set-output name=annotation_title::${annotation_title}" + + if [[ "$tag" =~ ^(simp-|v)?[0-9]+\.[0-9]+\.[0-9]+(-(rc|alpha|beta|pre|post)?([0-9]+)?)?$ ]]; then + if [ -n "${BASH_REMATCH[2]}" ]; then + echo "::set-output name=prerelease::true" + 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 + + # Prepare annotation body as a file for the next step + # + # * The GitHub Release render the text in this file as markdown + # * The file is needed because :set-output only supports single lines + # * The `perl -pe` removes RPM-style date headers from the CHANGELOG, + # because they don't render well as markdown on the Release page + # + echo "$annotation" | tail -n +2 | \ + perl -pe 'BEGIN{undef $/;} s/\n\* (Mon|Tue|Wed|Thu|Fri|Sat|Sun) .*?\n//smg;' > /tmp/annotation.body + + - name: Create Release + uses: actions/create-release@v1 + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ steps.tag-check.outputs.annotation_title }} + body_path: /tmp/annotation.body + prerelease: ${{ steps.tag-check.outputs.prerelease }} + draft: false + + 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 }} + outputs: + prerelease: ${{ steps.tag-check.outputs.prerelease == 'true' }} + 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: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + TARGET_TAG: ${{ steps.tag-check.outputs.tag }} + with: + 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}` ) + }) diff --git a/.github/workflows/validate_tokens_asset.yml b/.github/workflows/validate_tokens_asset.yml new file mode 100644 index 00000000..4c95714e --- /dev/null +++ b/.github/workflows/validate_tokens_asset.yml @@ -0,0 +1,72 @@ +# 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 +# ------------------------ -------- ---------------------------------------- +# GITLAB_API_PRIVATE_TOKEN Required GitLab token (should have `api` scope) +# NO_SCOPE_GITHUB_TOKEN Required GitHub token (should have no scopes) +# GITLAB_SERVER_URL Optional Specify a GL server other than gitlab.com +# 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: + gitlab: + name: 'GitLab token has scope for developer' + runs-on: ubuntu-18.04 + env: + GITLAB_API_PRIVATE_TOKEN: ${{ secrets.GITLAB_API_PRIVATE_TOKEN }} + GITLAB_API_URL: ${{ secrets.GITLAB_API_URL }} + GITLAB_ORG: ${{ github.event.organization.login }} + steps: + - run: | + GITLAB_API_URL="${GITLAB_API_URL:-https://gitlab.com/api/v4}" + curl -I --http1.1 --fail --silent --show-error \ + --header 'Content-Type: application/json' \ + --header "Authorization: Bearer $GITLAB_API_PRIVATE_TOKEN" \ + "${CURL_CMD[@]}" "${GITLAB_API_URL}/groups/$GITLAB_ORG/audit_events" + + github-no-scope: + name: 'No-scope GitHub token has NO scopes' + runs-on: ubuntu-18.04 + 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 +