diff --git a/.github/update-hugo.md b/.github/update-hugo.md new file mode 100644 index 0000000..e6c066c --- /dev/null +++ b/.github/update-hugo.md @@ -0,0 +1,19 @@ +## Update Hugo to v${LATEST_VERSION} + +This is an automated PR to update Hugo from v${CURRENT_VERSION} to v${LATEST_VERSION} (${RELEASE_TYPE} release). + +### Changes + +- Hugo version updated from ${CURRENT_VERSION} to ${LATEST_VERSION} +${GO_UPDATE_NOTE} + +### Hugo release notes + +https://github.com/gohugoio/hugo/releases/tag/v${LATEST_VERSION} + +### Release checklist + +- [ ] Merge this PR +- [ ] Run `nox -s tag -- ${LATEST_VERSION}` locally to create a signed tag +- [ ] Push the tag: `git push origin v${LATEST_VERSION}` +- [ ] Run `nox -s release -- ${LATEST_VERSION}` to create the GitHub release (or create it manually) diff --git a/.github/workflows/update-hugo.yml b/.github/workflows/update-hugo.yml new file mode 100644 index 0000000..6b2002e --- /dev/null +++ b/.github/workflows/update-hugo.yml @@ -0,0 +1,111 @@ +name: Update Hugo version + +on: + schedule: + # Check for new Hugo releases every six hours + - cron: "0 */6 * * *" + workflow_dispatch: + +permissions: {} + +jobs: + check-and-update: + name: Check for new Hugo release + runs-on: ubuntu-latest + permissions: + contents: write # to update files and push branches + pull-requests: write # to create PR + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - name: Check for new Hugo release, and apply updates + id: check-hugo-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + CURRENT_VERSION=$(grep -oP 'HUGO_VERSION = "\K[0-9.]+' setup.py) + echo "Current version: $CURRENT_VERSION" + + # Find the next Hugo release after our current version + NEXT_VERSION=$(gh api repos/gohugoio/hugo/releases --paginate --jq '.[].tag_name' | \ + sed 's/^v//' | sort -V | awk -v cur="$CURRENT_VERSION" 'found {print; exit} $0 == cur {found=1}') + + if [ -z "$NEXT_VERSION" ]; then + echo "Already up to date (v$CURRENT_VERSION)" + echo "updated=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + LATEST_VERSION="$NEXT_VERSION" + echo "Next version to update to: $LATEST_VERSION" + + # Check if a PR already exists for this version, and skip if it does + EXISTING_PR=$(gh pr list --search "Update Hugo to v$LATEST_VERSION" --state open --json number --jq '.[0].number // empty') + if [ -n "$EXISTING_PR" ]; then + echo "PR #$EXISTING_PR already exists for v$LATEST_VERSION" + echo "updated=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Resolve Go version from Hugo's go.mod, and find the latest patch release + GO_MOD_VERSION=$(gh api "repos/gohugoio/hugo/contents/go.mod?ref=v$LATEST_VERSION" --jq '.content' | base64 -d | grep '^go ' | awk '{print $2}') + echo "Hugo v$LATEST_VERSION uses Go $GO_MOD_VERSION" + + GO_MAJOR_MINOR=$(echo "$GO_MOD_VERSION" | grep -oP '^\d+\.\d+') + GO_LATEST=$(curl -sL "https://go.dev/dl/?mode=json" | python3 -c " + import json, sys + releases = json.load(sys.stdin) + prefix = 'go${GO_MAJOR_MINOR}' + for r in releases: + if r['version'].startswith(prefix): + print(r['version'].removeprefix('go')) + break + else: + print('${GO_MOD_VERSION}') + ") + echo "Latest Go toolchain: $GO_LATEST" + + CURRENT_GO=$(grep -oP 'go-version: "\K[0-9.]+' .github/workflows/ci.yml | head -1) + echo "Current Go version in workflows: $CURRENT_GO" + + CURRENT_MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2) + LATEST_MINOR=$(echo "$LATEST_VERSION" | cut -d. -f2) + + if [ "$CURRENT_MINOR" != "$LATEST_MINOR" ]; then + RELEASE_TYPE="minor" + else + RELEASE_TYPE="patch" + fi + + sed -i "s/HUGO_VERSION = \"$CURRENT_VERSION\"/HUGO_VERSION = \"$LATEST_VERSION\"/" setup.py + sed -i "s/HUGO_VERSION = \"$CURRENT_VERSION\"/HUGO_VERSION = \"$LATEST_VERSION\"/" hugo/cli.py + + GO_UPDATE_NOTE="" + if [ "$CURRENT_GO" != "$GO_LATEST" ]; then + sed -i "s/go-version: \"$CURRENT_GO\"/go-version: \"$GO_LATEST\"/g" .github/workflows/ci.yml + sed -i "s/go-version: \"$CURRENT_GO\"/go-version: \"$GO_LATEST\"/g" .github/workflows/cd.yml + sed -i "s/go${CURRENT_GO}\./go${GO_LATEST}./g" .github/workflows/cd.yml + GO_UPDATE_NOTE=$'\n'"- Go toolchain updated from $CURRENT_GO to $GO_LATEST" + fi + + echo "updated=true" >> "$GITHUB_OUTPUT" + echo "latest_version=$LATEST_VERSION" >> "$GITHUB_OUTPUT" + echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" + echo "release_type=$RELEASE_TYPE" >> "$GITHUB_OUTPUT" + + export CURRENT_VERSION LATEST_VERSION RELEASE_TYPE GO_UPDATE_NOTE + envsubst < .github/update-hugo.md > /tmp/pr-body.md + + - name: Create pull request + if: steps.check-hugo-release.outputs.updated == 'true' + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: update-hugo-v${{ steps.check-hugo-release.outputs.latest_version }} + commit-message: "Update Hugo to v${{ steps.check-hugo-release.outputs.latest_version }}" + title: "Update Hugo to v${{ steps.check-hugo-release.outputs.latest_version }}" + body-path: /tmp/pr-body.md diff --git a/noxfile.py b/noxfile.py index 598245e..e52ec98 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,10 +1,14 @@ from __future__ import annotations +import re +import string +import subprocess from pathlib import Path import nox DIR = Path(__file__).parent.resolve() +REPO = "agriyakhetarpal/hugo-python-distributions" nox.options.sessions = ["lint", "tests"] @@ -29,3 +33,129 @@ def venv(session: nox.Session) -> None: session.install(file) session.run("hugo", "version") session.run("hugo", "env", "--logLevel", "debug") + + +def _get_version(session: nox.Session) -> str: + """Extract version from session posargs or setup.py.""" + if session.posargs: + return session.posargs[0].lstrip("v") + content = (DIR / "setup.py").read_text() + match = re.search(r'HUGO_VERSION = "([0-9.]+)"', content) + if not match: + session.error("Could not determine version. Pass it as: nox -s tag -- 0.157.0") + return match.group(1) + + +def _get_previous_tag(current_tag: str) -> str: + result = subprocess.run( + ["git", "tag", "--sort=-v:refname"], + capture_output=True, + text=True, + check=True, + ) + tags = result.stdout.strip().splitlines() + for i, t in enumerate(tags): + if t == current_tag and i + 1 < len(tags): + return tags[i + 1] + result = subprocess.run( + ["git", "describe", "--tags", "--abbrev=0", f"{current_tag}^"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + + +@nox.session(default=False) +def tag(session: nox.Session) -> None: + """Create a signed, annotated tag for a release. + + Usage: nox -s tag -- 0.157.0 + """ + version = _get_version(session) + tag_name = f"v{version}" + tag_message = f"hugo-python-distributions, version {version}" + + result = subprocess.run( + ["git", "tag", "-l", tag_name], capture_output=True, text=True, check=False + ) + if tag_name in result.stdout: + session.error(f"Tag {tag_name} already exists") + + branch = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + if branch != "main": + session.warn(f"You are on branch '{branch}', not 'main'. Proceed with caution.") + + session.log(f"Creating signed tag {tag_name}: {tag_message}") + session.run( + "git", + "tag", + "-s", + "-a", + tag_name, + "-m", + tag_message, + external=True, + ) + session.log(f"Tag {tag_name} created. Push it with: git push origin {tag_name}") + + +@nox.session(default=False) +def release(session: nox.Session) -> None: + """Create a GitHub release with formatted release notes. + + Usage: nox -s release -- 0.157.0 + """ + version = _get_version(session) + tag_name = f"v{version}" + + previous_tag = _get_previous_tag(tag_name) + is_patch = int(version.split(".")[2]) > 0 + template_name = "patch.md" if is_patch else "stable.md" + template_path = DIR / "release_notes" / template_name + template = string.Template(template_path.read_text()) + + commit_log = subprocess.run( + [ + "git", + "log", + f"{previous_tag}...HEAD", + "--pretty=format:- %s by @%an in %H", + "--no-merges", + ], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + + if commit_log: + changes_section = f"\n## Changes that made it to this release\n\n{commit_log}\n" + else: + changes_section = "" + + substitutions = { + "VERSION": version, + "PREVIOUS_TAG": previous_tag, + "CHANGES_SECTION": changes_section, + } + + body = template.substitute(substitutions) + + session.log(f"Creating GitHub release for {tag_name}") + session.run( + "gh", + "release", + "create", + tag_name, + "--title", + tag_name, + "--notes", + body, + external=True, + ) + session.log(f"Release {tag_name} created!") diff --git a/release_notes/patch.md b/release_notes/patch.md new file mode 100644 index 0000000..ee5e391 --- /dev/null +++ b/release_notes/patch.md @@ -0,0 +1,22 @@ +## hugo-python-distributions (`Hugo`) v${VERSION} + +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}. + +The release can be installed and used with any of the following commands: + +```bash +# either install it +pip install hugo +pipx install hugo +uv pip install hugo +uv tool install hugo + +# or run it directly +pipx run hugo +uv run hugo +uvx hugo +``` + +on Linux (amd64, aarch64, s390x, ppc64le), macOS (amd64, arm64), and Windows (amd64, arm64, i686). +${CHANGES_SECTION} +**Full range of commits**: https://github.com/agriyakhetarpal/hugo-python-distributions/compare/${PREVIOUS_TAG}...v${VERSION} diff --git a/release_notes/stable.md b/release_notes/stable.md new file mode 100644 index 0000000..92c2788 --- /dev/null +++ b/release_notes/stable.md @@ -0,0 +1,22 @@ +## hugo-python-distributions (`Hugo`) v${VERSION} + +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}. + +The release can be installed and used with any of the following commands: + +```bash +# either install it +pip install hugo +pipx install hugo +uv pip install hugo +uv tool install hugo + +# or run it directly +pipx run hugo +uv run hugo +uvx hugo +``` + +on Linux (amd64, aarch64, s390x, ppc64le), macOS (amd64, arm64), and Windows (amd64, arm64, i686). +${CHANGES_SECTION} +**Full range of commits**: https://github.com/agriyakhetarpal/hugo-python-distributions/compare/${PREVIOUS_TAG}...v${VERSION}