Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add build and release workflows #59

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Build

env:
POETRY_VERSION: "1.6.1"
PYTHON_VERSION: "3.12"

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
40 changes: 40 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -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.12
uses: actions/setup-python@v4
with:
python-version: "3.12"

- 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
103 changes: 103 additions & 0 deletions scripts/version
Original file line number Diff line number Diff line change
@@ -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'.")
Loading