Skip to content

Commit f239a21

Browse files
Add some release automation (#256)
* Add release notes for release and patch versions * Add a workflow to check for + update Hugo versions * Add machinery to make releases easier
1 parent 1a1f842 commit f239a21

5 files changed

Lines changed: 304 additions & 0 deletions

File tree

.github/update-hugo.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Update Hugo to v${LATEST_VERSION}
2+
3+
This is an automated PR to update Hugo from v${CURRENT_VERSION} to v${LATEST_VERSION} (${RELEASE_TYPE} release).
4+
5+
### Changes
6+
7+
- Hugo version updated from ${CURRENT_VERSION} to ${LATEST_VERSION}
8+
${GO_UPDATE_NOTE}
9+
10+
### Hugo release notes
11+
12+
https://github.com/gohugoio/hugo/releases/tag/v${LATEST_VERSION}
13+
14+
### Release checklist
15+
16+
- [ ] Merge this PR
17+
- [ ] Run `nox -s tag -- ${LATEST_VERSION}` locally to create a signed tag
18+
- [ ] Push the tag: `git push origin v${LATEST_VERSION}`
19+
- [ ] Run `nox -s release -- ${LATEST_VERSION}` to create the GitHub release (or create it manually)

.github/workflows/update-hugo.yml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
name: Update Hugo version
2+
3+
on:
4+
schedule:
5+
# Check for new Hugo releases every six hours
6+
- cron: "0 */6 * * *"
7+
workflow_dispatch:
8+
9+
permissions: {}
10+
11+
jobs:
12+
check-and-update:
13+
name: Check for new Hugo release
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: write # to update files and push branches
17+
pull-requests: write # to create PR
18+
steps:
19+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
20+
with:
21+
persist-credentials: false
22+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
23+
- name: Check for new Hugo release, and apply updates
24+
id: check-hugo-release
25+
env:
26+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
run: |
28+
set -euo pipefail
29+
30+
CURRENT_VERSION=$(grep -oP 'HUGO_VERSION = "\K[0-9.]+' setup.py)
31+
echo "Current version: $CURRENT_VERSION"
32+
33+
# Find the next Hugo release after our current version
34+
NEXT_VERSION=$(gh api repos/gohugoio/hugo/releases --paginate --jq '.[].tag_name' | \
35+
sed 's/^v//' | sort -V | awk -v cur="$CURRENT_VERSION" 'found {print; exit} $0 == cur {found=1}')
36+
37+
if [ -z "$NEXT_VERSION" ]; then
38+
echo "Already up to date (v$CURRENT_VERSION)"
39+
echo "updated=false" >> "$GITHUB_OUTPUT"
40+
exit 0
41+
fi
42+
43+
LATEST_VERSION="$NEXT_VERSION"
44+
echo "Next version to update to: $LATEST_VERSION"
45+
46+
# Check if a PR already exists for this version, and skip if it does
47+
EXISTING_PR=$(gh pr list --search "Update Hugo to v$LATEST_VERSION" --state open --json number --jq '.[0].number // empty')
48+
if [ -n "$EXISTING_PR" ]; then
49+
echo "PR #$EXISTING_PR already exists for v$LATEST_VERSION"
50+
echo "updated=false" >> "$GITHUB_OUTPUT"
51+
exit 0
52+
fi
53+
54+
# Resolve Go version from Hugo's go.mod, and find the latest patch release
55+
GO_MOD_VERSION=$(gh api "repos/gohugoio/hugo/contents/go.mod?ref=v$LATEST_VERSION" --jq '.content' | base64 -d | grep '^go ' | awk '{print $2}')
56+
echo "Hugo v$LATEST_VERSION uses Go $GO_MOD_VERSION"
57+
58+
GO_MAJOR_MINOR=$(echo "$GO_MOD_VERSION" | grep -oP '^\d+\.\d+')
59+
GO_LATEST=$(curl -sL "https://go.dev/dl/?mode=json" | python3 -c "
60+
import json, sys
61+
releases = json.load(sys.stdin)
62+
prefix = 'go${GO_MAJOR_MINOR}'
63+
for r in releases:
64+
if r['version'].startswith(prefix):
65+
print(r['version'].removeprefix('go'))
66+
break
67+
else:
68+
print('${GO_MOD_VERSION}')
69+
")
70+
echo "Latest Go toolchain: $GO_LATEST"
71+
72+
CURRENT_GO=$(grep -oP 'go-version: "\K[0-9.]+' .github/workflows/ci.yml | head -1)
73+
echo "Current Go version in workflows: $CURRENT_GO"
74+
75+
CURRENT_MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2)
76+
LATEST_MINOR=$(echo "$LATEST_VERSION" | cut -d. -f2)
77+
78+
if [ "$CURRENT_MINOR" != "$LATEST_MINOR" ]; then
79+
RELEASE_TYPE="minor"
80+
else
81+
RELEASE_TYPE="patch"
82+
fi
83+
84+
sed -i "s/HUGO_VERSION = \"$CURRENT_VERSION\"/HUGO_VERSION = \"$LATEST_VERSION\"/" setup.py
85+
sed -i "s/HUGO_VERSION = \"$CURRENT_VERSION\"/HUGO_VERSION = \"$LATEST_VERSION\"/" hugo/cli.py
86+
87+
GO_UPDATE_NOTE=""
88+
if [ "$CURRENT_GO" != "$GO_LATEST" ]; then
89+
sed -i "s/go-version: \"$CURRENT_GO\"/go-version: \"$GO_LATEST\"/g" .github/workflows/ci.yml
90+
sed -i "s/go-version: \"$CURRENT_GO\"/go-version: \"$GO_LATEST\"/g" .github/workflows/cd.yml
91+
sed -i "s/go${CURRENT_GO}\./go${GO_LATEST}./g" .github/workflows/cd.yml
92+
GO_UPDATE_NOTE=$'\n'"- Go toolchain updated from $CURRENT_GO to $GO_LATEST"
93+
fi
94+
95+
echo "updated=true" >> "$GITHUB_OUTPUT"
96+
echo "latest_version=$LATEST_VERSION" >> "$GITHUB_OUTPUT"
97+
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
98+
echo "release_type=$RELEASE_TYPE" >> "$GITHUB_OUTPUT"
99+
100+
export CURRENT_VERSION LATEST_VERSION RELEASE_TYPE GO_UPDATE_NOTE
101+
envsubst < .github/update-hugo.md > /tmp/pr-body.md
102+
103+
- name: Create pull request
104+
if: steps.check-hugo-release.outputs.updated == 'true'
105+
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
106+
with:
107+
token: ${{ secrets.GITHUB_TOKEN }}
108+
branch: update-hugo-v${{ steps.check-hugo-release.outputs.latest_version }}
109+
commit-message: "Update Hugo to v${{ steps.check-hugo-release.outputs.latest_version }}"
110+
title: "Update Hugo to v${{ steps.check-hugo-release.outputs.latest_version }}"
111+
body-path: /tmp/pr-body.md

noxfile.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from __future__ import annotations
22

3+
import re
4+
import string
5+
import subprocess
36
from pathlib import Path
47

58
import nox
69

710
DIR = Path(__file__).parent.resolve()
11+
REPO = "agriyakhetarpal/hugo-python-distributions"
812

913
nox.options.sessions = ["lint", "tests"]
1014

@@ -29,3 +33,129 @@ def venv(session: nox.Session) -> None:
2933
session.install(file)
3034
session.run("hugo", "version")
3135
session.run("hugo", "env", "--logLevel", "debug")
36+
37+
38+
def _get_version(session: nox.Session) -> str:
39+
"""Extract version from session posargs or setup.py."""
40+
if session.posargs:
41+
return session.posargs[0].lstrip("v")
42+
content = (DIR / "setup.py").read_text()
43+
match = re.search(r'HUGO_VERSION = "([0-9.]+)"', content)
44+
if not match:
45+
session.error("Could not determine version. Pass it as: nox -s tag -- 0.157.0")
46+
return match.group(1)
47+
48+
49+
def _get_previous_tag(current_tag: str) -> str:
50+
result = subprocess.run(
51+
["git", "tag", "--sort=-v:refname"],
52+
capture_output=True,
53+
text=True,
54+
check=True,
55+
)
56+
tags = result.stdout.strip().splitlines()
57+
for i, t in enumerate(tags):
58+
if t == current_tag and i + 1 < len(tags):
59+
return tags[i + 1]
60+
result = subprocess.run(
61+
["git", "describe", "--tags", "--abbrev=0", f"{current_tag}^"],
62+
capture_output=True,
63+
text=True,
64+
check=True,
65+
)
66+
return result.stdout.strip()
67+
68+
69+
@nox.session(default=False)
70+
def tag(session: nox.Session) -> None:
71+
"""Create a signed, annotated tag for a release.
72+
73+
Usage: nox -s tag -- 0.157.0
74+
"""
75+
version = _get_version(session)
76+
tag_name = f"v{version}"
77+
tag_message = f"hugo-python-distributions, version {version}"
78+
79+
result = subprocess.run(
80+
["git", "tag", "-l", tag_name], capture_output=True, text=True, check=False
81+
)
82+
if tag_name in result.stdout:
83+
session.error(f"Tag {tag_name} already exists")
84+
85+
branch = subprocess.run(
86+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
87+
capture_output=True,
88+
text=True,
89+
check=True,
90+
).stdout.strip()
91+
if branch != "main":
92+
session.warn(f"You are on branch '{branch}', not 'main'. Proceed with caution.")
93+
94+
session.log(f"Creating signed tag {tag_name}: {tag_message}")
95+
session.run(
96+
"git",
97+
"tag",
98+
"-s",
99+
"-a",
100+
tag_name,
101+
"-m",
102+
tag_message,
103+
external=True,
104+
)
105+
session.log(f"Tag {tag_name} created. Push it with: git push origin {tag_name}")
106+
107+
108+
@nox.session(default=False)
109+
def release(session: nox.Session) -> None:
110+
"""Create a GitHub release with formatted release notes.
111+
112+
Usage: nox -s release -- 0.157.0
113+
"""
114+
version = _get_version(session)
115+
tag_name = f"v{version}"
116+
117+
previous_tag = _get_previous_tag(tag_name)
118+
is_patch = int(version.split(".")[2]) > 0
119+
template_name = "patch.md" if is_patch else "stable.md"
120+
template_path = DIR / "release_notes" / template_name
121+
template = string.Template(template_path.read_text())
122+
123+
commit_log = subprocess.run(
124+
[
125+
"git",
126+
"log",
127+
f"{previous_tag}...HEAD",
128+
"--pretty=format:- %s by @%an in %H",
129+
"--no-merges",
130+
],
131+
capture_output=True,
132+
text=True,
133+
check=True,
134+
).stdout.strip()
135+
136+
if commit_log:
137+
changes_section = f"\n## Changes that made it to this release\n\n{commit_log}\n"
138+
else:
139+
changes_section = ""
140+
141+
substitutions = {
142+
"VERSION": version,
143+
"PREVIOUS_TAG": previous_tag,
144+
"CHANGES_SECTION": changes_section,
145+
}
146+
147+
body = template.substitute(substitutions)
148+
149+
session.log(f"Creating GitHub release for {tag_name}")
150+
session.run(
151+
"gh",
152+
"release",
153+
"create",
154+
tag_name,
155+
"--title",
156+
tag_name,
157+
"--notes",
158+
body,
159+
external=True,
160+
)
161+
session.log(f"Release {tag_name} created!")

release_notes/patch.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## hugo-python-distributions (`Hugo`) v${VERSION}
2+
3+
This release mirrors the [official v${VERSION} patch release for Hugo](https://github.com/gohugoio/hugo/releases/tag/v${VERSION}). For the changes incorporated into Hugo with this release, please refer to the [release notes](https://github.com/gohugoio/hugo/releases/tag/v${VERSION}) for Hugo v${VERSION}.
4+
5+
The release can be installed and used with any of the following commands:
6+
7+
```bash
8+
# either install it
9+
pip install hugo
10+
pipx install hugo
11+
uv pip install hugo
12+
uv tool install hugo
13+
14+
# or run it directly
15+
pipx run hugo
16+
uv run hugo
17+
uvx hugo
18+
```
19+
20+
on Linux (amd64, aarch64, s390x, ppc64le), macOS (amd64, arm64), and Windows (amd64, arm64, i686).
21+
${CHANGES_SECTION}
22+
**Full range of commits**: https://github.com/agriyakhetarpal/hugo-python-distributions/compare/${PREVIOUS_TAG}...v${VERSION}

release_notes/stable.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## hugo-python-distributions (`Hugo`) v${VERSION}
2+
3+
This release mirrors the [official v${VERSION} release for Hugo](https://github.com/gohugoio/hugo/releases/tag/v${VERSION}). For the changes incorporated into Hugo with this release, please refer to the [release notes](https://github.com/gohugoio/hugo/releases/tag/v${VERSION}) for Hugo v${VERSION}.
4+
5+
The release can be installed and used with any of the following commands:
6+
7+
```bash
8+
# either install it
9+
pip install hugo
10+
pipx install hugo
11+
uv pip install hugo
12+
uv tool install hugo
13+
14+
# or run it directly
15+
pipx run hugo
16+
uv run hugo
17+
uvx hugo
18+
```
19+
20+
on Linux (amd64, aarch64, s390x, ppc64le), macOS (amd64, arm64), and Windows (amd64, arm64, i686).
21+
${CHANGES_SECTION}
22+
**Full range of commits**: https://github.com/agriyakhetarpal/hugo-python-distributions/compare/${PREVIOUS_TAG}...v${VERSION}

0 commit comments

Comments
 (0)