From 23f0a712444ca88404f3b7f29ebcac5007e6ad74 Mon Sep 17 00:00:00 2001 From: Zanie Date: Thu, 4 Jan 2024 13:24:46 -0600 Subject: [PATCH] Add build and release workflows --- .github/workflows/build.yaml | 102 ++++++++++++++++++++++++++++++++ .github/workflows/release.yaml | 40 +++++++++++++ scripts/version | 103 +++++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/release.yaml create mode 100755 scripts/version diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..aab632b7 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,102 @@ +name: Build + +env: + POETRY_VERSION: "1.6.1" + PYTHON_VERSION: "3.11" + +on: + push: + branches: + - main + pull_request: + types: + - opened + - reopened + - synchronize + - labeled + branches: + - main + +jobs: + build-and-publish: + name: Publish test release + runs-on: ubuntu-latest + outputs: + build-version: ${{ steps.build.outputs.version }} + + if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'test-build') + + steps: + - uses: actions/checkout@v3 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v4 + with: + python-version: "${{ env.PYTHON_VERSION }}" + + - name: Set up Poetry + run: | + pip install poetry==${{ env.POETRY_VERSION }} + + - name: Publish to Test PyPI + id: build + env: + POETRY_PYPI_TOKEN_TEST_PYPI: ${{ secrets.TEST_PYPI_API_TOKEN }} + run: | + version=$(./scripts/version dev) + echo "version=$version" >> $GITHUB_OUTPUT + poetry version $version + poetry config repositories.test-pypi https://test.pypi.org/legacy/ + poetry publish --build -r test-pypi + + test-install: + # We test the install on a clean machine to avoid poetry behavior attempting to + # install the project root when it is checked out + name: Test install + runs-on: ubuntu-latest + needs: build-and-publish + timeout-minutes: 5 + + steps: + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v4 + with: + python-version: "${{ env.PYTHON_VERSION }}" + + - name: Set up Poetry + run: | + pip install poetry==${{ env.POETRY_VERSION }} + poetry init --name 'test-project' --no-interaction + poetry source add test-pypi https://test.pypi.org/simple/ --priority=explicit + + - name: Wait for package to be available + run: > + until + curl --silent "https://test.pypi.org/simple/packse/" + | grep --quiet "${{ needs.build-and-publish.outputs.build-version }}"; + do sleep 10; + done + && + sleep 60 + # We sleep for an additional 60 seconds as it seems to take a bit longer for + # the package to be consistently available + + # Note: The above will not sleep forever due to the job level timeout + + - name: Install release from Test PyPI + run: > + poetry add + --source test-pypi + packse==${{ needs.build-and-publish.outputs.build-version }} + + - name: Check release version + run: | + installed=$(poetry run python -c "import pkg_resources; print(pkg_resources.get_distribution('packse').version)") + test $installed = ${{ needs.build-and-publish.outputs.build-version }} + + - name: Check CLI help + run: | + poetry run -- packse --help diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..16aeb8d5 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,40 @@ +name: Release + +env: + POETRY_VERSION: "1.6.1" + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+rc[0-9]+" + - "[0-9]+.[0-9]+[ab][0-9]+" + +jobs: + release: + name: Release to PyPI + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Set up Poetry + run: | + pip install poetry==${{ env.POETRY_VERSION }} + + # Note: If build and publish steps are ever separated, the version must + # be set before building + - name: Publish package + env: + POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }} + run: | + poetry version "${GITHUB_REF#refs/*/}" + poetry publish --build diff --git a/scripts/version b/scripts/version new file mode 100755 index 00000000..aa156814 --- /dev/null +++ b/scripts/version @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""" +Simple utility for generating version strings based on the information in the git index. + +Supports three modes: parts, local, and dev. + +parts + Display all available version parts. + Format: {timestamp: str, short_hash: str, closest_tag: str, distance: int} + + $ version parts + {'timestamp': '1663305183', 'short_hash': '440f9b6', 'closest_tag': '0.0.0', 'distance': 50} + +local + Generate a PEP 440 compliant local version tag. + Format: {last_tag}+{num commits}.{commit hash} + + $ version local + 0.0.0+50.440f9b6 + +dev: Generate a PEP 440 compliant development prerelease version tag. + Format: {last_tag + 1}.dev{timestamp} + + $ version dev + 0.0.1.dev1663305183 + +Implementation based on versioneer but is unlikely to be robust to old git versions and +tagging schemes that deviate from PEP 440. +""" +import re +import subprocess +import sys + + +def panic(message): + print(message, file=sys.stderr) + exit(1) + + +def run(command): + return subprocess.check_output(command).decode().strip() + + +def get_version_parts(): + git_describe = run(["git", "describe", "--tags", "--always", "--long"]) + commit_timestamp = run(["git", "show", "-s", "--format=%ct", "HEAD"]) + + parts = {} + + parts["timestamp"] = commit_timestamp + + if "-" in git_describe: + # TAG-NUM-gHEX + match = re.match(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) + if not match: + panic() + + # tag + parts["closest_tag"] = match.group(1) + + # distance: number of commits since tag + parts["distance"] = int(match.group(2)) + + # commit: short hex revision ID + parts["short_hash"] = match.group(3) + + else: + # HEX: no tags + parts["short_hash"] = git_describe + + parts["closest_tag"] = "0.0.0" + + git_rev_list = run(["git", "rev-list", "HEAD", "--left-right"]) + # total number of commits + parts["distance"] = len(git_rev_list.split()) + + return parts + + +if __name__ == "__main__": + if not len(sys.argv) > 1: + panic("Missing mode. Expected one of 'parts', 'local', 'dev'.") + + mode = sys.argv[1].lower() + + parts = get_version_parts() + + if mode == "parts": + print(parts) + + elif mode == "local": + print("{closest_tag}+{distance}.{short_hash}".format(**parts)) + + elif mode == "dev": + # bump patch on closet tag + tag_parts = parts["closest_tag"].split(".") + tag_parts[-1] = str(int(tag_parts[-1]) + 1) + patch_tag = ".".join(tag_parts) + + print("{}.dev{timestamp}".format(patch_tag, **parts)) + + else: + panic(f"Invalid mode {mode!r}. Expected one of 'parts', 'local', 'dev'.")