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 GH workflow to build and release on GH and PyPI #1946

Merged
merged 8 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
94 changes: 94 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: CD
concurrency: cd

# Trigger workflow on completed CI (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 a ref starting with 'v' (release tag)
if: ${{ github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'v') }}
# NOTE: This works because we currently only trigger CI on a push to the 'develop'
# branch or a 'v*'-tag, but it seems rather brittle.
# Unfortunately, there is not much more info we get from the CI workflow
# ('workflow_run') than the ref name. No ref, ref_type, etc., so we don't even know
# if a tag or a branch was pushed. :(
# See https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#workflow_run
# NOTE: (2) An alternative solution might be to restructure workflows, so that all
# test logic from 'ci.yml' is moved to a separate workflow file '_test.yml', that
# can be included in both CI (triggered on push to 'develop'-branch) and CD
# (triggered on push to 'v*'-tag) workflows.
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
jku marked this conversation as resolved.
Show resolved Hide resolved
tag_name: ${{ github.event.workflow_run.head_branch }}
body: "Release waiting for review..."
files: dist/*

- name: Store build artifacts
# NOTE: The release job could download the assets from the GitHub release page,
# published in the previous step. But using the GitHub upload/download actions
# seems more robust as there is no need to compute download URLs.
# NOTE: (2) action-gh-release returns download URLSs as output, which could be
# propagated to next job along with release_id (see above)
# https://github.com/softprops/action-gh-release#outputs
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535
with:
name: build-artifacts
path: dist

release:
name: Release
runs-on: ubuntu-latest
needs: build
environment: release
jku marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Up @@ -4,6 +4,10 @@ on:
push:
branches:
- develop
tags:
# TODO: Should we restrict to vX.Y.Z tags?
- v*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine even without the TODO


pull_request:
workflow_dispatch:

Expand Down
60 changes: 25 additions & 35 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,29 @@
# 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
jku marked this conversation as resolved.
Show resolved Hide resolved
* Ensure `docs/CHANGELOG.md` contains a one-line summary of each [notable
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 worfklow](.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 *deployemnt*](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments) on GitHub
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
➔ [Review *deployemnt*](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments) 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 (removse `-rc` suffix and updates release notes).*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
*An approval resumes the CD workflow to publish the release on PyPI, and to finalize the
GitHub release (removse `-rc` suffix and updates release notes).*
*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 release artifacts PyPI.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
8. `verify_release` may be used again to make sure the release artifacts PyPI.
8. `verify_release` may be used again to make sure the PyPI release artifacts match local build as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦 Apologies!

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 @@ -126,9 +127,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 @@ -143,16 +152,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