diff options
author | Mario Corchero <mcorcherojim@bloomberg.net> | 2021-07-05 13:26:33 +0200 |
---|---|---|
committer | Paul Ganssle <paul@ganssle.io> | 2021-07-06 12:26:41 -0400 |
commit | 6b337ea412d399fb48771c544b1a6880763b46c6 (patch) | |
tree | 52fab2c07b4c4e11d71896325b2b585900a8695f | |
parent | 9c2ad8f981ece1bdb3d52527f1cb39523b11d862 (diff) | |
download | dateutil-git-6b337ea412d399fb48771c544b1a6880763b46c6.tar.gz |
Automate cutting new releases
This replaces the manual workflow that involved invoking `release.py`
with a GitHub workflow triggered either manually or via pushing tags and
cutting releases.
-rw-r--r-- | .github/workflows/publish.yml | 58 | ||||
-rw-r--r-- | RELEASING | 68 | ||||
-rw-r--r-- | release.py | 81 | ||||
-rw-r--r-- | tox.ini | 24 |
4 files changed, 112 insertions, 119 deletions
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..39b2e87 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,58 @@ +# This workflow is triggered three ways: +# +# 1. Manually triggering the workflow via the GitHub UI (Actions > Upload +# package) will upload to test.pypi.org without the need to create a tag. +# 2. When a tag is created, the workflow will upload the package to +# test.pypi.org. +# 3. When a GitHub Release is made, the workflow will upload the package to pypi.org. +# +# It is done this way until PyPI has draft reviews, to allow for a two-stage +# upload with a chance for manual intervention before the final publication. +name: Upload package + +on: + release: + types: [created] + push: + tags: + - '*' + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U tox + - name: Create tox environments + run: | + tox -p -e py,build,release --notest + - name: Run tests + run: | + tox -e py + - name: Build package + run: | + tox -e build + - name: Publish package + env: + TWINE_USERNAME: "__token__" + run: | + if [[ "$GITHUB_EVENT_NAME" == "push" || \ + "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + export TWINE_REPOSITORY_URL="https://test.pypi.org/legacy/" + export TWINE_PASSWORD="${{ secrets.TEST_PYPI_UPLOAD_TOKEN }}" + elif [[ "$GITHUB_EVENT_NAME" == "release" ]]; then + export TWINE_REPOSITORY="pypi" + export TWINE_PASSWORD="${{ secrets.PYPI_UPLOAD_TOKEN }}" + else + echo "Unknown event name: ${GITHUB_EVENT_NAME}" + exit 1 + fi + tox -e release @@ -13,10 +13,8 @@ Release Checklist interesting to anyone consuming the package (e.g. changes to CI) with a reference to the Github PR. [ ] Commit the changes in git and make a pull request. -[ ] Accept the pull request and tag the repository with the release number. -[ ] Add the contents of the NEWS file to the Github release notes for the - release. -[ ] Upload the source and binary distributions with `tox -e release`. +[ ] Follow the "Releasing" steps below + Optional: ---------- @@ -24,12 +22,6 @@ Optional: [ ] Check that the documentation builds correctly (cd docs, make html) -Instructions ------------------------------------------ -See the instructions at https://packaging.python.org/en/latest/distributing/ -for more details. - - Versioning ---------- Try and keep to a semantic versioning scheme (http://semver.org/). The versions @@ -37,29 +29,51 @@ are managed with `setuptools_scm`, so to update the version, simply tag the relevant commit with the new version number. +Instructions +----------------------------------------- +See the instructions at https://packaging.python.org/en/latest/distributing/ +for more details. + + Building and Releasing ---------------------- -Building and releasing can be done using the `release.py` script, which -automates building, signing and uploading. Since it uses GPG for signing and -for decrypting a stored token, it requires that `gpg` be installed on your -system. Because it has python dependencies, the best way to use the -`release.py` script is to invoke it using `tox`. To build the source and binary -distributions, use: +Releasing is automated via the `publish.yml` GitHub Actions workflow. When a +new tag is pushed to the repository, the project is automatically built and +uploaded to Test PyPI. When the publish action is triggered manually (see +https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow +for more details), the result is uploaded to PyPI. + +To make a release: - tox -e build +1. After having made a PR with all the relevant changes, trigger the "Upload + package" to trigger an upload to Test PyPI. If desired, you can push a + `.dev0` or `.rc0` tag first, so that all uploads will have a prefix for the + *next* version rather than the previous version (e.g. if you are releasing + `3.1.2`, without making a new tag releases will have a version like + `3.1.1+gff8893e.d20220603`; if you push a `3.1.2.dev0` tag first, the version + number will be `3.1.2.dev0`, and subsequent commits will be things like + `3.1.2.dev0+fe9dacc4.d20220603`). -This will build the distributions in `dist/`. Once that is done, you can release -them with: +2. Check the Test PyPI page for `python-dateutil` to ensure that the dev release + worked correctly: https://test.pypi.org/project/python-dateutil/ - tox -e release + Dev releases may not appear as the default page, so click "Release history" + and navigate to the release you are trying to check. Make sure that the + metadata looks right and in particular that the `Requires` metadata is + present. -if you have the token stored in your `~/.pypirc` file. If you have stored the -relevant token in an encrypted file, use the `--passfile` argument: +4. If the release failed or was unsatisfactory in some way, make the required + changes and got back to step 1. Pushing a new tag is not necessary. - tox -e release -- --passfile .token.gpg +5. When everything looks good, merge the release PR, pull the result to your + local branch and create a new tag with a non-dev version number, + e.g. `3.1.2`. Push this to the repository, wait for the Test PyPI run to + trigger, and ensure that the upload worked. -The `release` command defaults to uploading to test.pypi.org. To upload to -pypi.org, use the `--release` flag, so putting it all together, we have: +6. Create a new GitHub release with the new entries from `NEWS` in the + description. This will trigger the workflow to build and release the final + version to PyPI.org. Check https://pypi.org/project/python-dateutil to + ensure that everything worked correctly. - tox -e build - tox -e release -- --passfile .token.gpg --release +7. Delete any dev tags created during the testing process from your upstream + and local branches. diff --git a/release.py b/release.py deleted file mode 100644 index 3608d7f..0000000 --- a/release.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Release script -""" - -import glob -import os -import shutil -import subprocess -import sys - -import click - -@click.group() -def cli(): - pass - -@cli.command() -def build(): - DIST_PATH = 'dist' - if os.path.exists(DIST_PATH) and os.listdir(DIST_PATH): - if click.confirm('{} is not empty - delete contents?'.format(DIST_PATH)): - shutil.rmtree(DIST_PATH) - os.makedirs(DIST_PATH) - else: - click.echo('Aborting') - sys.exit(1) - - subprocess.check_call(['python', '-m', 'build', - '--wheel', '--sdist', - '--outdir', DIST_PATH, - '.']) - -@cli.command() -def sign(): - # Sign all the distribution files - for fpath in glob.glob('dist/*'): - subprocess.check_call(['gpg', '--armor', '--output', fpath + '.asc', - '--detach-sig', fpath]) - - # Verify the distribution files - for fpath in glob.glob('dist/*'): - if fpath.endswith('.asc'): - continue - - subprocess.check_call(['gpg', '--verify', fpath + '.asc', fpath]) - - -@cli.command() -@click.option('--passfile', default=None, - help='File path to read and decrypt with gpg.') -@click.option('--release/--no-release', default=False) -def upload(passfile, release): - if release: - repository='pypi' - else: - repository='pypitest' - - env = os.environ.copy() - if passfile is not None: - gpg_call = subprocess.run(['gpg', '-d', passfile], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - username, password = gpg_call.stdout.decode('utf-8').split('\n') - env['TWINE_USERNAME'] = username - env['TWINE_PASSWORD'] = password - - dist_files = glob.glob('dist/*') - for dist_file in dist_files: - if dist_file.endswith('.asc'): - continue - if dist_file + '.asc' not in dist_files: - raise ValueError('Missing signature file for: {}'.format(dist_file)) - - args = ['twine', 'upload', '-r', repository] + dist_files - - p = subprocess.Popen(args, env=env) - p.wait() - -if __name__ == "__main__": - cli() @@ -98,18 +98,20 @@ description = Build an sdist and bdist basepython = python3.7 skip_install = true passenv = * -deps = click >= 7.0 - build[virtualenv] >= 0.3.0 +deps = build[virtualenv] >= 0.3.0 commands = - python release.py build + python -m build --wheel --sdist --outdir dist . [testenv:release] -description = Sign and upload the built distributions to PyPI -basepython = python3.7 -skip_install = true -passenv = * -deps = click >= 7.0 - twine >= 2.0.0 +description = Make a release; must be called after "build" +skip_install = True +deps = + twine +depends = + build +passenv = + TWINE_* commands = - python release.py sign - python release.py upload {posargs} + twine check {toxinidir}/dist/* + twine upload {toxinidir}/dist/* \ + {posargs:-r {env:TWINE_REPOSITORY:testpypi} --non-interactive} |