diff --git a/.github/workflows/create-release-branch.yml b/.github/workflows/create-release-branch.yml new file mode 100644 index 000000000..4b2304266 --- /dev/null +++ b/.github/workflows/create-release-branch.yml @@ -0,0 +1,151 @@ +name: Create release branch + +on: + workflow_dispatch: + inputs: + major_minor_version: + description: 'Major.Minor release version' + required: true + base_commit: + description: 'Base commit SHA' + required: true + pull_request: + # Workflow should only ever be run from main, so exclude + # running on pull requests to release branch resources. + branches: ['main'] + paths: + # Run workflow on changes to the workflow definition and its + # dependencies to spot check the workflow functionality. + - '.github/workflows/create-release-branch.yml' + - 'scripts/create-release-branch.sh' + - 'scripts/build-third-party-licenses.sh' + - 'scripts/update-getting-started-guide-.sh' + +env: + MAJOR_MINOR_VERSION: '' + BASE_COMMIT: '' + +jobs: + test-create-branch: + if: github.event_name == 'pull_request' + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v4 + + - name: Mock workflow inputs on pull request + run: | + echo "MAJOR_MINOR_VERSION=0.${{ github.event.pull_request.number }}" >> $GITHUB_ENV + echo "BASE_COMMIT=${{ github.sha }}" >> $GITHUB_ENV + + - name: Test create release branch + run: bash scripts/create-release-branch.sh --assert --base ${{ env.BASE_COMMIT }} --dry-run ${{ env.MAJOR_MINOR_VERSION }} + + - name: Install go-licenses + run: go install github.com/google/go-licenses@v1.6.0 + + - name: Generate third party licenses file + run: bash scripts/build-third-party-licenses.sh + + - name: Test update getting started version in release branch + run: bash scripts/update-getting-started-guide-version.sh --assert ${{ env.MAJOR_MINOR_VERSION }} + + - name: Test rollback create releae branch + run: bash scripts/create-release-branch.sh --assert --dry-run --rollback ${{ env.MAJOR_MINOR_VERSION }} + + create-branch: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-20.04 + + permissions: + # Write permissions needed to create release branch. + # Risk for pwn requests is mitigated by seperating jobs such that + # workflows running with write permissions only use code from main. + contents: write + + steps: + - uses: actions/checkout@v4 + with: + ref: main + sparse-checkout: | + scripts/create-release-branch.sh + + - name: Set environment variables for workflow + run: | + echo "MAJOR_MINOR_VERSION=${{ github.event.inputs.major_minor_version }}" >> $GITHUB_ENV + echo "BASE_COMMIT=${{ github.event.inputs.base_commit }}" >> $GITHUB_ENV + + - name: Create release branch + run: bash scripts/create-release-branch.sh --dry-run --base ${{ env.BASE_COMMIT }} ${{ env.MAJOR_MINOR_VERSION }} + + initial-pr: + needs: create-branch + if: github.event_name == 'workflow_dispatch' && needs.create-branch.result == 'success' + runs-on: ubuntu-20.04 + + permissions: + # Write permissions needed to create pull request. + # Risk for pwn requests is mitigated by seperating jobs such that + # workflows running with write permissions only use code from the + # branch which was cut from main. + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + ref: release/${{ env.MAJOR_MINOR_VERSION }} + + - uses: actions/setup-go@v5 + + - name: Install go-licenses + run: go install github.com/google/go-licenses@v1.6.0 + + - name: Generate third party licenses file + run: bash scripts/build-third-party-licenses.sh + + - name: Update getting started version in release branch + run: bash scripts/update-getting-started-guide-version.sh --verbose "${{ env.MAJOR_MINOR_VERSION }}.0" + + - name: Create PR + uses: peter-evans/create-pull-request@v6 + with: + title: 'Prepare release ${{ env.MAJOR_MINOR_VERSION }}' + commit-message: | + Prepare release ${{ env.MAJOR_MINOR_VERSION }} + + This change adds the THIRD_PARTY_LICENSES file and updates the getting started guide for release/${{ env.MAJOR_MINOR_VERSION }}. + body: | + This change adds the THIRD_PARTY_LICENSES file and updates the getting started guide for release/${{ env.MAJOR_MINOR_VERSION }}. + + Auto-generated by [create-pull-request](https://github.com/peter-evans/create-pull-request) + labels: easy-to-review, automated-pr + token: ${{ secrets.GITHUB_TOKEN }} + author: "GitHub " + signoff: true + branch: 'create-pull-request/prepare-release-${{ env.MAJOR_MINOR_VERSION }}' + base: 'release/${{ env.MAJOR_MINOR_VERSION }}' + delete-branch: true + + auto-rollback: + needs: initial-pr + # If the workflow was unable to create the pull request with the THIRD_PARTY_LICENSES file + # and getting started guide version updates, then the release branch should be rolled back. + if: github.event_name == 'workflow_dispatch' && needs.initial-pr.result == 'failure' + runs-on: ubuntu-20.04 + + permissions: + # Write permissions needed to rollback release branch. + # Risk for pwn requests is mitigated by seperating jobs such that + # workflows running with write permissions only use code from main. + contents: write + + steps: + - uses: actions/checkout@v4 + with: + ref: main + sparse-checkout: | + scripts/create-release-branch.sh + + - name: Delete release branch + run: bash scripts/create-release-branch.sh --rollback ${{ env.MAJOR_MINOR_VERSION }} diff --git a/.github/workflows/update-getting-started-guide.yml b/.github/workflows/update-getting-started-guide.yml index 5f896a681..78d49c5e4 100644 --- a/.github/workflows/update-getting-started-guide.yml +++ b/.github/workflows/update-getting-started-guide.yml @@ -4,7 +4,7 @@ on: release: types: ['released'] pull_request: - branches: ['main', 'release/**'] + branches: ['main'] paths: # Run workflow on changes to the workflow definition itself to spot check # the core version update functionality. @@ -45,8 +45,8 @@ jobs: permissions: # Write permissions needed to create pull request. - # Risk is mitigated by seperating jobs such that workflows - # running with write permissions only use code from main. + # Risk for pwn requests is mitigated by seperating jobs such that + # workflows running with write permissions only use code from main. contents: write pull-requests: write diff --git a/scripts/create-release-branch.sh b/scripts/create-release-branch.sh new file mode 100755 index 000000000..3b6f45c4f --- /dev/null +++ b/scripts/create-release-branch.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash + +# Copyright The Soci Snapshotter Authors. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A script to create a release branch on origin from the given commit. +# +# Usage: bash create-release-branch.sh [-a|--assert] [-b|--base] [-d|--dry-run] [-r|--rollback] + +set -eux -o pipefail + +ASSERT=false +BASE_COMMIT="" +DRYRUN=false +ROLLBACK=false + +while [[ $# -gt 0 ]]; do + case $1 in + --assert|-a) + ASSERT=true + shift # past argument + ;; + --base|-b) + shift # past argument + BASE_COMMIT=$1 + shift # past value + ;; + --dry-run|-d) + DRYRUN=true + shift # past argument + ;; + --rollback|-r) + ROLLBACK=true + shift # past argument + ;; + --*|-*) + echo "Unknown option $1" + exit 1 + ;; + *) + VERSION=$1 + shift # past argument + ;; + esac +done + +sanitize_input() { + # Strip 'v' prefix from input if present. + VERSION=${VERSION/v/} + [[ $VERSION =~ ^[0-9]+\.[0-9]+$ ]] || (echo "Error: version does not match expected . format" && exit 1) + + if [ -n "$BASE_COMMIT" ]; then + [[ $BASE_COMMIT =~ ^[0-9a-fA-F]{7,40}$ ]] || (echo "Error: base commit does not match expected short|full format" && exit 1) + FOUND=$(git log --pretty=format:"%H" | grep "$BASE_COMMIT") + [ -n "$FOUND" ] || (echo "Error: base commit not found in history" && exit 1) + fi +} + +assert_create_branch() { + if [ $ROLLBACK = true ]; then + [ ${#PUSH_OPTS[@]} -eq 0 ] || (echo "Error: rollback request but '--delete' not set as push option" && exit 1) + else + local current_branch + current_branch=$(git rev-parse --abbrev-ref HEAD) + [ $current_branch = "release/${VERSION}" ] || (echo "Error: incorrect branch, expected: release/${VERSION}, actual: $current_branch" && exit 1) + + local base_commit + base_commit=$(git show -s --format="%H") + [ $base_commit = $BASE_COMMIT ] || (echo "Error: incorrect base commit, expected: $BASE_COMMIT, actual: $base_commit" && exit 1) + fi +} + +assert_push_command() { + local push_cmd + push_cmd=$1 + + if [ $ROLLBACK = true ]; then + [[ $push_cmd == "git push --delete origin release/${VERSION}" ]] || (echo "Error: expected '--delete' in git push for rollback" && exit 1) + else + [[ $push_cmd == "git push origin release/${VERSION}" ]] || (echo "Error: expected: 'git push origin release/${VERSION}', actual: '$push_cmd'" && exit 1) + fi +} + +sanitize_input + +PUSH_OPTS=() +if [ $ROLLBACK = true ]; then + echo "Rollback: setting '--delete' for git push" + PUSH_OPTS+=("--delete") +else + git checkout -b "release/${VERSION}" "${BASE_COMMIT}" +fi + +[ $ASSERT = true ] && assert_create_branch + +PUSH_CMD='git push "${PUSH_OPTS[@]}" origin "release/${VERSION}"' +if [ $DRYRUN = true ]; then + # Dry-run mode is not able to use git push --dry-run as it still requires + # write permissions to the remote repository. The intent is to run dry-run mode + # in pull request workflows with a reduced permission set to mitigate risk for pwn requests. + # Alternatively assert the push command is correct. + assert_push_command "$(eval echo ${PUSH_CMD})" +else + $PUSH_CMD +fi diff --git a/scripts/update-getting-started-guide-version.sh b/scripts/update-getting-started-guide-version.sh index 00147a333..0b57a6890 100755 --- a/scripts/update-getting-started-guide-version.sh +++ b/scripts/update-getting-started-guide-version.sh @@ -21,8 +21,6 @@ set -eux -o pipefail -tag=$1 - ASSERT=false VERBOSE=false @@ -41,14 +39,19 @@ while [[ $# -gt 0 ]]; do exit 1 ;; *) - tag=$1 + VERSION=$1 shift # past argument ;; esac done -# Strip 'v' prefix from tag if not already stripped. -VERSION=${tag/v/} +sanitize_input() { + # Strip 'v' prefix from input if present. + VERSION=${VERSION/v/} + [[ $VERSION =~ ^([0-9]+\.){2}[0-9]+(-.*){0,1}$ ]] || (echo "Error: version does not match expect .. version format" && exit 1) +} + +sanitize_input assert_diff() { local diff_output