From 8a47952cfc9a28d2fe23c05aaa52d77479a683fe Mon Sep 17 00:00:00 2001 From: Adam Kaplan Date: Thu, 16 Dec 2021 15:54:49 -0500 Subject: [PATCH] Verifiable Release Process - Add script to draft release notes. - Add release script to build and push images. - Update Makefile so the image host and namespace can be configured. - Update release workflow to push images to ghcr.io. - Sign the operator image with cosign --- .github/draft_release_notes.sh | 151 +++++++++++++++++++++++++++++++++ .github/workflows/release.yaml | 73 +++++++++++----- Makefile | 8 +- hack/release.sh | 31 +++++++ 4 files changed, 239 insertions(+), 24 deletions(-) create mode 100755 .github/draft_release_notes.sh create mode 100755 hack/release.sh diff --git a/.github/draft_release_notes.sh b/.github/draft_release_notes.sh new file mode 100755 index 00000000..2f8859d2 --- /dev/null +++ b/.github/draft_release_notes.sh @@ -0,0 +1,151 @@ +#! /bin/bash +# Copyright The Shipwright Contributors +# +# SPDX-License-Identifier: Apache-2.0 + +# This script assumes the GITHUB_TOKEN and REPOSITORY environment variables have been set; +# The PREVIOUS_TAG environment variable is optional - if omitted it defaults to the first commit of +# the repository. +# The script produces a 'Changes.md' file as its final output; +# the file 'last-300-prs-with-release-note.txt' that is produces is intermediate data; it is not +# pruned for now to assist development of the release notes process (we are still curating all this) + +if [ -z ${GITHUB_TOKEN+x} ]; then + echo "Error: GITHUB_TOKEN is not set" + exit 1 +fi + +if [ -z "${REPOSITORY}" ]; then + echo "Error: GitHub repository was not set" + exit 1 +fi + +# sudo apt-get -y update +# sudo apt-get -y install wget curl git +curl -L https://github.com/github/hub/releases/download/v2.14.2/hub-linux-amd64-2.14.2.tgz | tar xzf - +PWD="$(pwd)" +export PATH=$PWD/hub-linux-amd64-2.14.2/bin:$PATH +git fetch --all --tags --prune --force + +if [ -z ${PREVIOUS_TAG+x} ]; then + PREVIOUS_TAG=$(git rev-list --max-parents=0 HEAD) +fi + +echo "# Draft Release changes since ${PREVIOUS_TAG}" > Changes.md +echo > Features.md +echo "## Features" >> Features.md +echo > Fixes.md +echo "## Fixes" >> Fixes.md +echo > API.md +echo "## API Changes" >> API.md +echo > Docs.md +echo "## Docs" >> Docs.md +echo > Misc.md +echo "## Misc" >> Misc.md + +# this effectively gets the commit associated with github.event.inputs.tags +COMMON_ANCESTOR=$(git merge-base $PREVIOUS_TAG HEAD) +echo "COMMON_ANCESTOR is ${COMMON_ANCESTOR}" +# in theory the new tag has not been created yet; do we want another input that specifies the existing +# commit desired for drafting the release? for now, we are using HEAD in the above git merge-base call +# and PR cross referencing below + +# use of 'hub', which is an extension of the 'git' CLI, allows for pulling of PRs, though we can't search based on commits +# associated with those PRs, so we grab a super big number, 300, which should guarantee grabbing all the PRs back to +# github.events.inputs.tags; we use grep -v to filter out release-note-none and release-note-action-required. +# NOTE: investigated using the new 'gh' cli command, but its 'gh pr list' does not currently support the -f option so +# staying with 'hub' for now. +hub pr list --state merged -L 300 -f "%sm;%au;%i;%t;%L%n" | grep -E ", release-note|release-note," | grep -v release-note-none | grep -v release-note-action-required > last-300-prs-with-release-note.txt +# this is for debug while we sort out env differences between Gabe's fedora and GitHub Actions' ubuntu +echo "start dump last-300-prs-with-release-note.txt for potential debug" +cat last-300-prs-with-release-note.txt +echo "end dump last-300-prs-with-release-note.txt for potential debug" +# now we cylce through last-300-prs-with-release-note.txt, filtering out stuff that is too old or other anomalies, +# and update Changes.md with the release note. +while IFS= read -r pr; do + SHA=$(echo $pr | cut -d';' -f1) + + # skip the common ancestor, which in essences is the commit associated with the tag github.event.inputs.tags + if [ "$SHA" == "$COMMON_ANCESTOR" ]; then + continue + fi + + # stylistic clarification, purposefully avoiding slicker / cleverer / more compact scripting conventions + + # this makes sure that this PR has merged + git merge-base --is-ancestor $SHA HEAD + rc=$? + if [ ${rc} -eq 1 ]; then + continue + fi + # otherwise, if the current commit from the last 300 PRs is not an ancestor of github.event.inputs.tags, we have gone too far, so skip + git merge-base --is-ancestor $COMMON_ANCESTOR $SHA + rc=$? + if [ ${rc} -eq 1 ]; then + continue + fi + # if we are at this point, we have a PR with a release note to add + AUTHOR=$(echo $pr | cut -d';' -f2) + PR_NUM=$(echo $pr | cut -d';' -f3) + echo "Examining from ${AUTHOR} PR ${PR_NUM}" + PR_BODY=$(wget -q -O- "https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUM:1}") + echo "$PR_BODY" | grep -oPz '(?s)(?<=```release-note..)(.+?)(?=```)' > /dev/null 2>&1 + rc=$? + if [ ${rc} -eq 1 ]; then + echo "First validation: the release-note field for PR ${PR_NUM} was not properly formatted. Until it is fixed, it will be skipped for release note inclusion." + echo "See the PR template at https://raw.githubusercontent.com/${REPOSITORY}/master/.github/pull_request_template.md for verification steps" + continue + fi + PR_BODY_FILTER_ONE=$(echo $PR_BODY | grep -oPz '(?s)(?<=```release-note..)(.+?)(?=```)') + echo $PR_BODY_FILTER_ONE | grep -avP '\W*(Your release note here|action required: your release note here|NONE)\W*' > /dev/null 2>&1 + rc=$? + if [ ${rc} -eq 1 ]; then + echo "Second validation: the release-note field for PR ${PR_NUM} was not properly formatted. Until it is fixed, it will be skipped for release note inclusion." + echo "See the PR template at https://raw.githubusercontent.com/${REPOSITORY}/master/.github/pull_request_template.md for verification steps" + continue + fi + PR_RELEASE_NOTE=$(echo $PR_BODY_FILTER_ONE | grep -avP '\W*(Your release note here|action required: your release note here|NONE)\W*') + PR_RELEASE_NOTE_NO_NEWLINES=$(echo $PR_RELEASE_NOTE | sed 's/\\n//g' | sed 's/\\r//g') + MISC=yes + echo $pr | grep 'kind/bug' + rc=$? + if [ ${rc} -eq 0 ]; then + echo >> Fixes.md + echo "$PR_NUM by @${AUTHOR}: $PR_RELEASE_NOTE_NO_NEWLINES" >> Fixes.md + MISC=no + fi + echo $pr | grep 'kind/api-change' + rc=$? + if [ ${rc} -eq 0 ]; then + echo >> API.md + echo "$PR_NUM by @${AUTHOR}: $PR_RELEASE_NOTE_NO_NEWLINES" >> API.md + MISC=no + fi + echo $pr | grep 'kind/feature' + rc=$? + if [ ${rc} -eq 0 ]; then + echo >> Features.md + echo "$PR_NUM by @${AUTHOR}: $PR_RELEASE_NOTE_NO_NEWLINES" >> Features.md + MISC=no + fi + echo $pr | grep 'kind/documentation' + rc=$? + if [ ${rc} -eq 0 ]; then + echo >> Docs.md + echo "$PR_NUM by @${AUTHOR}: $PR_RELEASE_NOTE_NO_NEWLINES" >> Docs.md + MISC=no + fi + if [ "$MISC" == "yes" ]; then + echo >> Misc.md + echo "$PR_NUM by @${AUTHOR}: $PR_RELEASE_NOTE_NO_NEWLINES" >> Misc.md + fi + # update the PR template if our greps etc. for pulling the release note changes + #PR_RELEASE_NOTE=$(wget -q -O- https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUM:1} | grep -oPz '(?s)(?<=```release-note..)(.+?)(?=```)' | grep -avP '\W*(Your release note here|action required: your release note here|NONE)\W*') + echo "Added from ${AUTHOR} PR ${PR_NUM:1} to the release note draft" +done < last-300-prs-with-release-note.txt + +cat Features.md >> Changes.md +cat Fixes.md >> Changes.md +cat API.md >> Changes.md +cat Docs.md >> Changes.md +cat Misc.md >> Changes.md diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 109784b2..9c2b98ec 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,36 +6,63 @@ on: release: description: 'Desired tag' required: true - tags: + previous-tag: description: 'Previous tag' - required: true - + required: false jobs: release: - if: ${{ github.repository == 'shipwright-io/operator' }} + # if: ${{ github.repository == 'shipwright-io/build' }} runs-on: ubuntu-latest + permissions: + id-token: write # To be able to get OIDC ID token to sign images. + contents: write # To be able to update releases. + packages: write # To be able to push images and signatures. + env: + IMAGE_HOST: ghcr.io + IMAGE_NAMESPACE: ${{ github.repository }} + VERSION: ${{ github.event.inputs.release }} steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Install Go - uses: actions/setup-go@v2 - - - name: Login to docker + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all history, needed for release note generation. + # Install tools + - uses: actions/setup-go@v2 + with: + go-version: 1.17.x + - uses: sigstore/cosign-installer@v1.2.0 + - name: Build Release Images env: - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} - run: | - echo "$REGISTRY_PASSWORD" | docker login --username "$REGISTRY_USERNAME" --password-stdin - - - name: Build and upload Operator Image + REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + REGISTRY_USERNAME: ${{ github.repository_owner }} + run: + make release + - name: Sign released images env: - VERSION: ${{ github.events.input.release }} + # This enables keyless mode + # (https://github.com/sigstore/cosign/blob/main/KEYLESS.md) which signs + # images using an ephemeral key tied to the GitHub Actions identity via + # OIDC. + COSIGN_EXPERIMENTAL: "true" run: | - make ko-publish - - - name: Build and upload Operator Bundle Image + grep -o "ghcr.io[^\"]*" "${GITHUB_WORKSPACE}/bundle/manifests/shipwright-operator.clusterserviceversion.yaml" | xargs cosign sign \ + -a sha=${{ github.sha }} \ + -a run_id=${{ github.run_id }} \ + -a run_attempt=${{ github.run_attempt }} + - name: Build Release Changelog env: - VERSION: ${{ github.events.input.release }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PREVIOUS_TAG: ${{ github.event.inputs.previous-tag }} + REPOSITORY: ${{ github.repository }} run: | - make bundle-push + "${GITHUB_WORKSPACE}/.github/draft_release_notes.sh" + - name: Draft release + id: draft_release + uses: actions/create-release@v1 + with: + release_name: ${{ github.event.inputs.release }} + tag_name: ${{ github.event.inputs.release }} + draft: true + prerelease: true + body_path: Changes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index f50eb698..789b5515 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,9 @@ GOBIN=$(shell go env GOBIN) endif CONTAINER_ENGINE ?= docker -IMAGE_REPO ?= quay.io/shipwright +IMAGE_HOST ?= quay.io +IMAGE_NAMESPACE ?= shipwright +IMAGE_REPO ?= $(IMAGE_HOST)/$(IMAGE_NAMESPACE) TAG ?= $(VERSION) IMAGE_PUSH ?= true @@ -186,6 +188,10 @@ bundle-build: bundle bundle-push: bundle-build $(CONTAINER_ENGINE) push $(BUNDLE_IMG) +.PHONY: release +release: ko + CONTAINER_ENGINE="$(CONTAINER_ENGINE)" KO_BIN="$(KO)" IMAGE_HOST=${IMAGE_HOST} IMAGE_NAMESPACE=${IMAGE_NAMESPACE} TAG=${TAG} hack/release.sh + # Install OLM on the current cluster .PHONY: install-olm install-olm: operator-sdk diff --git a/hack/release.sh b/hack/release.sh new file mode 100755 index 00000000..89567d3d --- /dev/null +++ b/hack/release.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +KO_BIN=${KO_BIN:-ko} +CONTAINER_ENGINE=${CONTAINER_ENGINE:-docker} +USERNAME=${REGISTRY_USERNAME:-""} +PASSWORD=${REGISTRY_PASSWORD:-""} + +function login() { + echo "Logging into container registry $IMAGE_HOST" + echo "$PASSWORD" | ${KO_BIN} login -u "$USERNAME" --password-stdin "$IMAGE_HOST" + echo "$PASSWORD" | ${CONTAINER_ENGINE} login -u "$USERNAME" --password-stdin "$IMAGE_HOST" +} + +if [ -z "${IMAGE_HOST}" ]; then + echo "Error: image host is not set" + exit 1 +fi + +if [ -n "${USERNAME}" ] && [ -n "${PASSWORD}" ]; then + login +else + echo "Skipping registry login - build will rely on existing credentials" +fi + +echo "Building and pushing operator image" +make ko-publish + +echo "Regenerating bundle and pushing bundle image" +make bundle-push CONTAINER_ENGINE="${CONTAINER_ENGINE}"