diff --git a/.github/scripts/generate_changelog.sh b/.github/scripts/generate_changelog.sh new file mode 100755 index 0000000..c4c77f6 --- /dev/null +++ b/.github/scripts/generate_changelog.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# Copyright 2023 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. + +set -e +set -u + +function printChangelog() { + local TITLE=$1 + shift + # Skip the sentinel value. + local ENTRIES=("${@:2}") + 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 "" + +# 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 + 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 =~ ^refactor(\(.*\))?: ]]; 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[@]}" \ No newline at end of file diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh new file mode 100755 index 0000000..e467fab --- /dev/null +++ b/.github/scripts/publish_preflight_check.sh @@ -0,0 +1,179 @@ +#!/bin/bash + +# Copyright 2023 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. + + +###################################### 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 INIT_FILE="src/firebase_functions/__init__.py" +echo_info "Loading version from: ${INIT_FILE}" + +readonly RELEASE_VERSION=`grep "__version__" ${INIT_FILE} | awk '{print $3}' | tr -d \"` || true +if [[ -z "${RELEASE_VERSION}" ]]; then + echo_warn "Failed to extract release version from: ${INIT_FILE}" + terminate +fi + +if [[ ! "${RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9]+)?$ ]]; then + echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting." + terminate +fi + +echo_info "Extracted release version: ${RELEASE_VERSION}" +echo "version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Check release artifacts" +echo_info "--------------------------------------------" +echo_info "" + +if [[ ! -d dist ]]; then + echo_warn "dist directory does not exist." + terminate +fi + +readonly BIN_DIST="dist/firebase_functions-${RELEASE_VERSION}-py3-none-any.whl" +if [[ -f "${BIN_DIST}" ]]; then + echo_info "Found binary distribution (bdist_wheel): ${BIN_DIST}" +else + echo_warn "Binary distribution ${BIN_DIST} not found." + terminate +fi + +readonly SRC_DIST="dist/firebase_functions-${RELEASE_VERSION}.tar.gz" +if [[ -f "${SRC_DIST}" ]]; then + echo_info "Found source distribution (sdist): ${SRC_DIST}" +else + echo_warn "Source distribution ${SRC_DIST} not found." + terminate +fi + +readonly ARTIFACT_COUNT=`ls dist/ | wc -l` +if [[ $ARTIFACT_COUNT -ne 2 ]]; then + echo_warn "Unexpected artifacts in the distribution directory." + ls -l dist + terminate +fi + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking previous releases" +echo_info "--------------------------------------------" +echo_info "" + +readonly PYPI_URL="https://pypi.org/pypi/firebase-functions/${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 "" + +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-functions-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 main >---" +git fetch origin main +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. +https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#example-of-a-multiline-string +FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[INFO\\]"` +EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) +echo "changelog=<<$EOF" >> "$GITHUB_OUTPUT" +echo $CHANGELOG >> "$GITHUB_OUTPUT" +echo $EOF >> "$GITHUG_OUTPUT" + + +echo "" +echo_info "--------------------------------------------" +echo_info "PREFLIGHT SUCCESSFUL" +echo_info "--------------------------------------------" \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..ec52733 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,113 @@ +# Copyright 2023 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 Candidate + +on: + pull_request: + types: [opened, synchronize, closed] + # Allow workflow to be triggered manually. + workflow_dispatch: + +jobs: + stage_release: + # To publish a release, merge the release PR with the label 'release:publish'. + # To stage a release without publishing it, manually invoke the workflow. + # . or apply the 'release:stage' label to a PR. + if: (github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'release:publish')) || + github.event.workflow_dispatch || + contains(github.event.pull_request.labels.*.name, 'release:stage') + + runs-on: ubuntu-latest + + steps: + - name: Checkout source for staging + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + pip install --upgrade pip + python -m pip install -e ".[dev]" + + - name: Test with pytest & coverage + run: | + python -m pytest --cov=src --cov-report term --cov-report html --cov-report xml -vv + + # Build the Python Wheel and the source distribution. + - name: Package release artifacts + run: | + python -m pip install setuptools wheel + python setup.py bdist_wheel sdist + + # 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@v3 + with: + name: dist + path: dist/ + + publish_release: + needs: stage_release + + # 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 label 'release:publish', and + # 4. the title prefix '[chore] Release '. + if: github.event.pull_request.merged && + github.ref == 'master' && + contains(github.event.pull_request.labels.*.name, 'release:publish') && + startsWith(github.event.pull_request.title, '[chore] Release ') + + runs-on: ubuntu-latest + + steps: + - name: Checkout source for publish + uses: actions/checkout@v3 + + # Download the artifacts created by the stage_release job. + - name: Download release candidates + uses: actions/download-artifact@v3 + with: + name: dist + path: dist + + - name: Publish preflight check + id: preflight + run: ./.github/scripts/publish_preflight_check.sh + + - name: Create release tag + # Skip creating a release tag for prereleases + if: (!contains(github.event.pull_request.labels.*.name, 'release:prerelease')) + uses: elgohr/Github-Release-Action@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.preflight.outputs.version }} + release_name: Firebase Functions Python SDK ${{ steps.preflight.outputs.version }} + body: ${{ steps.preflight.outputs.changelog }} + draft: false + prerelease: false + + - name: Publish to Pypi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: firebase + password: ${{ secrets.PYPI_PASSWORD }} \ No newline at end of file diff --git a/setup.py b/setup.py index b5033f8..f1fc72a 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ """ Setup for Firebase Functions Python. """ +from os import path from setuptools import find_packages, setup install_requires = [ @@ -30,19 +31,37 @@ 'google-cloud-tasks>=2.13.1' ] +# Read in the package metadata per recommendations from: +# https://packaging.python.org/guides/single-sourcing-package-version/ +init_path = path.join(path.dirname(path.abspath(__file__)), 'src', + 'firebase_functions', '__init__.py') +version = {} +with open(init_path) as fp: + exec(fp.read(), version) # pylint: disable=exec-used + +long_description = ( + 'The Firebase Functions Python SDK provides an SDK for defining' + ' Cloud Functions for Firebase.') + setup( name='firebase_functions', - version='0.0.1', + version=version['__version__'], description='Firebase Functions Python SDK', + long_description=long_description, + url='https://github.com/firebase/firebase-functions-python', + author='Firebase Team', + keywords=['firebase', 'functions', 'google', 'cloud'], + license='Apache License 2.0', install_requires=install_requires, extras_require={'dev': dev_requires}, packages=find_packages(where='src'), package_dir={'': 'src'}, python_requires='>=3.10', classifiers=[ - 'Development Status :: 1 - Planning', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', ], ) diff --git a/src/firebase_functions/__init__.py b/src/firebase_functions/__init__.py index c598e1a..1a4c656 100644 --- a/src/firebase_functions/__init__.py +++ b/src/firebase_functions/__init__.py @@ -14,3 +14,5 @@ """ Firebase Functions for Python. """ + +__version__ = "0.0.1rc0"