Skip to content

Name the vote-passed versions explicitly and validate them before use - #822

Merged
wu-sheng merged 1 commit into
mainfrom
fix/release-vote-passed-prompts
Aug 14, 2026
Merged

Name the vote-passed versions explicitly and validate them before use#822
wu-sheng merged 1 commit into
mainfrom
fix/release-vote-passed-prompts

Conversation

@wu-sheng

@wu-sheng wu-sheng commented Aug 14, 2026

Copy link
Copy Markdown
Member

Problem

vote-passed <old_version> published the version resolved from the tag and deleted the
version passed as the argument:

cmd_vote_passed() {
    local old_version="${1:-}"     # ← the argument, and it gets svn rm'd
    version=$(resolve_version "")  # ← what actually gets released

Two versions that do opposite things, and only one of them positional — the deleted one.
Reading vote-passed 9.7.0 as "release 9.7.0" is the natural interpretation, and it would
have promoted 9.7.0 into dist/release and then immediately removed it.

Fix

Take them by name, or ask for them:

./tools/releasing/release.sh vote-passed --release 9.7.0 --old_version 9.6.0
./tools/releasing/release.sh vote-passed --release 9.7.0 --no-cleanup
./tools/releasing/release.sh vote-passed          # asks for both

Anything not named is prompted for, each prompt saying what happens to that version:

  The version being released. It must already be tagged and voted on.
  Most recently tagged: 9.7.0 (tag v9.7.0)
  Release version [9.7.0]:

  The version to remove from dist/release. ASF policy keeps only the
  current release there; older ones are served from archive.apache.org.
  Currently published in dist/release: 9.6.0
  Old version to remove [9.6.0] (or 'none' to skip):
  • Positional arguments are an error, pointing at the named form — existing muscle
    memory fails loudly instead of quietly meaning something else.
  • Equal versions are refused, both are validated as x.y.z, and the release version
    must already be tagged.
  • RELEASE_VERSION / OLD_VERSION still work; flags win over them.

The defaults behind those prompts were wrong too

The release version came from the highest version tag, but releases are not monotonic.
A 9.6.1 patch cut from the 9.6.0 line after 9.7.0 has shipped is newer in time but
lower in version:

$ git tag -a v9.6.1 v9.6.0^{commit} -m "patch"      # tagged today
$ git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1
v9.7.0                                              # ← already released

Ordered by tag creation date now — maven-release-plugin writes annotated tags, so that
timestamp belongs to the tag and survives fetches. The old version comes from
dist/release, the only place that knows what this release actually replaces, excluding
the version being released so re-running after a partial failure cannot offer to delete it.

A pre-existing bug this uncovered

error() wrote to stdout, and resolve_version is always called as
version=$(resolve_version ...). Its message was captured into the variable rather than
shown — releasing an untagged version aborted correctly but printed nothing about why, and
this reached every command, not just vote-passed.

info/warn/error now write to stderr. The email templates use plain echo, so they
stay on stdout and remain pipeable.

The old version was also unvalidated, so a typo like --old_version 9.6 would have been
handed to svn rm as a path.

Testing

Against the live repository and Apache SVN:

Case Result
--release 9.7.0 --old_version 9.6.0 releases 9.7.0, removes 9.6.0
--old-version (kebab alias) same
--release 9.7.0 --no-cleanup releases 9.7.0, dist/release untouched
RELEASE_VERSION / OLD_VERSION honoured; flags win
no arguments both detected as defaults, confirmed by the operator
vote-passed 9.6.0 aborts, points at the named form
both versions equal aborts: would delete the release being published
--release 9.9.9 (untagged) aborts with a visible message — previously silent
--old_version 9.6 aborts: must look like x.y.z
--release with no value / --bogus aborts
simulated v9.6.1 on the 9.6.0 line suggests 9.6.1; old ordering gave 9.7.0
email announce piped body still on stdout

🤖 Generated with Claude Code

@wu-sheng wu-sheng added the chore label Aug 14, 2026
@wu-sheng wu-sheng added this to the 9.8.0 milestone Aug 14, 2026
@wu-sheng wu-sheng changed the title Ask vote-passed for both versions instead of taking one positionally Name the vote-passed versions explicitly; never take them positionally Aug 14, 2026
@wu-sheng
wu-sheng force-pushed the fix/release-vote-passed-prompts branch from 62c440b to 9a3b90f Compare August 14, 2026 02:01
`vote-passed <old_version>` published the version resolved from the tag and
deleted the version passed as the argument. Two versions that do opposite things,
and only one of them positional - the deleted one. Reading `vote-passed 9.7.0` as
"release 9.7.0" is the natural interpretation, and it would have promoted 9.7.0
into dist/release and then immediately svn rm'd it.

Take them by name, or ask for them:

    vote-passed --release 9.7.0 --old_version 9.6.0
    vote-passed --release 9.7.0 --no-cleanup
    vote-passed                              # asks for both

Anything not named is prompted for, each prompt saying what will happen to that
version. Positional arguments are now an error pointing at the named form, so
existing muscle memory fails loudly rather than quietly meaning something else.
Equal versions are refused and the release version must already be tagged.
RELEASE_VERSION and OLD_VERSION still work, with the flags winning over them.

Versions are validated by one anchored regex, shared with cleanup. A glob such as
[0-9]*.[0-9]*.[0-9]* accepts "9.7.0/", which is textually different from "9.7.0"
and so slips past the equality guard, yet SVN canonicalises the trailing slash -
cleanup would have deleted the release promote had just published. Anchoring also
rejects whitespace, v-prefixes, traversal and command characters before any of it
reaches an svn path.

The defaults behind the prompts were wrong too. The release version came from the
highest version tag, but releases are not monotonic: a 9.6.1 patch cut from the
9.6.0 line after 9.7.0 has shipped is newer in time but lower in version, so the
suggestion would have been the already-released 9.7.0. Order by tag creation date
instead - maven-release-plugin writes annotated tags, so that timestamp belongs to
the tag and survives fetches. The old version now comes from dist/release, the
only place that knows what this release actually replaces, with the version being
released filtered out so re-running after a partial failure cannot offer to delete
it.

Both detectors are genuinely best effort. Under `set -euo pipefail` a failed
svn ls, or a grep matching nothing - which is exactly the state after a partial
failure, when only the new release remains - aborted the release instead of
yielding no suggestion, leaving the "could not read dist/release" branch
unreachable. They now return success with empty output.

Testing the flags uncovered a bug that predates them and reached every command.
error() wrote to stdout, and resolve_version is always called as
`version=$(resolve_version ...)`, so its message was captured into the variable
instead of shown: releasing an untagged version aborted, correctly, but printed
nothing about why. info/warn/error now write to stderr. The email templates use
plain echo, so they stay on stdout and remain pipeable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wu-sheng
wu-sheng force-pushed the fix/release-vote-passed-prompts branch from 9a3b90f to 10982b3 Compare August 14, 2026 03:33
@wu-sheng wu-sheng changed the title Name the vote-passed versions explicitly; never take them positionally Name the vote-passed versions explicitly and validate them before use Aug 14, 2026
@wu-sheng
wu-sheng merged commit fc00d4f into main Aug 14, 2026
12 of 17 checks passed
@wu-sheng
wu-sheng deleted the fix/release-vote-passed-prompts branch August 14, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants