Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
docs: update MODULE.bazel usage example
ci: bump Bazel version in test matrix

Breaking changes: use feat! or fix! and fill out the BREAKING CHANGE section below.
Breaking changes: use feat! or fix! in the PR title and fill out the
BREAKING CHANGE section below. The PR description becomes the squash commit
body, so do not mention that footer (with its colon) for non-breaking changes;
it forces a major release. See CONTRIBUTING.md#major-releases.

Delete this comment block before submitting.
-->
Expand Down
82 changes: 71 additions & 11 deletions .github/workflows/tag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,40 @@ jobs:
exit 1
fi

# Show which commits ccv treats as breaking, so a maintainer can tell an
# intended major from accidental `BREAKING CHANGE: ` text in a commit body
# (squash merges include the PR description). Matching body lines are
# printed under each subject. The grep is per line, so it may list a few
# more commits than ccv counts. Control characters and backticks are
# stripped so commit text cannot break out of the summary code block.
- name: List breaking commits
id: list
if: steps.ccv.outputs.new-tag-version-type == 'major'
continue-on-error: true # Informational only
run: |
sanitize() { LC_ALL=C tr -d '\000-\010\013-\037\177' | tr '`' "'"; }
# Same pattern as ccv v0.10.0; `^` anchors only the first alternative.
breaking='^(fix|feat)(\(.+\))?!: |BREAKING CHANGE: '
last_tag="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*')"
commits="$(
git log --format='%H' -E --grep="${breaking}" "${last_tag}..HEAD" |
while read -r sha; do
git log -1 --format='%h %s' "${sha}"
git log -1 --format='%b' "${sha}" | { grep -E -- "${breaking}" || true; } | sed 's/^/ /'
done | sanitize
)"
{
echo "### Likely breaking commits"
echo
echo "Best effort: commits since the closest version tag that match ccv's breaking-change pattern."
echo "An empty list is inconclusive; inspect \`git log\` in that case."
echo
echo '```text'
echo "Since $(sanitize <<<"${last_tag}"):"
echo "${commits:-(none found)}"
echo '```'
} >> "${GITHUB_STEP_SUMMARY}"

- name: Report dry run
if: steps.ccv.outputs.new-tag == 'true' && inputs.dry_run
env:
Expand All @@ -89,32 +123,58 @@ jobs:
echo "Dry run: next version would be \`${TAG}\` (${TYPE}) at \`${GITHUB_SHA}\`. No tag pushed."
if [[ "${TYPE}" == "major" ]]; then
echo
echo "A non-dry run would fail: major releases must be tagged by hand (see CONTRIBUTING.md#major-releases)."
echo "A non-dry run would fail: major releases must be tagged by hand. If the commits listed above"
echo "show the bump is accidental, tag the intended version instead (see CONTRIBUTING.md#major-releases)."
fi
} >> "${GITHUB_STEP_SUMMARY}"

- name: Block major version bump
if: steps.ccv.outputs.new-tag == 'true' && steps.ccv.outputs.new-tag-version-type == 'major' && !inputs.dry_run
env:
TAG: ${{ steps.ccv.outputs.new-tag-version }}
LIST_OK: ${{ steps.list.outcome == 'success' }}
run: |
echo "::error title=Major version bump::Commits since the last tag require a major bump to ${TAG}. Major releases are not tagged automatically."
# Next patch after the highest release tag, offered for an accidental bump.
latest="$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1 || true)"
patch_tag=""
if [[ "${latest}" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
patch_tag="v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$((BASH_REMATCH[3] + 1))"
fi
print_commands() {
echo '```bash'
echo "git fetch origin"
echo "git -c tag.gpgSign=false tag $1 ${GITHUB_SHA}"
echo "git push origin refs/tags/$1"
echo '```'
}
{
echo "## Major version bump blocked"
echo
echo "Commits since the last tag (\`feat!:\`, \`fix!:\` or \`BREAKING CHANGE:\`) require a major bump to \`${TAG}\`."
echo "No tag was pushed. Every later run will fail the same way until a major release is tagged by hand."
echo "Commits since the last tag require a major bump to \`${TAG}\` at \`${GITHUB_SHA}\`. No tag was pushed."
echo "Scheduled runs and dispatches fail the same way until a release tag is pushed by hand on this commit."
echo
echo "To release this commit, tag it explicitly and push with your own credentials."
echo "Tags cannot be deleted or moved, so double-check the commit first:"
if [[ "${LIST_OK}" == "true" ]]; then
echo "Check the breaking commits listed above first."
else
echo "Listing the breaking commits failed. Inspect \`git log\` since the last tag first."
fi
echo
echo '```bash'
echo "git fetch origin"
echo "git tag ${TAG} ${GITHUB_SHA}"
echo "git push origin refs/tags/${TAG}"
echo '```'
echo "Push the tag with your own credentials (a \`GITHUB_TOKEN\` push does not trigger \`release.yaml\`)."
echo "The push releases that version. Tags cannot be deleted or moved."
echo
echo "\`release.yaml\` runs on the tag push. See CONTRIBUTING.md#major-releases."
echo "**Intended breaking change:** release \`${TAG}\`:"
echo
print_commands "${TAG}"
echo
if [[ -n "${patch_tag}" ]]; then
echo "**Accidental bump** (for example the text \`BREAKING CHANGE: \` in a PR description): release the next patch \`${patch_tag}\` instead."
echo "Use a minor version if the commits contain \`feat:\`. Later runs count only commits after this tag."
echo
print_commands "${patch_tag}"
echo
fi
echo "The tag must be lightweight (not annotated or signed) and on this commit. See CONTRIBUTING.md#major-releases."
} >> "${GITHUB_STEP_SUMMARY}"
exit 1

Expand Down
40 changes: 30 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,23 +65,43 @@ such as `docs:`, `ci:`, `chore:` or `refactor:` do not.
### Major releases

A breaking change requires a major bump. The versioning tool treats only these
as breaking: `feat!:`, `fix!:`, or a `BREAKING CHANGE: ` footer (with a space,
as breaking: `feat!:`, `fix!:`, or the text `BREAKING CHANGE: ` (with a space,
not a hyphen). Other breaking forms, such as `refactor!:` or
`BREAKING-CHANGE:`, are not detected. Mark breaking changes with one of the
three supported forms.

The `Tag` workflow never pushes a major tag. It fails and prints the version
and the commit SHA it computed. Every later run fails the same way until a
maintainer tags that commit by hand with their own credentials. A tag pushed
by `GITHUB_TOKEN` would not trigger the release:
`BREAKING CHANGE: ` is matched **anywhere** in the commit message, not only in
the footer. Squash-merge commits include the PR description, so do not write
that text in a PR description unless the change is breaking. Prefer `!` in the
PR title (for example `feat!: ...`) to mark a breaking change.

The `Tag` workflow never pushes a major tag. On a major bump it fails, and the
run summary shows the computed version, the commit SHA, and a best-effort list
of the commits that look breaking, with the matching lines from their
messages. Scheduled runs and dispatches keep failing until a maintainer pushes
a release tag on that commit by hand, with their own credentials (a tag pushed
by `GITHUB_TOKEN` would not trigger the release). The summary prints the exact
commands. For an intended breaking change:

```bash
git fetch origin
git tag vX.0.0 <commit-sha-from-the-run-summary>
git -c tag.gpgSign=false tag vX.0.0 <commit-sha-from-the-run-summary>
git push origin refs/tags/vX.0.0
```

Version tags cannot be deleted or moved, so double-check the commit before
pushing. Pushing the tag triggers
[`release.yaml`](.github/workflows/release.yaml), which creates the GitHub
Release and submits a BCR PR.
If the listed commits show the bump is accidental, push the intended version
instead: the next patch after the latest release tag (the summary prints it),
or the next minor if there are `feat:` commits. The version must be higher
than every existing release tag.

Either way:

- The tag must be **lightweight** (not annotated or signed) and on the commit
from the summary. The versioning tool ignores annotated tags, so the bump
would not be cleared. `-c tag.gpgSign=false` prevents signing from your git
config.
- Pushing the tag triggers [`release.yaml`](.github/workflows/release.yaml),
which creates the GitHub Release and submits a BCR PR. Version tags cannot be
deleted or moved, so double-check the commit and version first.
- Later runs count only the commits after this tag, so an accidental breaking
commit no longer forces a major bump.
Loading