Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update hardcoded versions, properly fill release notes #17

Merged
merged 34 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f1d0c56
Add version_files optional argument
nwiltsie Sep 17, 2024
16688bb
Add tests for version file updates
nwiltsie Sep 17, 2024
a2b4361
Update README
nwiltsie Sep 28, 2024
21f5b64
Update CHANGELOG
nwiltsie Sep 28, 2024
439aa42
Fix relative paths
nwiltsie Sep 30, 2024
52915b2
Bugfix
nwiltsie Sep 30, 2024
70bf5eb
Bugfix
nwiltsie Sep 30, 2024
593b821
Log more information
nwiltsie Sep 30, 2024
6abcb41
Add version files to PR
nwiltsie Oct 1, 2024
82ef5a6
Add utils for adding/stripping the branch prefix
nwiltsie Oct 1, 2024
20905fc
Code cleanup
nwiltsie Oct 2, 2024
faca4fd
Add new finalize-release python script
nwiltsie Oct 2, 2024
1b7d388
Remove the JS finalization script
nwiltsie Oct 2, 2024
8fe35cf
Update the workflow
nwiltsie Oct 2, 2024
faae902
Log more information
nwiltsie Oct 2, 2024
19417c3
Does the space matter?
nwiltsie Oct 2, 2024
911df65
Actually install package
nwiltsie Oct 2, 2024
b29afe7
Make sure to set GH_TOKEN
nwiltsie Oct 2, 2024
347833f
Move checkout into subdirectory for symmetry
nwiltsie Oct 2, 2024
a738d11
Use the other token
nwiltsie Oct 2, 2024
dc70909
Add many more logging statements
nwiltsie Oct 2, 2024
6ead304
Actually sort the list
nwiltsie Oct 2, 2024
c526b9f
Fix tests
nwiltsie Oct 2, 2024
ea7bd4e
Format code
nwiltsie Oct 2, 2024
766c91a
Add missing # character to comment
nwiltsie Oct 2, 2024
dd4a579
Refactor prior tag finding into a util
nwiltsie Oct 2, 2024
a4d5696
Add tests for finding prior release tag
nwiltsie Oct 2, 2024
d500bee
Update CHANGELOG
nwiltsie Oct 2, 2024
b5a2e22
Fix lint
nwiltsie Oct 2, 2024
5269c31
Support type-hinted python version string
nwiltsie Oct 2, 2024
45997e7
Add another test
nwiltsie Oct 2, 2024
5736dc9
s/NoAppropriateTagException/NoAppropriateTagError/
nwiltsie Oct 2, 2024
b1e16c2
s/InvalidReleaseException/InvalidReleaseError/
nwiltsie Oct 2, 2024
db06213
Fix typo
nwiltsie Oct 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .github/workflows/wf-finalize-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,21 @@ jobs:
uses: actions/checkout@v4
with:
repository: uclahs-cds/tool-create-release
path: reusable
ref: ${{ steps.workflow-parsing.outputs.SHA }}
token: ${{ secrets.UCLAHS_CDS_REPO_READ_TOKEN }}

- id: parse-version
uses: actions/github-script@v7
env:
INPUT_DRAFT: ${{ inputs.draft }}
- name: Set up python
uses: actions/setup-python@v5
with:
# Use the separate token so that `published` events will be fired
github-token: ${{ secrets.UCLAHS_CDS_REPO_READ_TOKEN }}
script: |
const script = require('./scripts/finalize-release.js')
await script({github, context, core})
python-version: '3.10'

# Install the bundled package
- run: pip install ./reusable

- name: Finalize release
run: finalize-release "$DRAFT"
env:
DRAFT: ${{ inputs.draft }}
# Use the other token to allow the aliasing workflow to run
GH_TOKEN: ${{ secrets.UCLAHS_CDS_REPO_READ_TOKEN }}
18 changes: 14 additions & 4 deletions .github/workflows/wf-prepare-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ on:
description: IANA timezone to use when computing the current date
default: "America/Los_Angeles"
required: false
version_files:
type: string
description: Comma-separated list of relative paths to files with version numbers that should be updated. Every file must have exactly one line that looks like `version = "xxxx"` - some effort is made to handle different quoting styles, leading underscores, etc.
required: false
default: ""

jobs:
prepare-release:
Expand Down Expand Up @@ -89,15 +94,20 @@ jobs:
URL: ${{ github.server_url }}/${{ github.repository }}
VERSION: ${{ steps.get-next-version.outputs.next_version }}

- id: bump-version-files
run: bump-version-files "$REPO_DIR" "$VERSION" "$VERSIONFILES"
env:
REPO_DIR: caller
VERSION: ${{ steps.get-next-version.outputs.next_version }}
VERSIONFILES: ${{ inputs.version_files }}

- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.UCLAHS_CDS_REPO_READ_TOKEN }}
path: caller
add-paths: ${{ inputs.changelog }}
add-paths: ${{ inputs.changelog }},${{ inputs.version_files }}
nwiltsie marked this conversation as resolved.
Show resolved Hide resolved
commit-message: ${{ steps.bump-changelog.outputs.commit_message }}
title: ${{ steps.bump-changelog.outputs.pr_title }}
body-path: ${{ steps.bump-changelog.outputs.pr_bodyfile }}
# This branch name format needs to be kept in-sync with the parser in
# create-new-tag.yaml
branch: automation-create-release-${{ steps.get-next-version.outputs.next_version }}
branch: ${{ steps.get-next-version.outputs.branch_name }}
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- Add `version_files` input to update hard-coded version numbers during release

### Changed

- Move release finalization logic from JavaScript to python

### Fixed

- GitHub auto-generated release notes now link to prior tag, not alias

## [0.0.3] - 2024-09-30

### Added
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Usage of this tool requires adding three workflows to each calling repository (n
* Semantic repositories compute the version based on existing tags and user input for the bump type (`major`/`minor`/`patch`) and the prerelease flag.
* Non-semantic repositories accept the next version as an input.
1. Re-write the `CHANGELOG.md` file to move unreleased changes into a new dated release section.
1. (Optional) Update hardcoded version strings in files passed to `version_files` input.
1. Open a PR listing the target version number and release tag.

```mermaid
Expand Down Expand Up @@ -83,11 +84,20 @@ Parameters can be specified using the [`with`](https://docs.github.com/en/action
| ---- | ---- | ---- | ---- | ---- |
| `wf-prepare-release.yaml` | `bump_type` | string | yes | Kind of semantic release version to target. Must be one of `major`, `minor`, `patch`, or `exact`. Using `exact` requires `exact_version`. |
| `wf-prepare-release.yaml` | `prerelease` | boolean | no | If true, mark the bumped semantic release as a prerelease (only used if `bump_type` is not `exact`). |
| `wf-prepare-release.yaml` | `version_files` | string | no | Comma-separated relative paths to files containing hardcoded version strings (see note below). |
| `wf-prepare-release.yaml` | `exact_version` | string | no | The exact version to assign to the next release (only used if `bump_type` is `exact`). Must not include a leading `v` - use `1XXXX`, not `v1XXXX`. |
| `wf-prepare-release.yaml` | `changelog` | string | no | Relative path to the CHANGELOG file. Defaults to `./CHANGELOG.md`. |
| `wf-prepare-release.yaml` | `timezone` | string | no | IANA timezone to use when calculating the current date for the CHANGELOG. Defaults to `America/Los_Angeles`. |
| `wf-finalize-release.yaml` | `draft` | boolean | no | If true (the default), mark the new release as a draft and require manual intervention to continue. |


### Updating hard-coded strings with `version_files`

> [!TIP]
> If possible, avoid embedding version numbers in version-controlled files. An alternative is to dynamically generate version numbers during the build or release process - [hatch-vcs](/~https://github.com/ofek/hatch-vcs) or [setuptools-scm](https://pypi.org/project/setuptools-scm/) can do this for python packages.

Hard-coded version strings in files (e.g. in `nextflow.config` or `_version.py` files) can be updated by passing the filepath to the `version_files` input. The update behavior is simple and brittle: every input file must have exactly one line that looks like `version = '1.2.3'` (see the full regex in [`updatefiles.py`](./bumpchanges/updatefiles.py)). Multiple matches or no matches will stop the release.
aholmes marked this conversation as resolved.
Show resolved Hide resolved

## License

tool-generate-docs is licensed under the GNU General Public License version 2. See the file LICENSE.md for the terms of the GNU GPL license.
Expand Down
9 changes: 7 additions & 2 deletions bumpchanges/alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
import semver

from .logging import setup_logging, NOTICE, LoggingMixin
from .utils import dereference_tags, tag_to_semver, get_github_releases, Release
from .utils import (
dereference_tags,
tag_to_semver,
get_github_releases_from_checkout,
Release,
)


class IneligibleAliasError(Exception):
Expand Down Expand Up @@ -54,7 +59,7 @@ def __init__(self, repo_dir: Path):
for tag, commit in dereference_tags(self.repo_dir).items():
self._add_git_tag(tag, commit)

for release in get_github_releases(self.repo_dir):
for release in get_github_releases_from_checkout(self.repo_dir):
self._add_github_release(release)

def assert_invariants(self):
Expand Down
154 changes: 154 additions & 0 deletions bumpchanges/finalize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Finalize a release when a PR is merged."""

import argparse
import json
import logging
import os
import subprocess
import textwrap

from dataclasses import dataclass, field
from pathlib import Path


from .logging import setup_logging, NOTICE, LoggingMixin
from .utils import (
decode_branch_name,
version_to_tag_str,
tag_to_semver,
str_to_bool,
get_nearest_ancestor_release_tag,
NoAppropriateTagException,
)


class InvalidReleaseException(Exception):
aholmes marked this conversation as resolved.
Show resolved Hide resolved
"""Exception indicating that the workflow should not have run."""


@dataclass
class PreparedRelease(LoggingMixin):
"""A prepared release to finalize."""

# The git commit to tag
target: str
version: str
pr_number: int
owner_repo: str

# These will be infered based on the version string
tag: str = field(init=False)
prerelease: bool = field(init=False)

@classmethod
def from_environment(cls):
"""Parse a PreparedRelease from the environment."""
if os.environ["GITHUB_EVENT_NAME"] != "pull_request":
raise InvalidReleaseException("Workflow requires pull_request events")

with Path(os.environ["GITHUB_EVENT_PATH"]).open(encoding="utf-8") as infile:
event_data = json.load(infile)

if (
not event_data["pull_request"]["merged"]
or event_data["pull_request"]["state"] != "closed"
):
raise InvalidReleaseException(
"Workflow should only be called on merged and closed PRs"
)

return cls(
target=event_data["pull_request"]["merge_commit_sha"],
version=decode_branch_name(os.environ["GITHUB_HEAD_REF"]),
pr_number=event_data["number"],
owner_repo=os.environ["GITHUB_REPOSITORY"],
)

def __post_init__(self):
self.tag = version_to_tag_str(self.version)

self.prerelease = False

try:
if tag_to_semver(self.tag).prerelease:
self.prerelease = True
except ValueError:
pass

def create(self, draft: bool):
"""Create the release and return the URL."""
args = [
"gh",
"release",
"create",
self.tag,
"--repo",
self.owner_repo,
"--notes",
f"Automatically generated after merging #{self.pr_number}.",
"--generate-notes",
"--target",
self.target,
"--title",
f"Release {self.version}",
]

try:
prior_tag = get_nearest_ancestor_release_tag(self.owner_repo, self.tag)
self.logger.info("Autogenerated notes will start from `%s`", prior_tag)
args.extend(["--notes-start-tag", prior_tag])
except NoAppropriateTagException as err:
self.logger.info("No appropriate release notes start tag found: %s", err)

if draft:
args.append("--draft")

if self.prerelease:
args.append("--prerelease")

release_url = subprocess.check_output(args).decode("utf-8").strip()
self.logger.log(NOTICE, "Release created at %s", release_url)

# Post a comment linking to the new release
comment_header = "*Bleep bloop, I am a robot.*"
comment_body = textwrap.fill(
textwrap.dedent(f"""\
A new release has been {"drafted" if draft else "created"}
as {release_url}. Please review the details for accuracy.
"""),
width=2000,
)

subprocess.run(
[
"gh",
"issue",
"comment",
str(self.pr_number),
"--repo",
self.owner_repo,
"--body",
f"{comment_header}\n\n{comment_body}",
],
check=True,
)


def entrypoint():
"""Main entrypoint for this module."""
setup_logging()

parser = argparse.ArgumentParser()
parser.add_argument("draft", type=str_to_bool)

args = parser.parse_args()

try:
# Parse the environment to create the release
new_release = PreparedRelease.from_environment()

# Draft or create the release
new_release.create(args.draft)
except:
logging.getLogger(__name__).exception("Failed to create new release")
raise
36 changes: 15 additions & 21 deletions bumpchanges/getversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@


from .logging import setup_logging, NOTICE
from .utils import get_closest_semver_ancestor, version_to_tag_str, tag_exists
from .utils import (
get_closest_semver_ancestor,
version_to_tag_str,
tag_exists,
str_to_bool,
encode_branch_name,
)


def get_next_semver(repo_dir: Path, bump_type: str, prerelease: bool) -> str:
Expand Down Expand Up @@ -64,23 +70,6 @@ def validate_version_bump(
raise RuntimeError()


def str_to_bool(value: str) -> bool:
"""Convert a string to a boolean (case-insensitive)."""
truthy_values = {"true", "t", "yes", "y", "1"}
falsey_values = {"false", "f", "no", "n", "0"}

# Normalize input to lowercase
value = value.lower()

if value in truthy_values:
return True

if value in falsey_values:
return False

raise argparse.ArgumentTypeError(f"Invalid boolean value: '{value}'")


def entrypoint():
"""Main entrypoint for this module."""
setup_logging()
Expand All @@ -101,6 +90,11 @@ def entrypoint():
else:
next_version = get_next_semver(args.repo_dir, args.bump_type, args.prerelease)

Path(os.environ["GITHUB_OUTPUT"]).write_text(
f"next_version={next_version}\n", encoding="utf-8"
)
outputs = {
"next_version": next_version,
"branch_name": encode_branch_name(next_version),
}

with Path(os.environ["GITHUB_OUTPUT"]).open(mode="w", encoding="utf-8") as outfile:
for key, value in outputs.items():
outfile.write(f"{key}={value}\n")
Loading