Skip to content

Commit f0c1c76

Browse files
committed
Support for towncrier and release targets
Signed-off-by: Andreas Maier <andreas.r.maier@gmx.de>
1 parent b0d36bb commit f0c1c76

19 files changed

+632
-231
lines changed

.github/workflows/publish.yml

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# This GitHub workflow will publish the package to Pypi and create a new stable branch when releasing the master branch.
2+
# For more information see:
3+
# https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
4+
# https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
5+
6+
name: publish
7+
8+
on:
9+
push: # When pushing a tag
10+
tags:
11+
- "*"
12+
- "!*a0" # Exclude initial tag for a new version
13+
14+
jobs:
15+
publish:
16+
name: Build and publish to PyPI
17+
if: startsWith(github.ref, 'refs/tags')
18+
runs-on: ubuntu-latest
19+
# This workflow uses Pypi trusted publishing, see https://docs.pypi.org/trusted-publishers/
20+
# Requirements:
21+
# - On the Pypi project, the GitHub repo must be defined as a trusted publisher
22+
# - On the GitHub repo, an environment 'pypi' must exist
23+
environment: pypi # For Pypi trusted publishing
24+
permissions:
25+
id-token: write # For Pypi trusted publishing
26+
contents: write # For creating GitHub release
27+
steps:
28+
29+
#-------- Info gathering and checks
30+
- name: Set pushed tag
31+
id: set-tag
32+
uses: actions/github-script@v8
33+
with:
34+
result-encoding: string
35+
script: |
36+
const result = "${{ github.ref }}".match("refs/tags/(.*)$")[1]
37+
console.log(result)
38+
return result
39+
- name: Check validity of pushed tag
40+
run: |
41+
if [[ ${{ steps.set-tag.outputs.result }} =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
42+
echo "Pushed tag '${{ steps.set-tag.outputs.result }}' is valid";
43+
else
44+
echo "Pushed tag '${{ steps.set-tag.outputs.result }}' is invalid (must be 'M.N.U')";
45+
false;
46+
fi
47+
- name: Determine whether releasing the master branch
48+
id: set-is-master-branch
49+
uses: actions/github-script@v8
50+
with:
51+
result-encoding: string
52+
script: |
53+
const resp = await github.rest.git.getRef({
54+
owner: context.repo.owner,
55+
repo: context.repo.repo,
56+
ref: "heads/master",
57+
})
58+
const result = (resp.data.object.sha == "${{ github.sha }}")
59+
console.log(result)
60+
return result
61+
env:
62+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63+
- name: Determine name of stable branch for pushed tag
64+
id: set-stable-branch
65+
uses: actions/github-script@v8
66+
with:
67+
result-encoding: string
68+
script: |
69+
const result = "stable_"+"${{ steps.set-tag.outputs.result }}".match("([0-9]+\.[0-9]+)\.")[1]
70+
console.log(result)
71+
return result
72+
- name: Determine whether releasing stable branch for pushed tag
73+
id: set-is-stable-branch
74+
uses: actions/github-script@v8
75+
with:
76+
result-encoding: string
77+
script: |
78+
var resp
79+
try {
80+
resp = await github.rest.git.getRef({
81+
owner: context.repo.owner,
82+
repo: context.repo.repo,
83+
ref: "heads/${{ steps.set-stable-branch.outputs.result }}",
84+
})
85+
}
86+
catch(err) {
87+
console.log("false (stable branch does not exist: "+err+")")
88+
return false
89+
}
90+
const result = (resp.data.object.sha == "${{ github.sha }}")
91+
console.log(result)
92+
return result
93+
env:
94+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
95+
- name: Check released commit to be master branch or stable branch for pushed tag
96+
run: |
97+
if [[ ${{ steps.set-is-master-branch.outputs.result }} == 'false' && ${{ steps.set-is-stable-branch.outputs.result }} == 'false' ]]; then
98+
echo "Released commit is not 'master' or '${{ steps.set-stable-branch.outputs.result }}' branch";
99+
false;
100+
fi
101+
- name: Set update version
102+
id: set-update-version
103+
uses: actions/github-script@v8
104+
with:
105+
result-encoding: string
106+
script: |
107+
const result = "${{ steps.set-tag.outputs.result }}".match("[0-9]+\.[0-9]+\.([0-9]+)")[1]
108+
console.log(result)
109+
return result
110+
- name: Check update version to be 0 when releasing master branch
111+
if: ${{ steps.set-is-master-branch.outputs.result == 'true' }}
112+
run: |
113+
if [[ ${{ steps.set-update-version.outputs.result }} != '0' ]]; then
114+
echo "Update version '${{ steps.set-update-version.outputs.result }}' in tag '${{ steps.set-tag.outputs.result }}' is invalid (must be 0 when releasing master branch)";
115+
false;
116+
fi
117+
- name: Check update version to be non-0 when releasing stable branch for pushed tag
118+
if: ${{ steps.set-is-stable-branch.outputs.result == 'true' }}
119+
run: |
120+
if [[ ${{ steps.set-update-version.outputs.result }} == '0' ]]; then
121+
echo "Update version '${{ steps.set-update-version.outputs.result }}' in tag '${{ steps.set-tag.outputs.result }}' is invalid (must be non-0 when releasing stable branch for pushed tag)";
122+
false;
123+
fi
124+
125+
#-------- Setup of work environment
126+
- name: Checkout repo
127+
uses: actions/checkout@v5
128+
with:
129+
# fetch-depth 0 checks out all history for all branches and tags.
130+
# This is needed to get the authors from git.
131+
fetch-depth: 0
132+
- name: Set up Python
133+
uses: actions/setup-python@v6
134+
with:
135+
python-version: "3.13"
136+
- name: Development setup
137+
run: |
138+
make develop
139+
- name: Display Python packages
140+
run: |
141+
pip list
142+
143+
#-------- Publishing of package
144+
- name: Build the distribution
145+
run: |
146+
make build
147+
- name: Display the distribution directory
148+
run: |
149+
ls -l dist
150+
- name: Publish distribution to PyPI
151+
if: startsWith(github.ref, 'refs/tags')
152+
uses: pypa/gh-action-pypi-publish@release/v1
153+
with:
154+
packages_dir: dist
155+
# Pypi trusted publishing requires to have no password
156+
157+
#-------- Creation of Github release
158+
- name: Determine whether release on Github exists for the pushed tag
159+
id: set-release-exists
160+
uses: octokit/request-action@v2.x
161+
with:
162+
route: GET /repos/${{ github.repository }}/releases/tags/${{ steps.set-tag.outputs.result }}
163+
continue-on-error: true
164+
env:
165+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
166+
- name: Create release on Github for the pushed tag if it does not exist
167+
if: ${{ steps.set-release-exists.outputs.status == 404 }}
168+
uses: octokit/request-action@v2.x
169+
with:
170+
route: POST /repos/${{ github.repository }}/releases
171+
env:
172+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
173+
INPUT_TAG_NAME: ${{ steps.set-tag.outputs.result }}
174+
INPUT_NAME: "Release ${{ steps.set-tag.outputs.result }}"
175+
INPUT_BODY: "Change log https://pywbem.readthedocs.io/en/${{ steps.set-tag.outputs.result }}/changes.html"
176+
177+
#-------- Creation of stable branch
178+
# Note: This does not seem to depend on the disablement of the "Restrict pushes
179+
# that create matching branches" setting in the branch protection rules for stable_*.
180+
# It is possible that this fails with HTTP 422, for unknown reasons.
181+
- name: Create new stable branch when releasing master branch
182+
if: steps.set-is-master-branch.outputs.result == 'true'
183+
id: create-stable-branch
184+
uses: actions/github-script@v8
185+
with:
186+
script: |
187+
github.rest.git.createRef({
188+
owner: context.repo.owner,
189+
repo: context.repo.repo,
190+
ref: "refs/heads/${{ steps.set-stable-branch.outputs.result }}",
191+
sha: "${{ github.sha }}",
192+
})
193+
env:
194+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
195+
continue-on-error: true
196+
- name: Wait some time if HTTP error 422
197+
if: steps.set-is-master-branch.outputs.result == 'true' && steps.create-stable-branch.outputs.status == 422
198+
run: |
199+
sleep 10
200+
- name: Retry create new stable branch when releasing master branch if HTTP error 422
201+
if: steps.set-is-master-branch.outputs.result == 'true' && steps.create-stable-branch.outputs.status == 422
202+
id: create-stable-branch-2
203+
uses: actions/github-script@v8
204+
with:
205+
script: |
206+
github.rest.git.createRef({
207+
owner: context.repo.owner,
208+
repo: context.repo.repo,
209+
ref: "refs/heads/${{ steps.set-stable-branch.outputs.result }}",
210+
sha: "${{ github.sha }}",
211+
})
212+
env:
213+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
214+
continue-on-error: true
215+
- name: Handle HTTP error 422 from creating the new stable branch
216+
if: steps.set-is-master-branch.outputs.result == 'true' && steps.create-stable-branch-2.outputs.status == 422
217+
run: |
218+
echo "Error: Creating the new stable branch ${{ steps.set-stable-branch.outputs.result }} failed twice, but the publish still succeeded. Create the new stable branch manually:"
219+
echo "git checkout master"
220+
echo "git pull"
221+
echo "git checkout -b ${{ steps.set-stable-branch.outputs.result }}"
222+
echo "git push --set-upstream origin ${{ steps.set-stable-branch.outputs.result }}"
223+
false

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
*.log
2323
*.tmp
2424
tmp.*
25+
tmp_*
2526
*~
2627
.*~
2728
*.py,cover

Makefile

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ dist_dependent_files := \
271271
done_dir := done
272272

273273
# Packages whose dependencies are checked using pip-missing-reqs
274-
check_reqs_packages := pytest coverage coveralls flake8 pylint twine safety sphinx
274+
check_reqs_packages := pytest coverage coveralls flake8 pylint twine safety sphinx towncrier
275275

276276
.PHONY: help
277277
help:
@@ -293,8 +293,11 @@ help:
293293
@echo " doclinkcheck - Run Sphinx linkcheck on the documentation"
294294
@echo " authors - Generate AUTHORS.md file from git log"
295295
@echo " all - Do all of the above"
296+
@echo " release_branch - Create a release branch when releasing a version (requires VERSION and optionally BRANCH to be set)"
297+
@echo " release_publish - Publish to PyPI when releasing a version (requires VERSION and optionally BRANCH to be set)"
298+
@echo " start_branch - Create a start branch when starting a new version (requires VERSION and optionally BRANCH to be set)"
299+
@echo " start_tag - Create a start tag when starting a new version (requires VERSION and optionally BRANCH to be set)"
296300
@echo " install - Install $(package_name) as standalone and its dependent packages"
297-
@echo " upload - build + upload the distribution archive files to PyPI"
298301
@echo " clean - Remove any temporary files"
299302
@echo " clobber - Remove everything created to ensure clean start"
300303
@echo " pip_list - Display the installed Python packages as seen by make"
@@ -320,6 +323,8 @@ help:
320323
@echo " Optional, defaults to 'python'."
321324
@echo " PIP_CMD - Pip command to be used. Useful for Python 3 in some envs."
322325
@echo " Optional, defaults to 'pip'."
326+
@echo " VERSION=... - M.N.U version to be released or started"
327+
@echo " BRANCH=... - Name of branch to be released or started (default is derived from VERSION)"
323328

324329
.PHONY: platform
325330
platform:
@@ -395,6 +400,96 @@ mypy: $(done_dir)/mypy_$(pymn)_$(PACKAGE_LEVEL).done
395400
all: develop check_reqs build builddoc check pylint mypy installtest test doclinkcheck authors
396401
@echo "Makefile: $@ done."
397402

403+
.PHONY: release_branch
404+
release_branch:
405+
@bash -c 'if [ -z "$(VERSION)" ]; then echo ""; echo "Error: VERSION env var is not set"; echo ""; false; fi'
406+
@bash -c 'if [ -n "$$(git status -s)" ]; then echo ""; echo "Error: Local git repo has uncommitted files:"; echo ""; git status; false; fi'
407+
git fetch origin
408+
@bash -c 'if [ -z "$$(git tag -l $(VERSION)a0)" ]; then echo ""; echo "Error: Release start tag $(VERSION)a0 does not exist (the version has not been started)"; echo ""; false; fi'
409+
@bash -c 'if [ -n "$$(git tag -l $(VERSION))" ]; then echo ""; echo "Error: Release tag $(VERSION) already exists (the version has already been released)"; echo ""; false; fi'
410+
@bash -c 'if [[ -n "$${BRANCH}" ]]; then echo $${BRANCH} >branch.tmp; elif [[ "$${VERSION#*.*.}" == "0" ]]; then echo "master" >branch.tmp; else echo "stable_$${VERSION%.*}" >branch.tmp; fi'
411+
@bash -c 'if [ -z "$$(git branch --contains $(VERSION)a0 $$(cat branch.tmp))" ]; then echo ""; echo "Error: Release start tag $(VERSION)a0 is not in target branch $$(cat branch.tmp), but in:"; echo ""; git branch --contains $(VERSION)a0;. false; fi'
412+
@echo "==> This will start the release of $(package_name) version $(VERSION) to PyPI using target branch $$(cat branch.tmp)"
413+
@echo -n '==> Continue? [yN] '
414+
@bash -c 'read answer; if [ "$$answer" != "y" ]; then echo "Aborted."; false; fi'
415+
bash -c 'git checkout $$(cat branch.tmp)'
416+
git pull
417+
@bash -c 'if [ -z "$$(git branch -l release_$(VERSION))" ]; then echo "Creating release branch release_$(VERSION)"; git checkout -b release_$(VERSION); fi'
418+
git checkout release_$(VERSION)
419+
make authors
420+
towncrier build --version $(VERSION) --yes
421+
@bash -c 'if ls changes/*.rst >/dev/null 2>/dev/null; then echo ""; echo "Error: There are incorrectly named change fragment files that towncrier did not use:"; ls -1 changes/*.rst; echo ""; false; fi'
422+
git commit -asm "Release $(VERSION)"
423+
git push --set-upstream origin release_$(VERSION)
424+
rm -f branch.tmp
425+
@echo "Done: Pushed the release branch to GitHub - now go there and create a PR."
426+
@echo "Makefile: $@ done."
427+
428+
.PHONY: release_publish
429+
release_publish:
430+
@bash -c 'if [ -z "$(VERSION)" ]; then echo ""; echo "Error: VERSION env var is not set"; echo ""; false; fi'
431+
@bash -c 'if [ -n "$$(git status -s)" ]; then echo ""; echo "Error: Local git repo has uncommitted files:"; echo ""; git status; false; fi'
432+
git fetch origin
433+
@bash -c 'if [ -n "$$(git tag -l $(VERSION))" ]; then echo ""; echo "Error: Release tag $(VERSION) already exists (the version has already been released)"; echo ""; false; fi'
434+
@bash -c 'if [[ -n "$${BRANCH}" ]]; then echo $${BRANCH} >branch.tmp; elif [[ "$${VERSION#*.*.}" == "0" ]]; then echo "master" >branch.tmp; else echo "stable_$${VERSION%.*}" >branch.tmp; fi'
435+
@bash -c 'if [ "$$(git log --format=format:%s origin/$$(cat branch.tmp)~..origin/$$(cat branch.tmp))" != "Release $(VERSION)" ]; then echo ""; echo "Error: Release PR for $(VERSION) has not been merged yet"; echo ""; false; fi'
436+
@echo "==> This will publish $(package_name) version $(VERSION) to PyPI using target branch $$(cat branch.tmp)"
437+
@echo -n '==> Continue? [yN] '
438+
@bash -c 'read answer; if [ "$$answer" != "y" ]; then echo "Aborted."; false; fi'
439+
bash -c 'git checkout $$(cat branch.tmp)'
440+
git pull
441+
git tag -f $(VERSION)
442+
git push -f --tags
443+
git branch -D release_$(VERSION)
444+
git branch -D -r origin/release_$(VERSION)
445+
rm -f branch.tmp
446+
@echo "Done: Triggered the publish workflow - now wait for it to finish and verify the publishing."
447+
@echo "Makefile: $@ done."
448+
449+
.PHONY: start_branch
450+
start_branch:
451+
@bash -c 'if [ -z "$(VERSION)" ]; then echo ""; echo "Error: VERSION env var is not set"; echo ""; false; fi'
452+
@bash -c 'if [ -n "$$(git status -s)" ]; then echo ""; echo "Error: Local git repo has uncommitted files:"; echo ""; git status; false; fi'
453+
git fetch origin
454+
@bash -c 'if [ -n "$$(git tag -l $(VERSION))" ]; then echo ""; echo "Error: Release tag $(VERSION) already exists (the version has already been released)"; echo ""; false; fi'
455+
@bash -c 'if [ -n "$$(git tag -l $(VERSION)a0)" ]; then echo ""; echo "Error: Release start tag $(VERSION)a0 already exists (the new version has alreay been started)"; echo ""; false; fi'
456+
@bash -c 'if [ -n "$$(git branch -l start_$(VERSION))" ]; then echo ""; echo "Error: Start branch start_$(VERSION) already exists (the start of the new version is already underway)"; echo ""; false; fi'
457+
@bash -c 'if [[ -n "$${BRANCH}" ]]; then echo $${BRANCH} >branch.tmp; elif [[ "$${VERSION#*.*.}" == "0" ]]; then echo "master" >branch.tmp; else echo "stable_$${VERSION%.*}" >branch.tmp; fi'
458+
@echo "==> This will start new version $(VERSION) using target branch $$(cat branch.tmp)"
459+
@echo -n '==> Continue? [yN] '
460+
@bash -c 'read answer; if [ "$$answer" != "y" ]; then echo "Aborted."; false; fi'
461+
bash -c 'git checkout $$(cat branch.tmp)'
462+
git pull
463+
git checkout -b start_$(VERSION)
464+
echo "Dummy change for starting new version $(VERSION)" >changes/noissue.$(VERSION).notshown.rst
465+
git add changes/noissue.$(VERSION).notshown.rst
466+
git commit -asm "Start $(VERSION)"
467+
git push --set-upstream origin start_$(VERSION)
468+
rm -f branch.tmp
469+
@echo "Done: Pushed the start branch to GitHub - now go there and create a PR."
470+
@echo "Makefile: $@ done."
471+
472+
.PHONY: start_tag
473+
start_tag:
474+
@bash -c 'if [ -z "$(VERSION)" ]; then echo ""; echo "Error: VERSION env var is not set"; echo ""; false; fi'
475+
@bash -c 'if [ -n "$$(git status -s)" ]; then echo ""; echo "Error: Local git repo has uncommitted files:"; echo ""; git status; false; fi'
476+
git fetch origin
477+
@bash -c 'if [ -n "$$(git tag -l $(VERSION)a0)" ]; then echo ""; echo "Error: Release start tag $(VERSION)a0 already exists (the new version has alreay been started)"; echo ""; false; fi'
478+
@bash -c 'if [[ -n "$${BRANCH}" ]]; then echo $${BRANCH} >branch.tmp; elif [[ "$${VERSION#*.*.}" == "0" ]]; then echo "master" >branch.tmp; else echo "stable_$${VERSION%.*}" >branch.tmp; fi'
479+
@bash -c 'if [ "$$(git log --format=format:%s origin/$$(cat branch.tmp)~..origin/$$(cat branch.tmp))" != "Start $(VERSION)" ]; then echo ""; echo "Error: Start PR for $(VERSION) has not been merged yet"; echo ""; false; fi'
480+
@echo "==> This will complete the start of new version $(VERSION) using target branch $$(cat branch.tmp)"
481+
@echo -n '==> Continue? [yN] '
482+
@bash -c 'read answer; if [ "$$answer" != "y" ]; then echo "Aborted."; false; fi'
483+
bash -c 'git checkout $$(cat branch.tmp)'
484+
git pull
485+
git tag -f $(VERSION)a0
486+
git push -f --tags
487+
git branch -D start_$(VERSION)
488+
git branch -D -r origin/start_$(VERSION)
489+
rm -f branch.tmp
490+
@echo "Done: Pushed the release start tag and cleaned up the release start branch."
491+
@echo "Makefile: $@ done."
492+
398493
.PHONY: clobber
399494
clobber: clean
400495
@echo "Makefile: Removing everything for a fresh start"
@@ -416,15 +511,6 @@ clean:
416511
@echo "Makefile: Done removing temporary build products"
417512
@echo "Makefile: $@ done."
418513

419-
.PHONY: upload
420-
upload: $(dist_files)
421-
@echo "Makefile: Checking files before uploading to PyPI"
422-
twine check $(dist_files)
423-
@echo "Makefile: Uploading to PyPI: $(package_name) $(package_version)"
424-
twine upload $(dist_files)
425-
@echo "Makefile: Done uploading to PyPI"
426-
@echo "Makefile: $@ done."
427-
428514
$(done_dir)/base_$(pymn)_$(PACKAGE_LEVEL).done: Makefile base-requirements.txt minimum-constraints-develop.txt minimum-constraints-install.txt
429515
-$(call RM_FUNC,$@)
430516
@echo "Makefile: Installing/upgrading base packages with PACKAGE_LEVEL=$(PACKAGE_LEVEL)"
@@ -480,7 +566,7 @@ docchanges: $(done_dir)/develop_$(pymn)_$(PACKAGE_LEVEL).done
480566
.PHONY: doclinkcheck
481567
doclinkcheck: $(done_dir)/develop_$(pymn)_$(PACKAGE_LEVEL).done
482568
@echo "Makefile: Creating the doc link errors file"
483-
$(doc_cmd) -b linkcheck $(doc_opts) $(doc_build_dir)/linkcheck
569+
-$(doc_cmd) -b linkcheck $(doc_opts) $(doc_build_dir)/linkcheck
484570
@echo
485571
@echo "Makefile: Done creating the doc link errors file: $(doc_build_dir)/linkcheck/output.txt"
486572
@echo "Makefile: $@ done."

changes/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Keep this directory in git even if empty
2+
!.gitignore

0 commit comments

Comments
 (0)