diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index bc253239db..2d66292133 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ - + diff --git a/RELEASE.md b/RELEASE.md deleted file mode 100644 index c3ab6479f4..0000000000 --- a/RELEASE.md +++ /dev/null @@ -1,8 +0,0 @@ -# Release Process - -The Kubernetes controller-runtime Project is released on an as-needed basis. The process is as follows: - -1. An issue is proposing a new release with a changelog since the last release -1. 2 [OWNERS](OWNERS) must LGTM this release -1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` -1. The release issue is closed diff --git a/VERSIONING.md b/VERSIONING.md index f345bbf3dd..2ba11588ca 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -21,9 +21,9 @@ changes will go into an semi-immediate patch or minor release - Please *try* to avoid breaking changes when you can. They make users - face difficult decisions (when do I go through the pain of upgrading), - and make life hard for maintainers and contributors (dealing with - differences on stable branches). + face difficult decisions ("when do I go through the pain of + upgrading?"), and make life hard for maintainers and contributors + (dealing with differences on stable branches). ### Mantainers @@ -65,7 +65,9 @@ greatest code, including breaking changes, happens on master. The *release-X* branches contain stable, backwards compatible code. Every major (X) release, a new such branch is created. It is from these -branches that minor and patch releases are tagged. +branches that minor and patch releases are tagged. If some cases, it may +be neccessary open PRs for bugfixes directly against stable branches, but +this should generally not be the case. The maintainers are responsible for updating the contents of this branch; generally, this is done just before a release using release tooling that @@ -73,12 +75,8 @@ filters and checks for changes tagged as breaking (see below). ### Tooling -* [gen-release-notes.sh](hack/gen-release-notes.sh): generate release - notes for a range of commits, and check for next version type - (***TODO***) - -* [cherrypick-minor-version.sh](hack/cherrypick-minor-version.sh): update - a stable branch with appropriate commits from the master (***TODO***). +* [release-notes.sh](hack/release-notes.sh): generate release notes + for a range of commits, and check for next version type (***TODO***) * [verify-commit-messages.sh](hack/verify-commit-messages.sh): check that your PR and/or commit messages have the right versioning icon @@ -95,14 +93,14 @@ a: - Docs: :book: (`:book:`) - Infra/Tests/Other: :running: (`:running:`) -Individual commits may be tagged separately, but will generally be assumed -to match the PR. For instance, if you have a bugfix in with a breaking -change, it's generally encouraged to submit the bugfix separately, but if -you must put them in one PR, mark the commit separately. +You can also use the equivalent emoji directly, since GitHub doesn't +render the `:xyz:` aliases in PR titles. -*Commits marked separately will be treated similiarly to a distinct PR by -the cherrypick scripts*. Therefore, only use them if you want the given -commit to be considered separately. +Individual commits should not be tagged separately, but will generally be +assumed to match the PR. For instance, if you have a bugfix in with +a breaking change, it's generally encouraged to submit the bugfix +separately, but if you must put them in one PR, mark the commit +separately. ### Commands and Workflow @@ -124,21 +122,44 @@ a command reference. Minor and patch releases are generally done immediately after a feature or bugfix is landed, or sometimes a series of features tied together. -Major releases are done once a sufficient amount of breaking changes are -accrued. Since we don't intend to have a ton of these, the maintainers -will evaluate when to do a major release as it comes up. +Minor releases will only be tagged on the *most recent* major release +branch, except in exceptional circumstances. Patches will be backported +to maintained stable versions, as needed. + +Major releases are done shortly after a breaking change is merged -- once +a breaking change is merged, the next release *must* be a major revison. +We don't intend to have a lot of these, so we may put off merging breaking +PRs until a later date. ### Exact Steps -1. (*if doing a minor or patch release*) Update the release-X branch with - the latest set of changes using the cherrypick tooling (***TODO***) +Follow the release-specific steps below, then follow the general steps +after that. + +#### Minor and patch releases + +1. Update the release-X branch with the latest set of changes by calling + `git rebase master` from the release branch. + +#### Major releases -2. Generate release notes using the release note tooling (***TODO***) +1. Create a new release branch named `release-X` (where `X` is the new + version) off of master. + +#### General + +2. Generate release notes using the release note tooling. 3. Add a release for controller-runtime on GitHub, using those release notes, with a title of `vX.Y.Z`. + +4. Do a similar process for + [controller-tools](https://github.com/kubernetes-sigs/controller-tools) -4. Announce the release in `#kubebuilder` on Slack with a pinned message. +5. Announce the release in `#kubebuilder` on Slack with a pinned message. + +6. Potentially update + [kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) as well. ### Breaking Changes @@ -148,8 +169,11 @@ maintainers/contributors, who have to deal with differences between master and stable branches. That being said, we'll occaisonally want to make breaking changes. They'll -be merged onto master, but won't make it into a release immediately (see -[Release Proccess](#release-process)). +be merged onto master, and will then trigger a major release (see [Release +Proccess](#release-process)). Because breaking changes induce a major +revision, the maintainers may delay a particular breaking change until +a later date when they are ready to make a major revision with a few +breaking changes. If you're going to make a breaking change, please make sure to explain in detail why it's helpful. Is it necessary to cleanly resolve an issue? @@ -158,6 +182,11 @@ Does it improve API ergonomics? Maintainers should treat breaking changes with caution, and evaluate potential non-breaking solutions (see below). +Note that API breakage in public APIs due to dependencies will trigger +a major revision, so you may occaisonally need to have a major release +anyway, due to changes in libraries like `k8s.io/client-go` or +`k8s.io/apimachinery`. + *NB*: Pre-1.0 releases treat breaking changes a bit more lightly. We'll still consider carefully, but the pre-1.0 timeframe is useful for converging on a ergonomic API. diff --git a/hack/release/common.sh b/hack/release/common.sh new file mode 100644 index 0000000000..4e9fbbf934 --- /dev/null +++ b/hack/release/common.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +shopt -s extglob + +cr_major_pattern=":warning:|$(printf "\xe2\x9a\xa0")" +cr_minor_pattern=":sparkles:|$(printf "\xe2\x9c\xa8")" +cr_patch_pattern=":bug:|$(printf "\xf0\x9f\x90\x9b")" +cr_docs_pattern=":book:|$(printf "\xf0\x9f\x93\x96")" +cr_other_pattern=":running:|$(printf "\xf0\x9f\x8f\x83")" + +# cr::symbol-type-raw turns :xyz: and the corresponding emoji +# into one of "major", "minor", "patch", "docs", "other", or +# "unknown", ignoring the '!' +cr::symbol-type-raw() { + case $1 in + @(${cr_major_pattern})?('!')) + echo "major" + ;; + @(${cr_minor_pattern})?('!')) + echo "minor" + ;; + @(${cr_patch_pattern})?('!')) + echo "patch" + ;; + @(${cr_docs_pattern})?('!')) + echo "docs" + ;; + @(${cr_other_pattern})?('!')) + echo "other" + ;; + *) + echo "unknown" + ;; + esac +} + +# cr::symbol-type turns :xyz: and the corresponding emoji +# into one of "major", "minor", "patch", "docs", "other", or +# "unknown". +cr::symbol-type() { + local type_raw=$(cr::symbol-type-raw $1) + if [[ ${type_raw} == "unknown" ]]; then + echo "unknown" + return + fi + + if [[ $1 == *'!' ]]; then + echo "major" + return + fi + + echo ${type_raw} +} + +# git::is-release-branch-name checks if its argument is a release branch name +# (release-0.Y or release-X). +git::is-release-branch-name() { + [[ ${1-must specify release branch name to check} =~ release-((0\.[[:digit:]])|[[:digit:]]+) ]] +} + +# git::ensure-release-branch checks that we're on a release branch +git::ensure-release-branch() { + local current_branch=$(git rev-parse --abbrev-ref HEAD) + if ! git::is-release-branch-name ${current_branch}; then + echo "branch ${current_branch} does not appear to be a release branch (release-X)" >&2 + exit 1 + fi +} + +# git::export-current-version outputs the current version +# as exported variables (${maj,min,patch}_ver, last_tag) after +# checking that we're on the right release branch. +git::export-current-version() { + # make sure we're on a release branch + git::ensure-release-branch + + # deal with the release-0.1 branch, or similar + local release_ver=${BASH_REMATCH[1]} + maj_ver=${release_ver} + local tag_pattern='v${maj_ver}.([[:digit:]]+).([[:digit]]+)' + if [[ ${maj_ver} =~ 0\.([[:digit:]]+) ]]; then + maj_ver=0 + min_ver=${BASH_REMATCH[1]} + local tag_pattern="v0.(${min_ver}).([[:digit:]]+)" + fi + + # make sure we've got a tag that matches our release branch + last_tag=$(git describe --tags --abbrev=0) # try to fetch just the "current" tag name + if [[ ! ${last_tag} =~ ${tag_pattern} ]]; then + echo "tag ${last_tag} does not appear to be a release for this release (${release_ver})-- it should be v${maj_ver}.Y.Z" >&2 + exit 1 + fi + + export min_ver=${BASH_REMATCH[1]} + export patch_ver=${BASH_REMATCH[2]} + export maj_ver=${maj_ver} + export last_tag=${last_tag} +} + +# git::next-version figures out the next version to tag +# (it also sets the current version variables to the current version) +git::next-version() { + git::export-current-version + + local feature_commits=$(git rev-list ${last_tag}..${end_range} --grep="${cr_minor_pattern}") + local breaking_commits=$(git rev-list ${last_tag}..${end_range} --grep="${cr_major_pattern}") + + if [[ -z ${breaking_commits} && ${maj_ver} > 0 ]]; then + local next_ver="v$(( maj_ver + 1 )).0.0" + elif [[ -z ${feature_commits} ]]; then + local next_ver="v${maj_ver}.$(( min_ver + 1 )).0" + else + local next_ver="v${maj_ver}.${min_ver}.$(( patch_ver + 1 ))" + fi + + echo "${next_ver}" +} diff --git a/hack/release/release-notes.sh b/hack/release/release-notes.sh new file mode 100755 index 0000000000..1a98343af4 --- /dev/null +++ b/hack/release/release-notes.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +# import our common stuff +source "$(dirname ${BASH_SOURCE})/common.sh" + +# TODO: work with both release branch and major release +git::ensure-release-branch +git::export-current-version +# check the next version +next_ver=$(git::next-version) + +features="" +bugfixes="" +breaking="" +unknown="" +MERGE_PR="Merge pull request #([[:digit:]]+) from ([[:alnum:]-]+)/.+" +NEWLINE=" +" +head_commit=$(git rev-parse HEAD) +while read commit_word commit; do + read title + read # skip the blank line + read prefix body + + if [[ ${prefix} == v*.*.* && ( ${commit} == ${head_commit} || $(git tag --points-at ${commit}) == v*.*.* ) ]]; then + # skip version merges + continue + fi + set +x + if [[ ! ${title} =~ ${MERGE_PR} ]]; then + echo "Unable to determine PR number for merge ${commit} with title '${title}', aborting." >&2 + exit 1 + fi + pr_number=${BASH_REMATCH[1]} + pr_type=$(cr::symbol-type ${prefix}) + pr_title=${body} + if [[ ${pr_type} == "unknown" ]]; then + pr_title="${prefix} ${pr_title}" + fi + case ${pr_type} in + major) + breaking="${breaking}- ${pr_title} (#${pr_number})${NEWLINE}" + ;; + minor) + features="${features}- ${pr_title} (#${pr_number})${NEWLINE}" + ;; + patch) + bugfixes="${bugfixes}- ${pr_title} (#${pr_number})${NEWLINE}" + ;; + docs|other) + # skip non-code-changes + ;; + unknown) + unknown="${unknown}- ${pr_title} (#${pr_number})${NEWLINE}" + ;; + *) + echo "unknown PR type '${pr_type}' on PR '${pr_title}'" >&2 + exit 1 + esac +done <<<$(git rev-list ${last_tag}..HEAD --merges --pretty=format:%B) + +# TODO: sort non merge commits with tags + +[[ -n "${breaking}" ]] && printf '\e[1;31mbreaking changes this version\e[0m' >&2 +[[ -n "${unknown}" ]] && printf '\e[1;35munknown changes in this release -- categorize manually\e[0m' >&2 + +echo "" >&2 +echo "" >&2 +echo "# ${next_ver}" + +if [[ -n ${breaking} ]]; then + echo "" + echo "## :warning: Breaking Changes" + echo "" + echo "${breaking}" +fi + +if [[ -n ${features} ]]; then + echo "" + echo "## :sparkles: New Features" + echo "" + echo "${features}" +fi + +if [[ -n ${bugfixes} ]]; then + echo "" + echo "## :bug: Bug Fixes" + echo "" + echo "${bugfixes}" +fi + +if [[ -n ${unknown} ]]; then + echo "" + echo "## :question: *categorize these manually*" + echo "" + echo "${unknown}" +fi + +echo "" +echo "*Thanks to all our contributors!*"