Skip to content

Commit b00b2e5

Browse files
authored
Set up publishing to PyPI via GitHub Actions (#40)
1 parent 737dfd0 commit b00b2e5

12 files changed

+284
-13
lines changed

.flake8

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ ignore=
1717
D409
1818
D413
1919
per-file-ignores =
20+
.github/*: D
2021
docs/*: D
2122
tests/*: D
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env python3
2+
3+
import re
4+
5+
from utils import REPO_ROOT, get_current_package_version
6+
7+
CHANGELOG_PATH = REPO_ROOT / 'CHANGELOG.md'
8+
9+
# Checks whether the current package version has an entry in the CHANGELOG.md file
10+
if __name__ == '__main__':
11+
current_package_version = get_current_package_version()
12+
13+
if not CHANGELOG_PATH.is_file():
14+
raise RuntimeError('Unable to find CHANGELOG.md file')
15+
16+
with open(CHANGELOG_PATH) as changelog_file:
17+
for line in changelog_file:
18+
# The heading for the changelog entry for the given version can start with either the version number, or the version number in a link
19+
if re.match(fr'\[?{current_package_version}([\] ]|$)', line):
20+
break
21+
else:
22+
raise RuntimeError(f'There is no entry in the changelog for the current package version ({current_package_version})')
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env python3
2+
3+
from utils import get_current_package_version
4+
5+
# Print the current package version from the src/package_name/_version.py file to stdout
6+
if __name__ == '__main__':
7+
print(get_current_package_version(), end='')
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import re
5+
import urllib.request
6+
7+
from utils import PACKAGE_NAME, get_current_package_version, set_current_package_version
8+
9+
# Checks whether the current package version number was not already used in a published release,
10+
# and if not, modifies the package version number in src/package_name/_version.py from a stable release version (X.Y.Z) to a beta version (X.Y.ZbN)
11+
if __name__ == '__main__':
12+
current_version = get_current_package_version()
13+
14+
# We can only transform a stable release version (X.Y.Z) to a beta version (X.Y.ZbN)
15+
if not re.match(r'^\d+\.\d+\.\d+$', current_version):
16+
raise RuntimeError(f'The current version {current_version} does not match the proper semver format for stable releases (X.Y.Z)')
17+
18+
# Load the version numbers of the currently published versions from PyPI
19+
# If the URL returns 404, it means the package has no releases yet (which is okay in our case)
20+
package_info_url = f'https://pypi.org/pypi/{PACKAGE_NAME}/json'
21+
try:
22+
conn = urllib.request.urlopen(package_info_url)
23+
package_data = json.load(urllib.request.urlopen(package_info_url))
24+
published_versions = list(package_data['releases'].keys())
25+
except urllib.error.HTTPError as e:
26+
if e.code != 404:
27+
raise e
28+
published_versions = []
29+
30+
# We don't want to publish a beta version with the same version number as an already released stable version
31+
if current_version in published_versions:
32+
raise RuntimeError(f'The current version {current_version} was already released!')
33+
34+
# Find the highest beta version number that was already published
35+
latest_beta = 0
36+
for version in published_versions:
37+
if version.startswith(f'{current_version}b'):
38+
beta_version = int(version.split('b')[1])
39+
if beta_version > latest_beta:
40+
latest_beta = beta_version
41+
42+
# Write the latest beta version number to src/package_name/_version.py
43+
new_beta_version_number = f'{current_version}b{latest_beta + 1}'
44+
set_current_package_version(new_beta_version_number)

.github/scripts/utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pathlib
2+
3+
PACKAGE_NAME = 'apify_client'
4+
REPO_ROOT = pathlib.Path(__file__).parent.resolve() / '../..'
5+
VERSION_FILE_PATH = REPO_ROOT / f'src/{PACKAGE_NAME}/_version.py'
6+
7+
8+
# Load the current version number from src/package_name/_version.py
9+
# It is on a line in the format __version__ = 1.2.3
10+
def get_current_package_version():
11+
with open(VERSION_FILE_PATH, 'r') as version_file:
12+
for line in version_file:
13+
if line.startswith('__version__'):
14+
delim = '"' if '"' in line else "'"
15+
version = line.split(delim)[1]
16+
return version
17+
else:
18+
raise RuntimeError('Unable to find version string.')
19+
20+
21+
# Write the given version number from src/package_name/_version.py
22+
# It replaces the version number on the line with the format __version__ = 1.2.3
23+
def set_current_package_version(version):
24+
with open(VERSION_FILE_PATH, 'r+') as version_file:
25+
updated_version_file_lines = []
26+
version_string_found = False
27+
for line in version_file:
28+
if line.startswith('__version__'):
29+
version_string_found = True
30+
line = f"__version__ = '{version}'"
31+
updated_version_file_lines.append(line)
32+
33+
if not version_string_found:
34+
raise RuntimeError('Unable to find version string.')
35+
36+
version_file.seek(0)
37+
version_file.write('\n'.join(updated_version_file_lines))
38+
version_file.truncate()

.github/workflows/check_docs.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ name: Check documentation status
33
on: push
44

55
jobs:
6-
build:
6+
check_docs:
77
runs-on: ubuntu-20.04
88

99
steps:
10-
- uses: actions/checkout@v2
10+
- name: Checkout repository
11+
uses: actions/checkout@v2
1112

1213
- name: Set up Python
1314
uses: actions/setup-python@v2

.github/workflows/lint_and_test.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ name: Lint and test
33
on: push
44

55
jobs:
6-
build:
6+
lint_and_test:
77
runs-on: ubuntu-20.04
88
strategy:
99
matrix:
1010
python-version: [3.7, 3.8, 3.9]
1111

1212
steps:
13-
- uses: actions/checkout@v2
13+
- name: Checkout repository
14+
uses: actions/checkout@v2
1415

1516
- name: Set up Python ${{ matrix.python-version }}
1617
uses: actions/setup-python@v2

.github/workflows/release.yml

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
name: Check & Release
2+
3+
on:
4+
# Push to master will publish a beta version
5+
push:
6+
branches:
7+
- master
8+
# A release via GitHub releases will publish a stable version
9+
release:
10+
types: [published]
11+
12+
jobs:
13+
lint_and_test:
14+
name: Lint and run unit tests
15+
runs-on: ubuntu-20.04
16+
strategy:
17+
matrix:
18+
python-version: [3.7, 3.8, 3.9]
19+
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v2
23+
24+
- name: Set up Python ${{ matrix.python-version }}
25+
uses: actions/setup-python@v2
26+
with:
27+
python-version: ${{ matrix.python-version }}
28+
29+
- name: Install dependencies
30+
run: |
31+
python -m pip install --upgrade pip
32+
pip install -e .[dev]
33+
34+
- name: Lint
35+
run: ./lint_and_test.sh lint
36+
37+
- name: Type check
38+
run: ./lint_and_test.sh types
39+
40+
- name: Unit tests
41+
run: ./lint_and_test.sh tests
42+
43+
check_docs:
44+
name: Check whether the documentation is up to date
45+
runs-on: ubuntu-20.04
46+
47+
steps:
48+
- name: Checkout repository
49+
uses: actions/checkout@v2
50+
51+
- name: Set up Python
52+
uses: actions/setup-python@v2
53+
with:
54+
python-version: 3.7
55+
56+
- name: Install dependencies
57+
run: |
58+
python -m pip install --upgrade pip
59+
pip install -e .[dev]
60+
61+
- name: Check whether docs are built from the latest code
62+
run: ./docs/res/check.sh
63+
64+
deploy:
65+
name: Publish to PyPI
66+
needs: [lint_and_test, check_docs]
67+
runs-on: ubuntu-20.04
68+
69+
steps:
70+
- name: Checkout repository
71+
uses: actions/checkout@v2
72+
73+
- name: Set up Python
74+
uses: actions/setup-python@v2
75+
with:
76+
python-version: 3.7
77+
78+
- name: Install dependencies
79+
run: |
80+
python -m pip install --upgrade pip
81+
pip install --upgrade setuptools twine wheel
82+
83+
- # Determine if this is a beta or latest release
84+
name: Determine release type
85+
run: echo "RELEASE_TYPE=$(if [ ${{ github.event_name }} = release ]; then echo stable; else echo beta; fi)" >> $GITHUB_ENV
86+
87+
- # Check whether the released version is listed in CHANGELOG.md
88+
name: Check whether the released version is listed in the changelog
89+
run: python ./.github/scripts/check_version_in_changelog.py
90+
91+
- # Check version consistency and increment pre-release version number for beta releases (must be the last step before build)
92+
name: Bump pre-release version
93+
if: env.RELEASE_TYPE == 'beta'
94+
run: python ./.github/scripts/update_version_for_beta_release.py
95+
96+
- # Build a source distribution and a python3-only wheel
97+
name: Build distribution files
98+
run: python setup.py sdist bdist_wheel
99+
100+
- # Check whether the package description will render correctly on PyPI
101+
name: Check package rendering on PyPI
102+
run: python -m twine check dist/*
103+
104+
- # Publish package to PyPI using their official GitHub action
105+
name: Publish package to PyPI
106+
run: python -m twine upload --non-interactive --disable-progress-bar dist/*
107+
env:
108+
TWINE_USERNAME: __token__
109+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
110+
111+
- # Tag the current commit with the version tag if this is a beta release (stable releases are tagged with the release process)
112+
name: Tag Version
113+
if: env.RELEASE_TYPE == 'beta'
114+
run: |
115+
git_tag=v`python ./.github/scripts/print_current_package_version.py`
116+
git tag $git_tag
117+
git push origin $git_tag
118+
119+
- # Upload the build artifacts to the release
120+
name: Upload the build artifacts to release
121+
uses: svenstaro/upload-release-action@v2
122+
if: env.RELEASE_TYPE == 'stable'
123+
with:
124+
repo_token: ${{ secrets.GITHUB_TOKEN }}
125+
file: dist/*
126+
file_glob: true
127+
tag: ${{ github.ref }}

.gitignore

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
__pycache__
2-
build/
3-
cli_test_client.py
2+
.mypy_cache
3+
.pytest_cache
44

55
.venv
66
.direnv
77
.envrc
88
.python-version
9-
.mypy_cache
10-
.pytest_cache
119

1210
*.egg-info/
1311
*.egg
12+
dist/
13+
build/
1414

1515
.vscode
1616
.idea
17+
18+
cli_test_client.py

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Changelog
2+
=========
3+
4+
[0.0.1](../../releases/tag/v0.0.1) - 2021-05-13
5+
-----------------------------------------------
6+
7+
Initial release of the package.

0 commit comments

Comments
 (0)