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
4 changes: 2 additions & 2 deletions .github/workflows/release_prep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ if [[ "${CHANGELOG_VERSION}" != "${VERSION}" ]]; then
die "Tag = '${TAG}' does not match version = '${CHANGELOG_VERSION}' in CHANGELOG.md."
fi

# Instead of embed the version in MODULE.bazel, we expect it to be correct already.
# perl -pi -e "s/version = \"\d+\.\d+\.\d+\",/version = \"${TAG}\",/g" MODULE.bazel
# Reject an unprepared tag rather than publishing inconsistent version references.
python3 tools/prepare_release.py --check "${VERSION}"

# Empty `BUILD.bazel`
{
Expand Down
7 changes: 4 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ repos:
name: Test publishing and CI policy
language: system
entry: python3 -m unittest discover -s tools -p '*_test.py'
files: ^(tools/|docs/assets/|release-site\.json$|\.github/workflows/)
files: ^(\.pre-commit/check_version\.sh$|tools/|docs/assets/|release-site\.json$|\.github/workflows/)
pass_filenames: false
- id: uncomment-bazelmod-includes
name: uncomment-bazelmod-includes
Expand All @@ -49,8 +49,9 @@ repos:
- id: compare-versions
name: compare-versions
description: |
Require matching CHANGELOG.md and MODULE.bazel versions. After a release,
the first later commit must advance both beyond the latest release tag.
Require matching MODULE.bazel and CHANGELOG.md development versions, and a
released or release-prepared README example. After a release, the first later
commit must advance the development version beyond the latest release tag.
language: system
entry: .pre-commit/check_version.sh
pass_filenames: False
Expand Down
14 changes: 14 additions & 0 deletions .pre-commit/check_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,17 @@ if [[ -n "${LATEST_RELEASE}" ]] &&
[[ "$(git rev-parse HEAD)" != "$(git rev-list -n1 "${LATEST_RELEASE}")" ]]; then
die "Development version (${BAZELMOD_VERSION}) still matches latest release tag (${LATEST_RELEASE}), but HEAD contains later work. Bump MODULE.bazel and CHANGELOG.md."
fi

README_VERSION="$(sed -nE 's/.*bazel_dep\(name = "mboworks_bashtest", version = "([^"]+)".*/\1/p' README.md)"
if [[ ! "${README_VERSION}" =~ ^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)$ ]]; then
die "README.md must contain one numeric bashtest dependency version."
fi
# Development keeps the latest released installation example. Release preparation
# advances it to the module version in the reviewed commit that will be tagged.
if [[ -n "${LATEST_RELEASE}" ]]; then
if [[ "${README_VERSION}" != "${LATEST_RELEASE}" && "${README_VERSION}" != "${BAZELMOD_VERSION}" ]]; then
die "README.md (${README_VERSION}) must match latest release (${LATEST_RELEASE}) or prepared release (${BAZELMOD_VERSION})."
fi
elif [[ "$(version_key "${README_VERSION}")" > "$(version_key "${BAZELMOD_VERSION}")" ]]; then
die "README.md (${README_VERSION}) is newer than MODULE.bazel (${BAZELMOD_VERSION})."
fi
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 0.7.1

* Derive consumer fixture versions from the module and automate the README
version update during release preparation. Test development/release version
consistency and post-release version bumps.

* Fixed the tools test's path expectation when bashtest is an external module.
* Run the framework test suite as an external dependency in CI to catch
root-workspace assumptions before BCR publication.

# 0.7.0

* Automatically provision Bash and Unix utilities, including PCRE support, for
Expand Down
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

module(
name = "mboworks_bashtest",
version = "0.7.0",
version = "0.7.1",
)

bazel_dep(name = "bazel_skylib", version = "1.9.2")
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,12 @@ remains byte-for-byte, so CRLF and LF golden files are intentionally different.

### MODULE.bazel

Check [Releases](https://github.com/mboworks/bashtest/releases) for details. All that is needed is a `bazel_dep` instruction with the correct version.
Use the latest published version from
[Releases](https://github.com/mboworks/bashtest/releases) when consuming bashtest
through the Bazel Central Registry.

```
bazel_dep(name = "mboworks_bashtest", version = "0.6.1")
bazel_dep(name = "mboworks_bashtest", version = "0.7.0")
```

Executable helpers can be declared with `tools = {"GENERATOR": "//tools:generator"}`.
Expand Down
1 change: 1 addition & 0 deletions bashtest/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ bashtest(
env = {
"TARGET_DATA": "$(rlocationpath :target_data)",
"TARGET_LOCATION": "$(location :target_data)",
"TARGET_ROOTPATH": "$(rootpath :target_data)",
},
tools = {
"HELPER": ":helper",
Expand Down
8 changes: 6 additions & 2 deletions bashtest/tests/external_consumer_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
# Run outside a Bazel test: this creates and builds a separate consumer module.
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# The override consumes this checkout, including unreleased development versions.
version="$(sed -nE 's/.*version = "([^"]+)".*/\1/p' "${repo}/MODULE.bazel" | head -n1)"
root="$(mktemp -d)"
cleanup() {
# Windows cannot remove a directory while it is a process working directory.
Expand All @@ -34,7 +36,7 @@ cd "${root}/consumer with spaces"
cp "${repo}/.bazelversion" .bazelversion
cat > MODULE.bazel <<EOF
module(name = "consumer")
bazel_dep(name = "mboworks_bashtest", version = "0.7.0", repo_name = "framework")
bazel_dep(name = "mboworks_bashtest", version = "${version}", repo_name = "framework")
local_path_override(module_name = "mboworks_bashtest", path = "${repo}")
EOF
cat > BUILD.bazel <<'EOF'
Expand All @@ -61,7 +63,9 @@ test::consumer() {
}
test_runner
EOF
bazel test --enable_runfiles=no --test_output=errors //:consumer_test
# Also run the framework's own suite as an external dependency, as BCR does.
# A standalone consumer test alone cannot catch root-workspace assumptions in it.
bazel test --enable_runfiles=no --test_output=errors //:consumer_test @framework//bashtest/tests/...
bazel run --enable_runfiles=no //:consumer_test -- --keep-tmpdir=never

# Exercise both documented update spellings with a workspace path containing spaces.
Expand Down
4 changes: 3 additions & 1 deletion bashtest/tests/tools_consumer_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
# Integration test: distinct default and test execution platforms, external module.
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# The override consumes this checkout, including unreleased development versions.
version="$(sed -nE 's/.*version = "([^"]+)".*/\1/p' "${repo}/MODULE.bazel" | head -n1)"
root="$(mktemp -d)"
cleanup() {
bazel shutdown >/dev/null 2>&1 || true
Expand All @@ -34,7 +36,7 @@ cp "${repo}/.bazelversion" .bazelversion
cp "${repo}/bashtest/tests/tool_fixture.bzl" tool_fixture.bzl
cat > MODULE.bazel <<EOF
module(name = "tools_consumer")
bazel_dep(name = "mboworks_bashtest", version = "0.7.0", repo_name = "framework")
bazel_dep(name = "mboworks_bashtest", version = "${version}", repo_name = "framework")
local_path_override(module_name = "mboworks_bashtest", path = "${repo}")
bazel_dep(name = "rules_shell", version = "0.8.0")
bazel_dep(name = "platforms", version = "1.1.0")
Expand Down
5 changes: 3 additions & 2 deletions bashtest/tests/tools_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ test::executable_and_runfiles() {
# shellcheck disable=SC2154
expect_eq "${HELPER}" "${HELPER_ALIAS}"
# shellcheck disable=SC2154
# location retains sh_test rootpath semantics; use rlocation above for I/O.
expect_eq 'bashtest/tests/target_data.txt' "${TARGET_LOCATION}"
# location retains sh_test rootpath semantics in root and external repos.
# Use rlocation above for I/O, including manifest-only execution.
expect_eq "${TARGET_ROOTPATH}" "${TARGET_LOCATION}"
# Distinct executables must both be available in this single test invocation.
# shellcheck disable=SC2154
expect_ne "${HELPER}" "${SECOND_HELPER}"
Expand Down
25 changes: 25 additions & 0 deletions docs/infrastructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,28 @@ style guide or competing shell formatter is introduced.
Run `bazel test //...`, `python3 -m unittest discover -s tools -p '*_test.py'`, and
`pre-commit run --all-files`. CI also builds the configured release documentation to validate
links and mappings before publication.

## Version consistency

`MODULE.bazel` and the first `CHANGELOG.md` heading identify the development
version. Both external consumer scripts derive their dependency version from
`MODULE.bazel`, because their local overrides consume this checkout. Historical
changelog entries retain their original versions.

The README installation example stays at the latest released version during
development. Before a release, run `python3 tools/prepare_release.py X.Y.Z` on a
release-preparation branch. It verifies the module/changelog version and updates
the README automatically. Review and merge that change before tagging. This brief
preparation stage is the only time the example should name an upcoming release.
Both `tools/trigger_release.sh` (including dry runs) and the archive workflow require
`prepare_release.py --check` to pass, so the tag, archive README, module, and
release notes agree. BCR metadata and release assets already derive their version
from the release tag. Preparation does not commit, tag, or publish anything.

The `compare-versions` pre-commit hook accepts the latest numeric release tag for
the README, or the development version for a prepared release. Without local tags,
it can only validate the README's format and upper bound; CI fetches all tags.
The released commit may keep its release version; the first subsequent commit
must advance the development version. `tools/version_test.py` exercises these
relationships and release preparation in temporary Git repositories. These tests
run through the infrastructure pre-commit hook and CI.
46 changes: 46 additions & 0 deletions tools/prepare_release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) M. Boerger, the MBO Works authors
# SPDX-License-Identifier: Apache-2.0
"""Prepare tracked version references before reviewing and tagging a release."""

import argparse
from pathlib import Path
import re


VERSION = r'(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)'
DEPENDENCY = re.compile(r'(bazel_dep\(name = "mboworks_bashtest", version = ")[^"]+("\))')


def prepare(root, version, check=False):
if not re.fullmatch(VERSION, version):
raise ValueError('Release version must use numeric X.Y.Z format.')
module = re.search(r'\bversion\s*=\s*"([^"]+)"', (root / 'MODULE.bazel').read_text())
changelog = re.search(r'^# (\S+)$', (root / 'CHANGELOG.md').read_text(), re.MULTILINE)
if not module or module[1] != version or not changelog or changelog[1] != version:
raise ValueError('Requested release must match MODULE.bazel and the first CHANGELOG.md heading.')
path = root / 'README.md'
original = path.read_text()
updated, count = DEPENDENCY.subn(lambda match: match[1] + version + match[2], original)
if count != 1:
raise ValueError('README.md must contain exactly one bashtest dependency example.')
if check:
if updated != original:
raise ValueError(f'Run python3 tools/prepare_release.py {version}, then review and merge the change before tagging.')
elif updated != original:
path.write_text(updated)


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('version')
parser.add_argument('--check', action='store_true', help='Verify without changing files')
args = parser.parse_args()
try:
prepare(Path.cwd(), args.version, args.check)
except (ValueError, OSError) as error:
parser.exit(1, f'ERROR: {error}\n')


if __name__ == '__main__':
main()
5 changes: 4 additions & 1 deletion tools/trigger_release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ for arg in "${@}"; do
done
[[ -n "${VERSION}" ]] || die "Usage: ${0} [--dry-run] <version>"

for tool in gh git gpg; do
for tool in gh git gpg python3; do
command -v "${tool}" >/dev/null 2>&1 || die "Required tool '${tool}' is not installed."
done

Expand All @@ -40,6 +40,9 @@ CHANGELOG_VERSION="$(sed -rne 's,^# ([0-9]+([.][0-9]+)+.*)$,\1,p' CHANGELOG.md |
[[ -z "$(git tag --list "${VERSION}")" ]] || die "Tag '${VERSION}' already exists."
gh release view "${VERSION}" >/dev/null 2>&1 && die "Release '${VERSION}' already exists."

# All tracked release references must already be reviewed and committed.
python3 tools/prepare_release.py --check "${VERSION}"

if [[ "${DRY_RUN}" == true ]]; then
echo "[dry-run] Would create and push signed tag '${VERSION}' at $(git rev-parse HEAD)."
exit 0
Expand Down
Loading
Loading