From 2b82a91ff20cf2d15f7c5eee0f06735db33900d6 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 30 Jan 2020 14:57:11 -0800 Subject: [PATCH 1/3] Experimental release workflow --- .github/scripts/prepare_release.sh | 70 ++++++++++++++++++ .github/workflows/release.yml | 113 +++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100755 .github/scripts/prepare_release.sh create mode 100644 .github/workflows/release.yml diff --git a/.github/scripts/prepare_release.sh b/.github/scripts/prepare_release.sh new file mode 100755 index 000000000..f75cf0d26 --- /dev/null +++ b/.github/scripts/prepare_release.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +###################################### Outputs ##################################### + +# 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3). +# 2. publish: Set when not executing in the dryrun mode. +# 3. tweet: Set when the release should be posted to Twitter. Also implies +# publish=true. +# 4. create_tag: Set when the release is not already tagged. +# 5. reuse_tag: Set when the release is already tagged. +# 6. directory: Directory where the release artifacts will be built. Either +# 'staging' or 'deploy'. + +#################################################################################### + +echo "[release:retry]: ${RETRY_RELEASE}" +echo "[release:dryrun]: ${DRYRUN_RELEASE}" +echo "[release:skip-tweet]: ${SKIP_TWEET}" +echo + +# Find current version. +RELEASE_VERSION=`python -c "exec(open('firebase_admin/__about__.py').read()); print(__version__)"` +echo "Releasing version ${RELEASE_VERSION}" +echo "::set-output name=version::v${RELEASE_VERSION}" + +# Handle dryrun mode. +if [[ "$DRYRUN_RELEASE" == "true" ]]; then + echo "Dryrun mode has been requested. No new tags or artifacts will be published." + DIRECTORY="staging" +else + echo "Dryrun mode has not been requested. Executing in the publish mode." + DIRECTORY="deploy" + echo "::set-output name=publish::true" + + if [[ "${SKIP_TWEET}" != "true" ]]; then + echo "Release will be posted to Twitter upon successful completion." + echo "::set-output name=tweet::true" + else + echo "Skip Tweet mode has been requested. Release will not be posted to Twitter." + fi +fi + +# Fetch all tags. +git fetch --depth=1 origin +refs/tags/*:refs/tags/* + +# Check if this release is already tagged. +git describe --tags v${RELEASE_VERSION} 2> /dev/null + +if [[ $? -eq 0 ]]; then + echo "Tag v${RELEASE_VERSION} already exists." + + if [[ "${RETRY_RELEASE}" != "true" ]]; then + echo "Retry mode has not been requested. Exiting." + echo "Label your PR with [release: retry] to build a release from an existing tag." + exit 1 + fi + + echo "Retry mode has been requested. Releasing from the existing tag." + echo "::set-output name=reuse_tag::true" + + # When a tag already exists, we will use it to build artifacts even when + # the dryrun mode is requested. + DIRECTORY="deploy" +else + echo "Tag v${RELEASE_VERSION} does not exist." + echo "::set-output name=create_tag::true" +fi + +echo "Release artifacts will be built in the ${DIRECTORY} directory." +echo "::set-output name=directory::${DIRECTORY}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..6b9cedce0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,113 @@ +name: Release + +on: + pull_request: + branches: + - master + types: + - closed + +jobs: + publish_release: + # Only run the workflow when a pull request has been: + # 1. merged to the master branch + # 2. baring the label 'release:pending', and + # 3. the title prefix '[chore] Release'. + if: github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'release:pending') && + startsWith(github.event.pull_request.title, '[chore] Release') + + runs-on: ubuntu-latest + + # Checkout source to the staging directory. + steps: + - name: Checkout source for staging + uses: actions/checkout@v2 + with: + path: staging + + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.6 + + # Handle additional options/labels passed with the release PR. + # 1. release:retry - Continue the release from an already existing tag. + # 2. release:dryrun - Dry run the release process without publishing new tags or artifacts. + # 3. release:skip-tweet - Complete the release process without posting to Twitter. + - name: Prepare release + id: prepare + run: ./.github/scripts/prepare_release.sh + working-directory: staging + env: + RETRY_RELEASE: ${{ contains(github.event.pull_request.labels.*.name, 'release:retry') }} + DRYRUN_RELEASE: ${{ contains(github.event.pull_request.labels.*.name, 'release:dryrun') }} + SKIP_TWEET: ${{ contains(github.event.pull_request.labels.*.name, 'release:skip-tweet') }} + + # Dependencies are needed to both run the tests and package the release. + # Therefore we run this step unconditionally. + - name: Install dependencies + working-directory: staging + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + # Only run if the release is not already tagged. + - name: Run tests + if: success() && steps.prepare.outputs.create_tag + working-directory: staging + run: | + pytest + echo "Running integration tests" + + # Tag the release if not already tagged and not executing in the dryrun mode. + - name: Create release tag + if: success() && steps.prepare.outputs.create_tag && steps.prepare.outputs.publish + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.prepare.outputs.version }} + release_name: Firebase Admin Python SDK ${{ steps.prepare.outputs.version }} + draft: false + prerelease: false + + # Checkout the tag if not executing in the dryrun mode, or if executing in the + # dryrun mode with a pre-existing tag. If executed, sources will be made available + # in a new deploy directory. + - name: Checkout source for deployment + uses: actions/checkout@v2 + if: steps.prepare.outputs.publish || steps.prepare.outputs.reuse_tag + with: + ref: ${{ steps.prepare.outputs.version }} + path: deploy + + # Package release artifacts. When not executing in the dryrun mode, always package + # from the deploy directory. When executing in the dryrun mode with a without + # a release tag, package from the staging directory. + - name: Package release artifacts + working-directory: ${{ steps.prepare.outputs.directory }} + run: | + echo Packaging release artifacts + mkdir -p dist + echo Test release > dist/output.txt + + # Attach the packaged artifacts to the workflow output. These can be manually + # downloaded for later inspection if necessary. + - name: Archive artifacts + uses: actions/upload-artifact@v1 + with: + name: dist + path: ${{ steps.prepare.outputs.directory }}/dist + + # If not executing in the dryrun mode, publish the packagted artifacts to Pypi. + - name: Publish to Pypi + if: success() && steps.prepare.outputs.publish + working-directory: deploy + run: echo Publishing to Pypi + + # If not executing in the dryrun mode, and the Tweet is not suppressed, post to + # Twitter. + - name: Post to Twitter + if: success() && steps.prepare.outputs.tweet + run: echo Posting Tweet From 3eb9a4842580deb39f4d94a40dfe64848296e687 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 31 Jan 2020 13:24:28 -0800 Subject: [PATCH 2/3] Release note generation and more improvements --- .github/scripts/generate_changelog.sh | 60 +++++++++++++++ .github/scripts/prepare_release.sh | 105 ++++++++++++++++---------- .github/workflows/release.yml | 57 +++++--------- 3 files changed, 146 insertions(+), 76 deletions(-) create mode 100755 .github/scripts/generate_changelog.sh diff --git a/.github/scripts/generate_changelog.sh b/.github/scripts/generate_changelog.sh new file mode 100755 index 000000000..f0e5d1cf9 --- /dev/null +++ b/.github/scripts/generate_changelog.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +set -e +set -u + +function printChangelog() { + local TITLE=$1 + shift + local ENTRIES=("$@") + if [ ${#ENTRIES[@]} -ne 0 ]; then + echo "### ${TITLE}" + echo "" + for ((i = 0; i < ${#ENTRIES[@]}; i++)) + do + echo "* ${ENTRIES[$i]}" + done + echo "" + fi +} + +if [[ -z "${GITHUB_SHA}" ]]; then + GITHUB_SHA="HEAD" +fi + +LAST_TAG=`git describe --tags $(git rev-list --tags --max-count=1) 2> /dev/null` || true +if [[ -z "${LAST_TAG}" ]]; then + echo "[info] No tags found. Including all commits up to ${GITHUB_SHA}." + VERSION_RANGE="${GITHUB_SHA}" +else + echo "[info] Last release tag: ${LAST_TAG}." + COMMIT_SHA=`git show-ref -s ${LAST_TAG}` + echo "[info] Last release commit: ${COMMIT_SHA}." + VERSION_RANGE="${COMMIT_SHA}..${GITHUB_SHA}" + echo "[info] Including all commits in the range ${VERSION_RANGE}." +fi + +echo "" +CHANGES=() +FIXES=() +FEATS=() +MISC=() + +while read -r line +do + COMMIT_MSG=`echo ${line} | cut -d ' ' -f 2-` + if [[ $COMMIT_MSG =~ ^change(\(.*\))?: ]]; then + CHANGES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^fix(\(.*\))?: ]]; then + FIXES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^feat(\(.*\))?: ]]; then + FEATS+=("$COMMIT_MSG") + else + MISC+=("${COMMIT_MSG}") + fi +done < <(git log ${VERSION_RANGE} --oneline) + +printChangelog "Breaking Changes" "${CHANGES[@]}" +printChangelog "New Features" "${FEATS[@]}" +printChangelog "Bug Fixes" "${FIXES[@]}" +printChangelog "Miscellaneous" "${MISC[@]}" diff --git a/.github/scripts/prepare_release.sh b/.github/scripts/prepare_release.sh index f75cf0d26..1069d4a49 100755 --- a/.github/scripts/prepare_release.sh +++ b/.github/scripts/prepare_release.sh @@ -4,32 +4,70 @@ # 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3). # 2. publish: Set when not executing in the dryrun mode. -# 3. tweet: Set when the release should be posted to Twitter. Also implies +# 3. tweet: Set when the release should be posted to Twitter. Only set when # publish=true. -# 4. create_tag: Set when the release is not already tagged. -# 5. reuse_tag: Set when the release is already tagged. -# 6. directory: Directory where the release artifacts will be built. Either -# 'staging' or 'deploy'. +# 4. changelog: Formatted changelog text for this release. #################################################################################### -echo "[release:retry]: ${RETRY_RELEASE}" +set -e +set -u + echo "[release:dryrun]: ${DRYRUN_RELEASE}" echo "[release:skip-tweet]: ${SKIP_TWEET}" -echo +echo "" + +# Find release version. +RELEASE_VERSION=`python -c "exec(open('firebase_admin/__about__.py').read()); print(__version__)"` || true +if [[ -z "${RELEASE_VERSION}" ]]; then + echo "Failed to extract release version from firebase_admin/__about__.py. Exiting." + exit 1 +fi -# Find current version. -RELEASE_VERSION=`python -c "exec(open('firebase_admin/__about__.py').read()); print(__version__)"` -echo "Releasing version ${RELEASE_VERSION}" +if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then + echo "Malformed release version string: ${RELEASE_VERSION}. Exiting." + exit 1 +fi + +echo "Extracted release version: ${RELEASE_VERSION}" echo "::set-output name=version::v${RELEASE_VERSION}" +# Ensure the release version does not already exist in Pypi. +PYPI_URL="https://pypi.org/pypi/firebase-admin/${RELEASE_VERSION}/json" +PYPI_STATUS=`curl -s -o /dev/null -L -w "%{http_code}" ${PYPI_URL}` +if [[ $PYPI_STATUS -eq 404 ]]; then + echo "Release version not found in Pypi." +elif [[ $PYPI_STATUS -eq 200 ]]; then + echo "Release version already present in Pypi. Exiting." + exit 1 +else + echo "Unexpected ${PYPI_STATUS} response from Pypi. Exiting." + exit 1 +fi + +# Fetch all tags. +git fetch --depth=1 origin +refs/tags/*:refs/tags/* + +# Check if this release is already tagged. +EXISTING_TAG=`git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}"` || true +if [[ -n "${EXISTING_TAG}" ]]; then + RELEASE_URL="https://github.com/firebase/firebase-admin-python/releases/tag/v${RELEASE_VERSION}" + echo "Tag v${RELEASE_VERSION} already exists. Exiting." + echo "If the tag was created in a previous unsuccessful attempt, delete it and try again." + echo "Delete any corresponding releases at ${RELEASE_URL}." + echo " $ git tag -d v${RELEASE_VERSION}" + echo " $ git push --delete origin v${RELEASE_VERSION}" + exit 1 +fi + +echo "Tag v${RELEASE_VERSION} does not exist." + # Handle dryrun mode. if [[ "$DRYRUN_RELEASE" == "true" ]]; then echo "Dryrun mode has been requested. No new tags or artifacts will be published." - DIRECTORY="staging" else - echo "Dryrun mode has not been requested. Executing in the publish mode." - DIRECTORY="deploy" + echo "Dryrun mode has NOT been requested." + echo "A new tag will be created, and release artifacts posted to Pypi." echo "::set-output name=publish::true" if [[ "${SKIP_TWEET}" != "true" ]]; then @@ -40,31 +78,20 @@ else fi fi -# Fetch all tags. -git fetch --depth=1 origin +refs/tags/*:refs/tags/* - -# Check if this release is already tagged. -git describe --tags v${RELEASE_VERSION} 2> /dev/null - -if [[ $? -eq 0 ]]; then - echo "Tag v${RELEASE_VERSION} already exists." - - if [[ "${RETRY_RELEASE}" != "true" ]]; then - echo "Retry mode has not been requested. Exiting." - echo "Label your PR with [release: retry] to build a release from an existing tag." - exit 1 - fi - - echo "Retry mode has been requested. Releasing from the existing tag." - echo "::set-output name=reuse_tag::true" +# Fetch history of the master branch. +git fetch origin master --prune --unshallow - # When a tag already exists, we will use it to build artifacts even when - # the dryrun mode is requested. - DIRECTORY="deploy" -else - echo "Tag v${RELEASE_VERSION} does not exist." - echo "::set-output name=create_tag::true" -fi +# Generate changelog from commit history. +echo "Generating changelog from history." +echo "" +CURRENT_DIR=$(dirname "$0") +CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` +echo "$CHANGELOG" -echo "Release artifacts will be built in the ${DIRECTORY} directory." -echo "::set-output name=directory::${DIRECTORY}" +# Parse and preformat the text to handle multi-line output. +# See https://github.community/t5/GitHub-Actions/set-output-Truncates-Multiline-Strings/td-p/37870 +FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[info\\]"` +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//'%'/'%25'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\n'/'%0A'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\r'/'%0D'}" +echo "::set-output name=changelog::${FILTERED_CHANGELOG}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b9cedce0..91b183d77 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,8 +23,6 @@ jobs: steps: - name: Checkout source for staging uses: actions/checkout@v2 - with: - path: staging - name: Set up Python uses: actions/setup-python@v1 @@ -32,61 +30,32 @@ jobs: python-version: 3.6 # Handle additional options/labels passed with the release PR. - # 1. release:retry - Continue the release from an already existing tag. - # 2. release:dryrun - Dry run the release process without publishing new tags or artifacts. - # 3. release:skip-tweet - Complete the release process without posting to Twitter. + # 1. release:dryrun - Dry run the release process without publishing new tags or artifacts. + # 2. release:skip-tweet - Complete the release process without posting to Twitter. - name: Prepare release id: prepare run: ./.github/scripts/prepare_release.sh - working-directory: staging env: - RETRY_RELEASE: ${{ contains(github.event.pull_request.labels.*.name, 'release:retry') }} DRYRUN_RELEASE: ${{ contains(github.event.pull_request.labels.*.name, 'release:dryrun') }} SKIP_TWEET: ${{ contains(github.event.pull_request.labels.*.name, 'release:skip-tweet') }} # Dependencies are needed to both run the tests and package the release. # Therefore we run this step unconditionally. - name: Install dependencies - working-directory: staging run: | python -m pip install --upgrade pip pip install -r requirements.txt # Only run if the release is not already tagged. - name: Run tests - if: success() && steps.prepare.outputs.create_tag - working-directory: staging run: | pytest echo "Running integration tests" - # Tag the release if not already tagged and not executing in the dryrun mode. - - name: Create release tag - if: success() && steps.prepare.outputs.create_tag && steps.prepare.outputs.publish - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.prepare.outputs.version }} - release_name: Firebase Admin Python SDK ${{ steps.prepare.outputs.version }} - draft: false - prerelease: false - - # Checkout the tag if not executing in the dryrun mode, or if executing in the - # dryrun mode with a pre-existing tag. If executed, sources will be made available - # in a new deploy directory. - - name: Checkout source for deployment - uses: actions/checkout@v2 - if: steps.prepare.outputs.publish || steps.prepare.outputs.reuse_tag - with: - ref: ${{ steps.prepare.outputs.version }} - path: deploy - # Package release artifacts. When not executing in the dryrun mode, always package # from the deploy directory. When executing in the dryrun mode with a without # a release tag, package from the staging directory. - name: Package release artifacts - working-directory: ${{ steps.prepare.outputs.directory }} run: | echo Packaging release artifacts mkdir -p dist @@ -98,16 +67,30 @@ jobs: uses: actions/upload-artifact@v1 with: name: dist - path: ${{ steps.prepare.outputs.directory }}/dist + path: dist + + # Tag the release if not executing in the dryrun mode. We pull this action froma + # custom fork of a contributor until https://github.com/actions/create-release/pull/32 + # is merged. Also note that v1 of this action does not support the "body" parameter. + - name: Create release tag + if: success() && steps.prepare.outputs.publish + uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.prepare.outputs.version }} + release_name: Firebase Admin Python SDK ${{ steps.prepare.outputs.version }} + body: ${{ steps.prepare.outputs.changelog }} + draft: false + prerelease: false # If not executing in the dryrun mode, publish the packagted artifacts to Pypi. - name: Publish to Pypi if: success() && steps.prepare.outputs.publish - working-directory: deploy run: echo Publishing to Pypi - # If not executing in the dryrun mode, and the Tweet is not suppressed, post to - # Twitter. + # If the Tweet is not suppressed, post to Twitter. - name: Post to Twitter if: success() && steps.prepare.outputs.tweet run: echo Posting Tweet + continue-on-error: true From 5ae70f0f81e5a2d14c22f1cbc2f657a3934dde6a Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 3 Feb 2020 11:50:40 -0800 Subject: [PATCH 3/3] Simplified release process --- .github/scripts/generate_changelog.sh | 23 ++-- .github/scripts/prepare_release.sh | 97 --------------- .github/scripts/publish_preflight_check.sh | 134 +++++++++++++++++++++ .github/workflows/release.yml | 90 ++++++++------ 4 files changed, 199 insertions(+), 145 deletions(-) delete mode 100755 .github/scripts/prepare_release.sh create mode 100755 .github/scripts/publish_preflight_check.sh diff --git a/.github/scripts/generate_changelog.sh b/.github/scripts/generate_changelog.sh index f0e5d1cf9..3c97dca0c 100755 --- a/.github/scripts/generate_changelog.sh +++ b/.github/scripts/generate_changelog.sh @@ -6,7 +6,8 @@ set -u function printChangelog() { local TITLE=$1 shift - local ENTRIES=("$@") + # Skip the sentinel value. + local ENTRIES=("${@:2}") if [ ${#ENTRIES[@]} -ne 0 ]; then echo "### ${TITLE}" echo "" @@ -24,21 +25,25 @@ fi LAST_TAG=`git describe --tags $(git rev-list --tags --max-count=1) 2> /dev/null` || true if [[ -z "${LAST_TAG}" ]]; then - echo "[info] No tags found. Including all commits up to ${GITHUB_SHA}." + echo "[INFO] No tags found. Including all commits up to ${GITHUB_SHA}." VERSION_RANGE="${GITHUB_SHA}" else - echo "[info] Last release tag: ${LAST_TAG}." + echo "[INFO] Last release tag: ${LAST_TAG}." COMMIT_SHA=`git show-ref -s ${LAST_TAG}` - echo "[info] Last release commit: ${COMMIT_SHA}." + echo "[INFO] Last release commit: ${COMMIT_SHA}." VERSION_RANGE="${COMMIT_SHA}..${GITHUB_SHA}" - echo "[info] Including all commits in the range ${VERSION_RANGE}." + echo "[INFO] Including all commits in the range ${VERSION_RANGE}." fi echo "" -CHANGES=() -FIXES=() -FEATS=() -MISC=() + +# Older versions of Bash (< 4.4) treat empty arrays as unbound variables, which triggers +# errors when referencing them. Therefore we initialize each of these arrays with an empty +# sentinel value, and later skip them. +CHANGES=("") +FIXES=("") +FEATS=("") +MISC=("") while read -r line do diff --git a/.github/scripts/prepare_release.sh b/.github/scripts/prepare_release.sh deleted file mode 100755 index 1069d4a49..000000000 --- a/.github/scripts/prepare_release.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash - -###################################### Outputs ##################################### - -# 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3). -# 2. publish: Set when not executing in the dryrun mode. -# 3. tweet: Set when the release should be posted to Twitter. Only set when -# publish=true. -# 4. changelog: Formatted changelog text for this release. - -#################################################################################### - -set -e -set -u - -echo "[release:dryrun]: ${DRYRUN_RELEASE}" -echo "[release:skip-tweet]: ${SKIP_TWEET}" -echo "" - -# Find release version. -RELEASE_VERSION=`python -c "exec(open('firebase_admin/__about__.py').read()); print(__version__)"` || true -if [[ -z "${RELEASE_VERSION}" ]]; then - echo "Failed to extract release version from firebase_admin/__about__.py. Exiting." - exit 1 -fi - -if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then - echo "Malformed release version string: ${RELEASE_VERSION}. Exiting." - exit 1 -fi - -echo "Extracted release version: ${RELEASE_VERSION}" -echo "::set-output name=version::v${RELEASE_VERSION}" - -# Ensure the release version does not already exist in Pypi. -PYPI_URL="https://pypi.org/pypi/firebase-admin/${RELEASE_VERSION}/json" -PYPI_STATUS=`curl -s -o /dev/null -L -w "%{http_code}" ${PYPI_URL}` -if [[ $PYPI_STATUS -eq 404 ]]; then - echo "Release version not found in Pypi." -elif [[ $PYPI_STATUS -eq 200 ]]; then - echo "Release version already present in Pypi. Exiting." - exit 1 -else - echo "Unexpected ${PYPI_STATUS} response from Pypi. Exiting." - exit 1 -fi - -# Fetch all tags. -git fetch --depth=1 origin +refs/tags/*:refs/tags/* - -# Check if this release is already tagged. -EXISTING_TAG=`git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}"` || true -if [[ -n "${EXISTING_TAG}" ]]; then - RELEASE_URL="https://github.com/firebase/firebase-admin-python/releases/tag/v${RELEASE_VERSION}" - echo "Tag v${RELEASE_VERSION} already exists. Exiting." - echo "If the tag was created in a previous unsuccessful attempt, delete it and try again." - echo "Delete any corresponding releases at ${RELEASE_URL}." - echo " $ git tag -d v${RELEASE_VERSION}" - echo " $ git push --delete origin v${RELEASE_VERSION}" - exit 1 -fi - -echo "Tag v${RELEASE_VERSION} does not exist." - -# Handle dryrun mode. -if [[ "$DRYRUN_RELEASE" == "true" ]]; then - echo "Dryrun mode has been requested. No new tags or artifacts will be published." -else - echo "Dryrun mode has NOT been requested." - echo "A new tag will be created, and release artifacts posted to Pypi." - echo "::set-output name=publish::true" - - if [[ "${SKIP_TWEET}" != "true" ]]; then - echo "Release will be posted to Twitter upon successful completion." - echo "::set-output name=tweet::true" - else - echo "Skip Tweet mode has been requested. Release will not be posted to Twitter." - fi -fi - -# Fetch history of the master branch. -git fetch origin master --prune --unshallow - -# Generate changelog from commit history. -echo "Generating changelog from history." -echo "" -CURRENT_DIR=$(dirname "$0") -CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` -echo "$CHANGELOG" - -# Parse and preformat the text to handle multi-line output. -# See https://github.community/t5/GitHub-Actions/set-output-Truncates-Multiline-Strings/td-p/37870 -FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[info\\]"` -FILTERED_CHANGELOG="${FILTERED_CHANGELOG//'%'/'%25'}" -FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\n'/'%0A'}" -FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\r'/'%0D'}" -echo "::set-output name=changelog::${FILTERED_CHANGELOG}" diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh new file mode 100755 index 000000000..38b0be20c --- /dev/null +++ b/.github/scripts/publish_preflight_check.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +###################################### Outputs ##################################### + +# 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3). +# 2. changelog: Formatted changelog text for this release. + +#################################################################################### + +set -e +set -u + +function echo_info() { + local MESSAGE=$1 + echo "[INFO] ${MESSAGE}" +} + +function echo_warn() { + local MESSAGE=$1 + echo "[WARN] ${MESSAGE}" +} + +function terminate() { + echo "" + echo_warn "--------------------------------------------" + echo_warn "PREFLIGHT FAILED" + echo_warn "--------------------------------------------" + exit 1 +} + + +echo_info "Starting publish preflight check..." +echo_info "Git revision : ${GITHUB_SHA}" +echo_info "Workflow triggered by : ${GITHUB_ACTOR}" +echo_info "GitHub event : ${GITHUB_EVENT_NAME}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Extracting release version" +echo_info "--------------------------------------------" +echo_info "" + +readonly ABOUT_FILE="firebase_admin/__about__.py" +echo_info "Loading version from: ${ABOUT_FILE}" + +readonly VERSION_SCRIPT="exec(open('${ABOUT_FILE}').read()); print(__version__)" +readonly RELEASE_VERSION=`python -c "${VERSION_SCRIPT}"` || true +if [[ -z "${RELEASE_VERSION}" ]]; then + echo_warn "Failed to extract release version from: ${ABOUT_FILE}" + terminate +fi + +if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then + echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting." + terminate +fi + +echo_info "Extracted release version: ${RELEASE_VERSION}" +echo "::set-output name=version::v${RELEASE_VERSION}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking previous releases" +echo_info "--------------------------------------------" +echo_info "" + +readonly PYPI_URL="https://pypi.org/pypi/firebase-admin/${RELEASE_VERSION}/json" +readonly PYPI_STATUS=`curl -s -o /dev/null -L -w "%{http_code}" ${PYPI_URL}` +if [[ $PYPI_STATUS -eq 404 ]]; then + echo_info "Release version ${RELEASE_VERSION} not found in Pypi." +elif [[ $PYPI_STATUS -eq 200 ]]; then + echo_warn "Release version ${RELEASE_VERSION} already present in Pypi." + terminate +else + echo_warn "Unexpected ${PYPI_STATUS} response from Pypi. Exiting." + terminate +fi + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking release tag" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch --depth=1 origin +refs/tags/*:refs/tags/* >---" +git fetch --depth=1 origin +refs/tags/*:refs/tags/* +echo "" + +readonly EXISTING_TAG=`git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}"` || true +if [[ -n "${EXISTING_TAG}" ]]; then + echo_warn "Tag v${RELEASE_VERSION} already exists. Exiting." + echo_warn "If the tag was created in a previous unsuccessful attempt, delete it and try again." + echo_warn " $ git tag -d v${RELEASE_VERSION}" + echo_warn " $ git push --delete origin v${RELEASE_VERSION}" + + readonly RELEASE_URL="https://github.com/firebase/firebase-admin-python/releases/tag/v${RELEASE_VERSION}" + echo_warn "Delete any corresponding releases at ${RELEASE_URL}." + terminate +fi + +echo_info "Tag v${RELEASE_VERSION} does not exist." + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Generating changelog" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch origin master --prune --unshallow >---" +git fetch origin master --prune --unshallow +echo "" + +echo_info "Generating changelog from history..." +readonly CURRENT_DIR=$(dirname "$0") +readonly CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` +echo "$CHANGELOG" + +# Parse and preformat the text to handle multi-line output. +# See https://github.community/t5/GitHub-Actions/set-output-Truncates-Multiline-Strings/td-p/37870 +FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[INFO\\]"` +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//'%'/'%25'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\n'/'%0A'}" +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\r'/'%0D'}" +echo "::set-output name=changelog::${FILTERED_CHANGELOG}" + + +echo "" +echo_info "--------------------------------------------" +echo_info "PREFLIGHT SUCCESSFUL" +echo_info "--------------------------------------------" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91b183d77..670a5cab4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,65 +1,64 @@ +# Copyright 2020 Google Inc. +# +# 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. + name: Release on: + # Only run the workflow when a PR is closed, or when a developer explicitly requests + # a build by sending a 'firebase_build' event. pull_request: - branches: - - master types: - closed + repository_dispatch: + types: + - firebase_build + jobs: - publish_release: - # Only run the workflow when a pull request has been: - # 1. merged to the master branch - # 2. baring the label 'release:pending', and - # 3. the title prefix '[chore] Release'. - if: github.event.pull_request.merged && - contains(github.event.pull_request.labels.*.name, 'release:pending') && - startsWith(github.event.pull_request.title, '[chore] Release') + stage_release: + # If triggered by a PR it must be merged and contain the label 'release:build'. + if: github.event.action == 'firebase_build' || + (github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'release:build')) runs-on: ubuntu-latest - # Checkout source to the staging directory. + # When manually triggering the build, the requester can specify a target branch or a tag + # via the 'ref' client parameter. steps: - name: Checkout source for staging uses: actions/checkout@v2 + with: + ref: ${{ github.event.client_payload.ref || github.ref }} - name: Set up Python uses: actions/setup-python@v1 with: python-version: 3.6 - # Handle additional options/labels passed with the release PR. - # 1. release:dryrun - Dry run the release process without publishing new tags or artifacts. - # 2. release:skip-tweet - Complete the release process without posting to Twitter. - - name: Prepare release - id: prepare - run: ./.github/scripts/prepare_release.sh - env: - DRYRUN_RELEASE: ${{ contains(github.event.pull_request.labels.*.name, 'release:dryrun') }} - SKIP_TWEET: ${{ contains(github.event.pull_request.labels.*.name, 'release:skip-tweet') }} - - # Dependencies are needed to both run the tests and package the release. - # Therefore we run this step unconditionally. - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - # Only run if the release is not already tagged. - name: Run tests run: | pytest echo "Running integration tests" - # Package release artifacts. When not executing in the dryrun mode, always package - # from the deploy directory. When executing in the dryrun mode with a without - # a release tag, package from the staging directory. - name: Package release artifacts - run: | - echo Packaging release artifacts - mkdir -p dist - echo Test release > dist/output.txt + run: python setup.py bdist_wheel bdist_egg # Attach the packaged artifacts to the workflow output. These can be manually # downloaded for later inspection if necessary. @@ -69,28 +68,41 @@ jobs: name: dist path: dist + # Check whether the release should be published. We publish only when the trigger PR is + # 1. merged + # 2. to the master branch + # 3. with the title prefix '[chore] Release '. + - name: Publish preflight check + if: success() && github.event.pull_request.merged && + github.ref == 'master' && + startsWith(github.event.pull_request.title, '[chore] Release ') + id: preflight + run: | + ./.github/scripts/publish_preflight_check.sh + echo ::set-env name=FIREBASE_PUBLISH::true + # Tag the release if not executing in the dryrun mode. We pull this action froma # custom fork of a contributor until https://github.com/actions/create-release/pull/32 # is merged. Also note that v1 of this action does not support the "body" parameter. - name: Create release tag - if: success() && steps.prepare.outputs.publish + if: success() && env.FIREBASE_PUBLISH uses: fleskesvor/create-release@1a72e235c178bf2ae6c51a8ae36febc24568c5fe env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: ${{ steps.prepare.outputs.version }} - release_name: Firebase Admin Python SDK ${{ steps.prepare.outputs.version }} - body: ${{ steps.prepare.outputs.changelog }} + tag_name: ${{ steps.preflight.outputs.version }} + release_name: Firebase Admin Python SDK ${{ steps.preflight.outputs.version }} + body: ${{ steps.preflight.outputs.changelog }} draft: false prerelease: false - # If not executing in the dryrun mode, publish the packagted artifacts to Pypi. - name: Publish to Pypi - if: success() && steps.prepare.outputs.publish + if: success() && env.FIREBASE_PUBLISH run: echo Publishing to Pypi - # If the Tweet is not suppressed, post to Twitter. + # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. - name: Post to Twitter - if: success() && steps.prepare.outputs.tweet + if: success() && env.FIREBASE_PUBLISH && + contains(github.event.pull_request.labels.*.name, 'release:tweet') run: echo Posting Tweet continue-on-error: true