diff --git a/.github/scripts/bump_version.py b/.github/scripts/bump_version.py index 6197aff..341fd5c 100644 --- a/.github/scripts/bump_version.py +++ b/.github/scripts/bump_version.py @@ -31,6 +31,12 @@ def bump(version: str) -> None: r'(?m)(^name = "sbx"\nversion = ")[^"]+("$)', rf"\g<1>{version}\2", ) + if ".dev" not in version: + replace_once( + ROOT / "README.md", + r"git\+https://github\.com/nueces/sbx\.git@v[^\s]+", + f"git+https://github.com/nueces/sbx.git@v{version}", + ) if __name__ == "__main__": diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 6cd08f2..4eb1327 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,8 +1,8 @@ name: Publish release -# Publishes merged release/v* PRs by creating the git tag, GitHub release, and next dev-bump PR. +# Publishes merged release/v* PRs by creating the git tag, GitHub release, website PR, and next dev-bump PR. # Previous: release-pr-checks.yml validates the release PR. -# Next: merge the dev-bump PR manually. +# Next: merge the website and dev-bump PRs manually. on: pull_request: @@ -37,12 +37,17 @@ jobs: git fetch origin main --tags changed="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")" - bad_files="$(echo "$changed" | grep -Ev '^(pyproject.toml|src/sbx/__init__.py|uv.lock)$' || true)" + bad_files="$(echo "$changed" | grep -Ev '^(README.md|pyproject.toml|src/sbx/__init__.py|uv.lock)$' || true)" if [ -n "$bad_files" ]; then echo "release PR contains unexpected files:" >&2 echo "$bad_files" >&2 exit 1 fi + counts="$(git diff --numstat "$BASE_SHA" "$HEAD_SHA" | awk '{ added += $1; deleted += $2 } END { print added+0 " " deleted+0 }')" + if [ "$counts" != "4 4" ]; then + echo "release PR should be exactly 4 insertions and 4 deletions; got $counts" >&2 + exit 1 + fi bad_init="$(git diff --unified=0 "$BASE_SHA" "$HEAD_SHA" -- src/sbx/__init__.py | grep -E '^[+-][^+-]' | grep -Ev '^[+-]__version__ = "[0-9]+\.[0-9]+\.[0-9]+"$' || true)" if [ -n "$bad_init" ]; then @@ -58,6 +63,13 @@ jobs: exit 1 fi + bad_readme="$(git diff --unified=0 "$BASE_SHA" "$HEAD_SHA" -- README.md | grep -E '^[+-][^+-]' | grep -Ev '^[+-]uv tool install git\+https://github\.com/nueces/sbx\.git@v[0-9]+\.[0-9]+\.[0-9]+$' || true)" + if [ -n "$bad_readme" ]; then + echo "README.md may only change the install tag" >&2 + echo "$bad_readme" >&2 + exit 1 + fi + grep -qx "version = \"$version\"" pyproject.toml grep -qx "__version__ = \"$version\"" src/sbx/__init__.py python .github/scripts/bump_version.py "$version" @@ -67,6 +79,7 @@ jobs: echo "tag already exists: $tag" >&2 exit 1 fi + echo "RELEASE_VERSION=$version" >> "$GITHUB_ENV" echo "RELEASE_TAG=$tag" >> "$GITHUB_ENV" - name: Create GitHub release @@ -75,6 +88,30 @@ jobs: git push origin "$RELEASE_TAG" gh release create "$RELEASE_TAG" --title "$RELEASE_TAG" --generate-notes + - name: Open website release PR + run: | + set -euo pipefail + token="${RELEASE_PR_TOKEN:-$GH_TOKEN}" + export GH_TOKEN="$token" + webpage_branch="webpage/release-$RELEASE_TAG" + + if git ls-remote --exit-code --heads origin "$webpage_branch" >/dev/null 2>&1; then + echo "branch already exists: $webpage_branch" + exit 0 + fi + + git clone --branch webpage/main --single-branch "https://x-access-token:${token}@github.com/${GITHUB_REPOSITORY}.git" webpage-release + cd webpage-release + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -c "$webpage_branch" + python ../.github/scripts/bump_webpage_version.py "$RELEASE_VERSION" index.html + git add index.html + git diff --cached --quiet && { echo "webpage already matches $RELEASE_VERSION"; exit 0; } + git commit -m "Update website for $RELEASE_TAG" + git push origin "$webpage_branch" + gh pr create --repo "$GITHUB_REPOSITORY" --base webpage/main --head "$webpage_branch" --title "Update website for $RELEASE_TAG" --body "Updates the website after $RELEASE_TAG was published." + - name: Open next dev version PR run: | set -euo pipefail @@ -98,6 +135,11 @@ jobs: git switch -c "$dev_branch" origin/main python .github/scripts/bump_version.py "$dev_version" git diff --quiet && { echo "main already at $dev_version"; exit 0; } + counts="$(git diff --numstat | awk '{ added += $1; deleted += $2 } END { print added+0 " " deleted+0 }')" + if [ "$counts" != "3 3" ]; then + echo "dev-bump PR should be exactly 3 insertions and 3 deletions; got $counts" >&2 + exit 1 + fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" diff --git a/.github/workflows/release-pr-checks.yml b/.github/workflows/release-pr-checks.yml index 026a5a5..e1faff3 100644 --- a/.github/workflows/release-pr-checks.yml +++ b/.github/workflows/release-pr-checks.yml @@ -29,12 +29,17 @@ jobs: git fetch origin main --tags version="${HEAD_REF#release/v}" changed="$(git diff --name-only "$BASE_REF...HEAD")" - bad_files="$(echo "$changed" | grep -Ev '^(pyproject.toml|src/sbx/__init__.py|uv.lock)$' || true)" + bad_files="$(echo "$changed" | grep -Ev '^(README.md|pyproject.toml|src/sbx/__init__.py|uv.lock)$' || true)" if [ -n "$bad_files" ]; then echo "release PR contains unexpected files:" >&2 echo "$bad_files" >&2 exit 1 fi + counts="$(git diff --numstat "$BASE_REF...HEAD" | awk '{ added += $1; deleted += $2 } END { print added+0 " " deleted+0 }')" + if [ "$counts" != "4 4" ]; then + echo "release PR should be exactly 4 insertions and 4 deletions; got $counts" >&2 + exit 1 + fi bad_init="$(git diff --unified=0 "$BASE_REF...HEAD" -- src/sbx/__init__.py | grep -E '^[+-][^+-]' | grep -Ev '^[+-]__version__ = "[0-9]+\.[0-9]+\.[0-9]+"$' || true)" if [ -n "$bad_init" ]; then @@ -50,6 +55,13 @@ jobs: exit 1 fi + bad_readme="$(git diff --unified=0 "$BASE_REF...HEAD" -- README.md | grep -E '^[+-][^+-]' | grep -Ev '^[+-]uv tool install git\+https://github\.com/nueces/sbx\.git@v[0-9]+\.[0-9]+\.[0-9]+$' || true)" + if [ -n "$bad_readme" ]; then + echo "README.md may only change the install tag" >&2 + echo "$bad_readme" >&2 + exit 1 + fi + grep -qx "version = \"$version\"" pyproject.toml grep -qx "__version__ = \"$version\"" src/sbx/__init__.py python .github/scripts/bump_version.py "$version" diff --git a/.github/workflows/start-release.yml b/.github/workflows/start-release.yml index afea949..6ba1571 100644 --- a/.github/workflows/start-release.yml +++ b/.github/workflows/start-release.yml @@ -1,6 +1,6 @@ name: Start release -# Manual release entry point: opens the package release PR and matching website PR. +# Manual release entry point: opens the package release PR. # Previous: none. # Next: release-pr-checks.yml runs on the opened release PR. @@ -44,7 +44,6 @@ jobs: fi release_tag="v$version" release_branch="release/$release_tag" - webpage_branch="webpage/release-$release_tag" if git rev-parse -q --verify "refs/tags/$release_tag" >/dev/null; then echo "tag already exists: $release_tag" >&2 @@ -54,11 +53,6 @@ jobs: echo "branch already exists: $release_branch" >&2 exit 1 fi - if git ls-remote --exit-code --heads origin "$webpage_branch" >/dev/null 2>&1; then - echo "branch already exists: $webpage_branch" >&2 - exit 1 - fi - echo "Previous release: ${previous_tag:-none}" echo "Current release: $release_tag" echo "Previous release: ${previous_tag:-none}" >> "$GITHUB_STEP_SUMMARY" @@ -66,7 +60,6 @@ jobs: echo "RELEASE_VERSION=$version" >> "$GITHUB_ENV" echo "RELEASE_TAG=$release_tag" >> "$GITHUB_ENV" echo "RELEASE_BRANCH=$release_branch" >> "$GITHUB_ENV" - echo "WEBPAGE_BRANCH=$webpage_branch" >> "$GITHUB_ENV" - name: Create main release PR run: | @@ -74,26 +67,16 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git switch -c "$RELEASE_BRANCH" python .github/scripts/bump_version.py "$RELEASE_VERSION" - git add pyproject.toml src/sbx/__init__.py uv.lock + git add pyproject.toml src/sbx/__init__.py uv.lock README.md git diff --cached --quiet && { echo "version files already match $RELEASE_VERSION" >&2; exit 1; } + counts="$(git diff --cached --numstat | awk '{ added += $1; deleted += $2 } END { print added+0 " " deleted+0 }')" + if [ "$counts" != "4 4" ]; then + echo "release PR should be exactly 4 insertions and 4 deletions; got $counts" >&2 + exit 1 + fi git commit -m "Release $RELEASE_TAG" git push origin "$RELEASE_BRANCH" pr_url="$(gh pr create --base main --head "$RELEASE_BRANCH" --title "Release $RELEASE_TAG" --body "Bumps package files for $RELEASE_TAG.")" pr_number="${pr_url##*/}" echo "RELEASE_PR=$pr_number" >> "$GITHUB_ENV" echo "Main release PR: $pr_url" >> "$GITHUB_STEP_SUMMARY" - - - name: Create webpage release PR - run: | - git clone --branch webpage/main --single-branch "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" webpage-release - cd webpage-release - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git switch -c "$WEBPAGE_BRANCH" - python ../.github/scripts/bump_webpage_version.py "$RELEASE_VERSION" index.html - git add index.html - git diff --cached --quiet && { echo "webpage already matches $RELEASE_VERSION" >&2; exit 1; } - git commit -m "Update website for $RELEASE_TAG" - git push origin "$WEBPAGE_BRANCH" - pr_url="$(gh pr create --repo "$GITHUB_REPOSITORY" --base webpage/main --head "$WEBPAGE_BRANCH" --title "Update website for $RELEASE_TAG" --body "Depends on #${RELEASE_PR}. Merge after $RELEASE_TAG exists.")" - echo "Webpage PR: $pr_url" >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index bc968d9..00bca44 100644 --- a/README.md +++ b/README.md @@ -359,9 +359,9 @@ Leave `version` blank to use the next patch version. Release workflows: -- `.github/workflows/start-release.yml` (`Start release`): manual entry point; opens the package release PR and matching website PR. +- `.github/workflows/start-release.yml` (`Start release`): manual entry point; opens the package release PR, including the README install tag. - `.github/workflows/release-pr-checks.yml` (`Release PR checks`): validates `release/v*` PRs only change version files. -- `.github/workflows/publish-release.yml` (`Publish release`): after the release PR is merged, creates the `v0.2.1` tag, GitHub release, and a PR bumping `main` to the next dev version, for example `0.2.2.dev0`. +- `.github/workflows/publish-release.yml` (`Publish release`): after the release PR is merged, creates the `v0.2.1` tag, GitHub release, website PR, and a PR bumping `main` to the next dev version, for example `0.2.2.dev0`. ## Notes diff --git a/tests/test_bump_version.py b/tests/test_bump_version.py index 76ef22b..d6d42e8 100644 --- a/tests/test_bump_version.py +++ b/tests/test_bump_version.py @@ -26,6 +26,10 @@ def test_bump_version_updates_project_files( 'name = "other"\nversion = "9.9.9"\n\nname = "sbx"\nversion = "0.1.0"\n', encoding="utf-8", ) + (tmp_path / "README.md").write_text( + "uv tool install git+https://github.com/nueces/sbx.git@v0.1.0\n", + encoding="utf-8", + ) monkeypatch.setattr(bump_version, "ROOT", tmp_path) bump_version.bump(version) @@ -39,6 +43,9 @@ def test_bump_version_updates_project_files( lock = (tmp_path / "uv.lock").read_text(encoding="utf-8") assert 'name = "other"\nversion = "9.9.9"' in lock assert f'name = "sbx"\nversion = "{version}"' in lock + readme = (tmp_path / "README.md").read_text(encoding="utf-8") + readme_version = "0.1.0" if ".dev" in version else version + assert f"git+https://github.com/nueces/sbx.git@v{readme_version}" in readme @pytest.mark.parametrize("version", ["1.2", "v1.2.3", "1.2.3-rc1"])