chore: cleanup release process (#516)

* chore: convert bin/README to md

* feat: replace all dates with bumpver

* chore: date in README is now changed by `bumpver`

* chore: clarify the RC workflow

* chore: use global `/tmp` in RC

* chore: slightly prettier description

* chore: move changes file to env

* chore: introduce release_tag workflow

chore(wip): test trigger on PR

chore(wip): add filtered diff

chore(wip): switch to PATTERNS

chore(wip): up to bumpver

chore(wip): temporary disable check

chore(wip): setup python

chore(wip): test with a specific version

chore(wip): test bumpver commit

chore(wip): fix the bumpver usage

chore: cleanup the release_tag workflow

* chore: create draft release from CI

* chore: update the docs

* chore: provide details on the release process

* chore: tiny header update

* chore: remove commented-out code
This commit is contained in:
Mitja O 2023-08-27 16:11:54 +02:00 committed by GitHub
parent e54270207d
commit 4645f7b10a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 289 additions and 72 deletions

View File

@ -130,10 +130,10 @@ jobs:
./bin/package.sh ./bin/package.sh
- name: Publish distribution 📦 to PyPI. - name: Publish distribution 📦 to PyPI.
uses: pypa/gh-action-pypi-publish@master uses: pypa/gh-action-pypi-publish@release/v1
with: with:
password: ${{ secrets.PYPI_API_TOKEN }} password: ${{ secrets.PYPI_API_TOKEN }}
print_hash: true print-hash: true
## DOCKER ## DOCKER
@ -214,3 +214,21 @@ jobs:
- name: "Image digest" - name: "Image digest"
run: "echo ${{ steps.docker_build.outputs.digest }}" run: "echo ${{ steps.docker_build.outputs.digest }}"
## GITHUB RELEASE DRAFT
create_release:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: "ubuntu-latest"
needs:
- "tests"
steps:
- uses: actions/checkout@v3
- uses: softprops/action-gh-release@v1
with:
body: 👋 This is a draft release. Please update it manually.
prerelease: false
draft: true
files: |
CHANGES.rst

View File

@ -2,37 +2,61 @@
name: release_candidate name: release_candidate
# TODO(actions): # Performed actions:
# - [x] create a new AUTO-RC-<DATE> branch # - [x] create a new AUTO-RC-<DATE> branch
# - [x] update CHANGES.rst # - [x] prepare RC metadata and description
# - [x] update CHANGES.rst (+ rc-title, + date)
# - [x] create changes commit # - [x] create changes commit
# - [x] push to GH # - [x] push to GH
# - [ ] update README.md # - [x] open a PR to `master`
# - [ ] create readme commit
# - [ ] push to GH
# - [ ] open a PR to `master`
# TODO(general):
# - [ ] setup the action
# - [ ] cleanup the action
on: on:
schedule: schedule:
- cron: '0 0 1 * *' # each 1st day of the month - cron: "0 0 1 * *" # each 1st day of the month
workflow_dispatch: # on manual trigger workflow_dispatch: # on manual trigger
jobs: jobs:
new-rc: new-rc:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CHANGES_FILE: CHANGES.rst
PR_BODY_FILE: /tmp/pr-body.md
RF_DOCS_FILE: ./docs/contents/repo-maintenance/release-work.md
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
# Flag to fetch all history. # Flag to fetch all history.
# @see https://github.com/marketplace/actions/checkout#Fetch-all-history-for-all-tags-and-branches # @see https://github.com/marketplace/actions/checkout#Fetch-all-history-for-all-tags-and-branches
fetch-depth: 0 fetch-depth: 0
- run: |
RC_DATE=$(date +'%m-%d-%Y') - id: get-rc-date
run: echo "RC_DATE=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
- id: make-pr-body-file
run: |
PR_BODY_FILE=${{ env.PR_BODY_FILE }}
RC_DATE=${{ steps.get-rc-date.outputs.RC_DATE }}
touch ${PR_BODY_FILE}
echo "📦 Automated release candidate for ${RC_DATE}." >> ${PR_BODY_FILE}
echo "" >> ${PR_BODY_FILE}
echo "_TODO:_" >> ${PR_BODY_FILE}
echo "- [ ] Manually adjust generated CHANGES lines" >> ${PR_BODY_FILE}
echo "- [ ] Manually adjust generated CHANGES title" >> ${PR_BODY_FILE}
echo "- [ ] Manually adjust generated CHANGES date" >> ${PR_BODY_FILE}
echo "- [ ] Approve and merge this PR" >> ${PR_BODY_FILE}
echo "- [ ] See \`${{ env.RF_DOCS_FILE }}\` to continue" >> ${PR_BODY_FILE}
echo "${PR_BODY_FILE}:"
cat ${PR_BODY_FILE}
- id: propose-rc
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CHANGES_FILE=${{ env.CHANGES_FILE }}
PR_BODY_FILE=${{ env.PR_BODY_FILE }}
RC_DATE=${{ steps.get-rc-date.outputs.RC_DATE }}
git config user.name github-actions git config user.name github-actions
git config user.email github-actions@github.com git config user.email github-actions@github.com
git checkout -b auto-release-candidate-${RC_DATE} git checkout -b auto-release-candidate-${RC_DATE}
@ -43,13 +67,11 @@ jobs:
./bin/update_changelog.sh ./bin/update_changelog.sh
git add CHANGES.rst git add ${CHANGES_FILE}
git commit -m "chore(rc-changes): update Changes.rst" git commit -m "chore(rc-changes): update ${CHANGES_FILE}"
git push git push
gh pr create --title "chore(auto-release-candidate-${RC_DATE})" \ gh pr create --title "chore(auto-release-candidate-${RC_DATE})" \
--body "Automated release candidate for ${RC_DATE}." \ --body-file ${PR_BODY_FILE} \
--base master \ --base master \
--draft --draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

58
.github/workflows/rt.yml vendored Normal file
View File

@ -0,0 +1,58 @@
# Release Tag workflow
name: release_tag
# Performed actions:
# - [x] check that last commit is the CHANGES edit
# - [x] infer the last RC version
# - [x] run bumpver.py with the new version
# - [x] push the commit and new tag
on:
workflow_dispatch: # on manual trigger
jobs:
new-tag:
if: ${{ github.ref_name == 'master' }}
runs-on: ubuntu-latest
env:
CHANGES_FILE: CHANGES.rst
EXPECTED_DIFF_COUNT: 1
steps:
- uses: actions/checkout@v3
- id: get-diff
uses: technote-space/get-diff-action@v6
with:
PATTERNS: |
${{ env.CHANGES_FILE }}
SET_ENV_NAME_COUNT: true
- id: check-changes
run: |
echo "${{ steps.get-diff.outputs.count }}/${{ env.EXPECTED_DIFF_COUNT }} changes in ${{ env.CHANGES_FILE }}."
exit ${{ steps.get-diff.outputs.count == env.EXPECTED_DIFF_COUNT && 0 || 1 }}
- id: get-version
run: |
LAST_VERSION=$(grep -m1 -E ' \([0-9]+-[0-9]+-[0-9]+\)$' ${CHANGE_FILE} | awk '{ print $1 }')
echo "LAST_VERSION=${LAST_VERSION}" >> "$GITHUB_OUTPUT"
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- id: install-requirements
run: pip install -r "requirements/dev.pip"
- name: run `bumpver`
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CHANGE_FILE=${{ env.CHANGES_FILE }}
LAST_VERSION=${{ steps.get-version.outputs.LAST_VERSION }}
git config user.name github-actions
git config user.email github-actions@github.com
python3 bin/bumpver.py -t "Automated release ${LAST_VERSION}" ${LAST_VERSION}
git push --follow-tags

31
bin/README.md Normal file
View File

@ -0,0 +1,31 @@
# Build scripts folder
## Highlight files
- `bumpver.py` : Bump, commit and tag new project versions
- `package.sh` : Build deployable artifact (wheel) in `/dist/` folder.
## Fully manual release check-list
1. Update `/CHANGES.rst` (+ Title + Date).
2. Push to GitHub to run all TCs once more.
3. Bump version: commit & tag it with `/bin/bumpver.py`. Use `--help`.
> 💡 Read [PEP-440](https://www.python.org/dev/peps/pep-0440/) to decide the version.
4. Push it in GitHub with `--follow-tags`.
### Manually publishing a new package
1. Generate package *wheel* with `/bin/package.sh`.
2. Upload to PyPi with `twine upload -s -i <gpg-user> dist/*`
3. Ensure that the new tag is built on
[`hub.docker.com`](https://hub.docker.com/r/pypiserver/pypiserver)
as `latest` and as a direct tag reference.
4. Copy release notes from `/CHANGES.rst` in GitHub as new *"release"*
page on the new tag.
> 💡 Check syntactic differences between `.md` and `.rst` files.

View File

@ -1,36 +0,0 @@
====================
Build scripts folder
====================
Files:
======
- ``bumpver.py`` : Bump, commit and tag new project versions
- ``check_readme.sh`` : Check that README has no RsT-syntactic errors.
- ``package.sh`` : Build deployable artifact (wheel) in ``/dist/`` folder.
- ``README.rst`` : This file.
Release check-list:
===================
1. Update ``/CHANGES.rst`` (+ Title + Date) & ``/README.md`` (Date,
not version).
2. Push to GitHub to run all TCs once more.
3. Bump version: commit & tag it with ``/bin/bumpver.py``.
Use ``--help``.
Read `PEP-440 <https://www.python.org/dev/peps/pep-0440/>`_ to decide the version.
4. Push it in GitHub with ``--follow-tags``.
5. Generate package *wheel* with ``/bin/package.sh``.
6. Upload to PyPi with ``twine upload -s -i <gpg-user> dist/*``
7. Ensure that the new tag is built on hub.docker.com as ``latest`` and as a
direct tag reference.
8. Copy release notes from ``/CHANGES.rst`` in GitHub as new *"release"* page
on the new tag. Check syntactic differences between ``.md`` and ``.rst`` files.

View File

@ -35,19 +35,20 @@ EXAMPLE:
""" """
import os.path as osp
import sys
import re
import functools as fnt import functools as fnt
import os.path as osp
import re
import sys
from datetime import datetime
import docopt import docopt
my_dir = osp.dirname(__file__) my_dir = osp.dirname(__file__)
VFILE = osp.join(my_dir, "..", "pypiserver", "__init__.py") VFILE = osp.join(my_dir, "..", "pypiserver", "__init__.py")
VFILE_regex_v = re.compile(r'version *= *__version__ *= *"([^"]+)"') VFILE_regex_version = re.compile(r'version *= *__version__ *= *"([^"]+)"')
VFILE_regex_d = re.compile(r'__updated__ *= *"([^"]+)"') VFILE_regex_datetime = re.compile(r'__updated__ *= *"([^"]+)"')
VFILE_regex_date = re.compile(r'__updated__ *= *"([^"\s]+)\s')
RFILE = osp.join(my_dir, "..", "README.md") RFILE = osp.join(my_dir, "..", "README.md")
@ -58,6 +59,13 @@ class CmdException(Exception):
pass pass
def get_current_date_info() -> (str, str):
now = datetime.now()
new_datetime = now.strftime("%Y-%m-%d %H:%M:%S%z")
new_date = now.strftime("%Y-%m-%d")
return (new_datetime, new_date)
@fnt.lru_cache() @fnt.lru_cache()
def read_txtfile(fpath): def read_txtfile(fpath):
with open(fpath, "rt", encoding="utf-8") as fp: with open(fpath, "rt", encoding="utf-8") as fp:
@ -138,7 +146,6 @@ def exec_cmd(cmd):
def do_commit(new_ver, old_ver, dry_run, amend, ver_files): def do_commit(new_ver, old_ver, dry_run, amend, ver_files):
import pathlib import pathlib
# new_ver = strip_ver2_commonprefix(old_ver, new_ver)
cmt_msg = "chore(ver): bump %s-->%s" % (old_ver, new_ver) cmt_msg = "chore(ver): bump %s-->%s" % (old_ver, new_ver)
ver_files = [pathlib.Path(f).as_posix() for f in ver_files] ver_files = [pathlib.Path(f).as_posix() for f in ver_files]
@ -183,11 +190,12 @@ def bumpver(
cmd.append(RFILE) cmd.append(RFILE)
exec_cmd(cmd) exec_cmd(cmd)
regexes = [VFILE_regex_v, VFILE_regex_d] regexes = [VFILE_regex_version, VFILE_regex_datetime, VFILE_regex_date]
old_ver, old_date = extract_file_regexes(VFILE, regexes) old_ver, old_datetime, old_date = extract_file_regexes(VFILE, regexes)
if not new_ver: if not new_ver:
yield old_ver yield old_ver
yield old_datetime
yield old_date yield old_date
else: else:
if new_ver == old_ver: if new_ver == old_ver:
@ -199,12 +207,13 @@ def bumpver(
msg += "!\n Use of --force recommended." msg += "!\n Use of --force recommended."
raise CmdException(msg % new_ver) raise CmdException(msg % new_ver)
from datetime import datetime new_datetime, new_date = get_current_date_info()
new_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S%z")
ver_files = [osp.normpath(f) for f in [VFILE, RFILE]] ver_files = [osp.normpath(f) for f in [VFILE, RFILE]]
subst_pairs = [(old_ver, new_ver), (old_date, new_date)] subst_pairs = [
(old_ver, new_ver),
(old_datetime, new_datetime),
(old_date, new_date),
]
for repl in replace_substrings(ver_files, subst_pairs): for repl in replace_substrings(ver_files, subst_pairs):
new_txt, fpath, replacements = repl new_txt, fpath, replacements = repl
@ -258,6 +267,7 @@ def main(*args):
except CmdException as ex: except CmdException as ex:
sys.exit(str(ex)) sys.exit(str(ex))
except Exception as ex: except Exception as ex:
print("Unexpected error happened.")
raise ex raise ex

View File

@ -32,6 +32,7 @@ mkdir -p $WORKSPACE_DIR
echo "Updating $CHANGE_FILE:" echo "Updating $CHANGE_FILE:"
# TODO(tech-debt): get `LAST_VERSION` with a separate bash script
LAST_VERSION=$(grep -m1 -E ' \([0-9]+-[0-9]+-[0-9]+\)$' $CHANGE_FILE | awk '{ print $1 }') LAST_VERSION=$(grep -m1 -E ' \([0-9]+-[0-9]+-[0-9]+\)$' $CHANGE_FILE | awk '{ print $1 }')
echo "Detected last release version: $LAST_VERSION" echo "Detected last release version: $LAST_VERSION"

View File

@ -0,0 +1,113 @@
# `Pypi-server` Release Workflow Reference
The official `pypi-server` releases are handled using
[GitHub Actions workflows](../../../.github/workflows/).
## General release process
```mermaid
flowchart LR
rc["release-candidate ⭐️"]
rn["release-notes 📝"]
rm["confirmed-tag ✅"]
ci["code-checks 🧪"]
pk["build-and-pack 📦"]
py["pypi-index 🗃️"]
do["docker-hub 🐳"]
gr["github-release 📣"]
subgraph "Preparation 🌱"
rc-->rn-->rm
end
subgraph "Integration 🪴"
rm-->ci-->pk
end
subgraph "Deploy 🌳"
pk--> py & do & gr
end
```
## Process walkthrough
> 🗺️ ***This description approximates the real GitHub workflows and steps.***
> 👀 *For a more detailed view, do check out the linked resources as you read.*
### Preparation 🌱
> 🛠️ *These step are applicable only for maintainers.*
#### Release candidate ⭐️
A new release candidate can be initiated ***manually** or **on a monthly schedule***.
This is done via the [`rc.yml`](../../../.github/workflows/rc.yml) GH
Workflow's `workflow_dispatch` or `schedule` trigger.
The workflow automatically prepares a list of changes for the `CHANGES.rst` and
creates a new Pull Request *(rc PR)* named
`chore(auto-release-candidate-YYY-MM-DD)` including these draft change notes.
#### Release notes 📝
In the created rc PR, open the `CHANGES.rst` and:
1. ***adjust the suggested changelog items***
2. ***choose & set the next released version***
3. ***set the right release date***
Commit the changes and push them to the head branch of the rc PR.
#### Confirmed tag ✅
1. Once everything is looking good, ***approve and merge*** the rc PR.
It will create the new *commit* with the updated `CHANGES.rst`
on the default branch.
2. Next, to create a release tag, ***manually run*** the
[`rt.yml`](../../../.github/workflows/rt.yml) GH Workflow.
First, it executes all the [`bumpver`](../../../bin/README.md) procedures.
Next, it commits and pushes the new **version tag** to the default branch.
### Integration 🪴
#### Code checks 🧪
Once any *commmit* or *tag* is pushed to the default branch,
[`ci.yml`](../../../.github/workflows/ci.yml) GH Workflow automatically
executes diverse code checks: e.g. *linting*, *formatting*, *tests*.
#### Build and pack 📦
If all the checks are successful, [`ci.yml`](../../../.github/workflows/ci.yml)
builds all the code artifacts: e.g. *wheels*, *docker images*.
### Deploy 🌳
#### Publish to PyPi 🗃️
> 🏷️ This happens only on new *version tags*.
Once everythig is built, [`ci.yml`](../../../.github/workflows/ci.yml) uploads
the wheels to the [`pypiserver` PyPi project](https://pypi.org/project/pypiserver/).
#### Publish to Docker Hub 🐳
> 🏷️ Docker image *tags* are determined on the fly.
If all is successful so far, [`ci.yml`](../../../.github/workflows/ci.yml) tags
the built docker images and pushes them to the
[`pypiserver` Docker Hub repository](https://hub.docker.com/r/pypiserver/pypiserver).
#### Publish a GitHub Release draft 📣
> 🛠️ *This step is applicable only for maintainers.*
> 🏷️ This happens only on new *version tags*.
To make the release noticeable, [`ci.yml`](../../../.github/workflows/ci.yml)
also creates a *draft*
[GitHub Release entry in the `pypiserver` repository](https://github.com/pypiserver/pypiserver/releases).
> 📝 Since it is a *draft*, the entry should be *manually* adjusted further.