Skip to content

Commit

Permalink
Merge pull request #1946 from lukpueh/auto-release
Browse files Browse the repository at this point in the history
Add GH workflow to build and release on GH and PyPI
  • Loading branch information
lukpueh authored Apr 21, 2022
2 parents 31ca674 + b99d043 commit 72424a9
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 51 deletions.
87 changes: 87 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: CD
concurrency: cd

# Trigger workflow on any completed CI (see further checks below)
on:
workflow_run:
workflows: [CI]
types: [completed]

jobs:
build:
name: Build
runs-on: ubuntu-latest
# Skip unless CI was successful and ran on release tag, a ref starting with 'v'.
# NOTE: We assume CI does not trigger on branches that start with 'v' (see #1961)
if: >-
github.event.workflow_run.conclusion == 'success' &&
startsWith(github.event.workflow_run.head_branch, 'v')
outputs:
release_id: ${{ steps.gh-release.outputs.id }}
steps:
- name: Checkout release tag
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
with:
ref: ${{ github.event.workflow_run.head_branch }}

- name: Set up Python
uses: actions/setup-python@0ebf233433c08fb9061af664d501c3f3ff0e9e20
with:
python-version: '3.x'

- name: Install build dependency
run: python3 -m pip install --upgrade pip build

- name: Build binary wheel and source tarball
run: python3 -m build --sdist --wheel --outdir dist/ .

- id: gh-release
name: Publish GitHub release candiate
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5
with:
name: ${{ github.event.workflow_run.head_branch }}-rc
tag_name: ${{ github.event.workflow_run.head_branch }}
body: "Release waiting for review..."
files: dist/*

- name: Store build artifacts
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
# NOTE: The GitHub release page contains the release artifacts too, but using
# GitHub upload/download actions seems robuster: there is no need to compute
# download URLs and tampering with artifacts between jobs is more limited.
with:
name: build-artifacts
path: dist

release:
name: Release
runs-on: ubuntu-latest
needs: build
environment: release
steps:
- name: Fetch build artifacts
uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741
with:
name: build-artifacts
path: dist

- name: Publish binary wheel and source tarball on PyPI
uses: pypa/gh-action-pypi-publish@717ba43cfbb0387f6ce311b169a825772f54d295
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

- name: Finalize GitHub release
uses: actions/github-script@9ac08808f993958e9de277fe43a64532a609130e
with:
script: |
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: '${{ needs.build.outputs.release_id }}',
name: '${{ github.event.workflow_run.head_branch }}',
body: 'See [CHANGELOG.md](https://github.com/' +
context.repo.owner + '/' + context.repo.repo + '/blob/' +
'${{ github.event.workflow_run.head_branch }}'+
'/docs/CHANGELOG.md) for details.'
})
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
name: CI

on:
# NOTE: CD relies on this configuration (see #1961)
push:
branches:
- develop
tags:
- v*

pull_request:
workflow_dispatch:

Expand Down
81 changes: 46 additions & 35 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,50 @@
# Release process

* Ensure you have a backup of all working files and then remove files not tracked by git
`git clean -xdf`. **NOTE**: this will delete all files in the tuf tree that aren't
tracked by git
* Ensure `docs/CHANGELOG.md` contains a one-line summary of each [notable

**Prerequisites (one-time setup)**


1. Go to [PyPI management page](https://pypi.org/manage/account/#api-tokens) and create
an [API token](https://pypi.org/help/#apitoken) with its scope limited to the tuf project.
1. Go to [GitHub
settings](https://github.com/theupdateframework/python-tuf/settings/environments),
create an
[environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#creating-an-environment)
called `release` and configure [review
protection](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#required-reviewers).
1. In the environment create a
[secret](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#environment-secrets)
called `PYPI_API_TOKEN` and paste the token created above.

## Release

1. Ensure `docs/CHANGELOG.md` contains a one-line summary of each [notable
change](https://keepachangelog.com/) since the prior release
* Update `tuf/__init__.py` to the new version number "A.B.C"
* Test packaging, uploading to Test PyPI and installing from a virtual environment
(ensure commands invoking `python` below are using Python 3)
* Remove existing dist build dirs
* Create source dist and wheel `python3 -m build`
* Sign source dist `gpg --detach-sign -a dist/tuf-A.B.C.tar.gz`
* Sign wheel `gpg --detach-sign -a dist/tuf-A.B.C-py3-none-any.whl`
* Upload to test PyPI `twine upload --repository testpypi dist/*`
* Verify the uploaded package at https://test.pypi.org/project/tuf/:
Note that installing packages with pip using test.pypi.org is potentially
dangerous (as dependencies may be squatted): download the file and install
the local file instead.
* Create a PR with updated `CHANGELOG.md` and version bumps
* Once the PR is merged, pull the updated `develop` branch locally
* Create a signed tag matching the updated version number on the merge commit
2. Update `tuf/__init__.py` to the new version number `A.B.C`
3. Create a PR with updated `CHANGELOG.md` and version bumps

➔ Review PR on GitHub

4. Once the PR is merged, pull the updated `develop` branch locally
5. Create a signed tag for the version number on the merge commit
`git tag --sign vA.B.C -m "vA.B.C"`
* Push the tag to GitHub `git push origin vA.B.C`
* Create a new release on GitHub, copying the `CHANGELOG.md` entries for the
release
* Create a package for the formal release
(ensure commands invoking `python` below are using Python 3)
* Remove existing dist build dirs
* Create source dist and wheel `python3 -m build`
* Sign source dist `gpg --detach-sign -a dist/tuf-A.B.C.tar.gz`
* Sign wheel `gpg --detach-sign -a dist/tuf-A.B.C-py3-none-any.whl`
* Upload to PyPI `twine upload dist/*`
* Verify the package at https://pypi.org/project/tuf/ and by installing with pip
* Attach both signed dists and their detached signatures to the release on GitHub
* `verify_release` should be used to make sure the release artifacts match the
git sources, preferably by another developer on a different machine.
* Announce the release on [#tuf on CNCF Slack](https://cloud-native.slack.com/archives/C8NMD3QJ3)
* Ensure [POUF 1](https://github.com/theupdateframework/taps/blob/master/POUFs/reference-POUF/pouf1.md), for the reference implementation, is up-to-date
6. Push the tag to GitHub `git push origin vA.B.C`

*A push triggers the [CI workflow](.github/workfows/ci.yml), which, on success,
triggers the [CD workflow](.github/workfows/cd.yml), which builds source dist and
wheel, creates a preliminary GitHub release under `vA.B.C-rc`, and pauses for review.*

7. Run `verify_release --skip-pypi` locally to make sure a build on your machine matches
the preliminary release artifacts published on GitHub.

➔ [Review *deployment*](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments)
on GitHub

*An approval resumes the CD workflow to publish the release on PyPI, and to finalize the
GitHub release (removes `-rc` suffix and updates release notes).*

8. `verify_release` may be used again to make sure the PyPI release artifacts match the
local build as well.
9. Announce the release on [#tuf on CNCF Slack](https://cloud-native.slack.com/archives/C8NMD3QJ3)
10. Ensure [POUF 1](https://github.com/theupdateframework/taps/blob/master/POUFs/reference-POUF/pouf1.md),
for the reference implementation, is up-to-date
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ commands =

[testenv:lint]
changedir = {toxinidir}
lint_dirs = tuf examples tests
lint_dirs = tuf examples tests verify_release
commands =
black --check --diff {[testenv:lint]lint_dirs}
isort --check --diff {[testenv:lint]lint_dirs}
Expand Down
40 changes: 25 additions & 15 deletions verify_release
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Builds a release from current commit and verifies that the release artifacts
on GitHub and PyPI match the built release artifacts.
"""

import argparse
import json
import os
import subprocess
Expand All @@ -17,12 +18,12 @@ from filecmp import dircmp
from tempfile import TemporaryDirectory

try:
import build as _ # type: ignore
import requests
import build
except ImportError:
print ("Error: verify_release requires modules 'requests' and 'build':")
print (" pip install requests build")
exit(1)
print("Error: verify_release requires modules 'requests' and 'build':")
print(" pip install requests build")
sys.exit(1)

# Project variables
# Note that only these project artifacts are supported:
Expand Down Expand Up @@ -135,9 +136,17 @@ def progress(s: str) -> None:


def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--skip-pypi",
action="store_true",
dest="skip_pypi",
help="Skip PyPI release check.",
)
args = parser.parse_args()

success = True
with TemporaryDirectory() as build_dir:

progress("Building release")
build_version = build(build_dir)
finished(f"Built release {build_version}")
Expand All @@ -152,16 +161,17 @@ def main() -> int:
if github_version != build_version:
finished(f"WARNING: GitHub latest version is {github_version}")

progress("Checking PyPI latest version")
pypi_version = get_pypi_pip_version()
if pypi_version != build_version:
finished(f"WARNING: PyPI latest version is {pypi_version}")

progress("Downloading release from PyPI")
if not verify_pypi_release(build_version, build_dir):
# This is expected while build is not reproducible
finished("ERROR: PyPI artifacts do not match built release")
success = False
if not args.skip_pypi:
progress("Checking PyPI latest version")
pypi_version = get_pypi_pip_version()
if pypi_version != build_version:
finished(f"WARNING: PyPI latest version is {pypi_version}")

progress("Downloading release from PyPI")
if not verify_pypi_release(build_version, build_dir):
# This is expected while build is not reproducible
finished("ERROR: PyPI artifacts do not match built release")
success = False

progress("Downloading release from GitHub")
if not verify_github_release(build_version, build_dir):
Expand Down

0 comments on commit 72424a9

Please sign in to comment.