merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit b4159fe9d4faa6b9fd8398325774f577f644154f
parent 9b9a3e49b9ca473bcd16ecd3b92074dfc8dd0633
Author: Florian Dold <florian@dold.me>
Date:   Wed, 25 Feb 2026 15:25:14 +0100

improve bump script

Diffstat:
Mcontrib/bump | 325++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 264 insertions(+), 61 deletions(-)

diff --git a/contrib/bump b/contrib/bump @@ -1,62 +1,265 @@ -#!/usr/bin/env bash +#!/usr/bin/env python3 # This file is in the public domain. -set -eu - -if [ $# != 1 ]; then - >&2 echo "Illegal number of arguments" - >&2 echo "Usage: $0 <version>" - exit -1 -fi - -VERSION="$1" -DATE="$(date -R)" -GIT_USER="$(git config user.name)" -GIT_EMAIL="$(git config user.email)" - -function updated { - local FILE=$1 - if [[ $(grep "${VERSION}" "${FILE}") ]]; then - echo "${FILE} already in ${VERSION}" - return -1 - fi -} - -# update configure.ac -function configure_ac { - if [[ $(grep AC_INIT configure.ac | grep "${VERSION}") ]]; - then - echo "configure.ac already in ${VERSION}" - return 0 - fi - - sed -i "/AC_INIT/s/,\\[\\(.*\\)\\],/,[${VERSION}],/" configure.ac - echo "configure.ac ${VERSION}" -} - -# update debian/changelog -function debian_changelog { - updated debian/changelog || return 0 - - cat <<EOF > ./debian/changelog.tmp -taler-merchant (${VERSION}) unstable; urgency=low - - * Release ${VERSION}. - - -- ${GIT_USER} <${GIT_EMAIL}> ${DATE} - -EOF - cat ./debian/changelog >> ./debian/changelog.tmp - mv ./debian/changelog.tmp ./debian/changelog - echo "debian/changelog ${VERSION}" -} - -function doc_doxygen_taler_doxy { - updated doc/doxygen/taler.doxy || return 0 - - sed -i "/PROJECT_NUMBER/s/= \(.*\)/= ${VERSION}/" doc/doxygen/taler.doxy - echo "doc/doxygen/taler.doxy ${VERSION}" -} - -configure_ac -debian_changelog -doc_doxygen_taler_doxy + +import sys +import os +import re +import argparse +import subprocess +from email.utils import formatdate + +# The list of files this script is responsible for updating +FILES_TO_UPDATE = [ + "configure.ac", + "debian/changelog", + "doc/doxygen/taler.doxy" +] + +def run_cmd(cmd, capture=True): + """Runs a shell command. Can capture output or print it directly to the terminal.""" + if capture: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return result.stdout.strip() + else: + subprocess.run(cmd, check=True) + +def check_git_clean(): + """Checks if the git working directory is clean.""" + try: + # --porcelain outputs nothing if the working tree is completely clean + status = run_cmd(["git", "status", "--porcelain"]) + if status: + print("Error: Git working directory is not clean.", file=sys.stderr) + print("Please commit or stash your changes before bumping the version.", file=sys.stderr) + print(f"\nUncommitted changes:\n{status}\n", file=sys.stderr) + sys.exit(1) + except subprocess.CalledProcessError: + print("Error: Could not determine git status. Are you in a git repository?", file=sys.stderr) + sys.exit(1) + +def confirm(prompt_text): + """Asks the user for a yes/no confirmation.""" + while True: + choice = input(f"{prompt_text} [y/N]: ").strip().lower() + if choice in ['y', 'yes']: + return True + elif choice in ['n', 'no', '']: + return False + print("Please answer 'y' or 'n'.") + +def get_current_version(): + """Extracts the current version from configure.ac.""" + try: + with open("configure.ac", 'r') as f: + for line in f: + if 'AC_INIT' in line: + match = re.search(r'AC_INIT\(.*?,\[(.*?)\],', line) + if match: + return match.group(1) + except FileNotFoundError: + print("Error: configure.ac not found. Run this from the project root.", file=sys.stderr) + sys.exit(1) + + print("Error: Could not parse current version from configure.ac", file=sys.stderr) + sys.exit(1) + +def bump_version(current_version, bump_type): + """Increments the specified part of a semantic version string.""" + parts = current_version.split('.') + + while len(parts) < 3: + parts.append('0') + + try: + major, minor, patch = map(int, parts[:3]) + except ValueError: + print(f"Error: Current version '{current_version}' is not strictly numeric (X.Y.Z). " + "Cannot auto-bump.", file=sys.stderr) + sys.exit(1) + + if bump_type == 'major': + return f"{major + 1}.0.0" + elif bump_type == 'minor': + return f"{major}.{minor + 1}.0" + elif bump_type == 'patch': + return f"{major}.{minor}.{patch + 1}" + +def is_updated(filepath, version): + """Checks if the version string already exists in the file.""" + try: + with open(filepath, 'r') as f: + if version in f.read(): + print(f"{filepath} already in {version}") + return True + except FileNotFoundError: + print(f"Warning: {filepath} not found, skipping...", file=sys.stderr) + return True + return False + +def configure_ac(version, dry_run=False): + filepath = FILES_TO_UPDATE[0] + if is_updated(filepath, version): + return + + with open(filepath, 'r') as f: + content = f.read() + + new_lines = [] + for line in content.splitlines(): + if 'AC_INIT' in line: + line = re.sub(r',\[.*?\],', f',[{version}],', line, count=1) + new_lines.append(line) + + if dry_run: + print(f"[DRY RUN] Would update AC_INIT version in {filepath} to {version}") + else: + with open(filepath, 'w') as f: + f.write('\n'.join(new_lines) + '\n') + print(f"{filepath} {version}") + +def debian_changelog(version, date_str, git_user, git_email, dry_run=False): + filepath = FILES_TO_UPDATE[1] + if is_updated(filepath, version): + return + + changelog_entry = ( + f"taler-merchant ({version}) unstable; urgency=low\n\n" + f" * Release {version}.\n\n" + f" -- {git_user} <{git_email}> {date_str}\n\n" + ) + + with open(filepath, 'r') as f: + existing_content = f.read() + + if dry_run: + print(f"[DRY RUN] Would prepend new release block to {filepath} for {version}") + else: + with open(filepath, 'w') as f: + f.write(changelog_entry + existing_content) + print(f"debian/changelog {version}") + +def doc_doxygen_taler_doxy(version, dry_run=False): + filepath = FILES_TO_UPDATE[2] + if is_updated(filepath, version): + return + + with open(filepath, 'r') as f: + lines = f.readlines() + + for i, line in enumerate(lines): + if 'PROJECT_NUMBER' in line: + lines[i] = re.sub(r'= .*', f'= {version}', line) + + if dry_run: + print(f"[DRY RUN] Would update PROJECT_NUMBER in {filepath} to {version}") + else: + with open(filepath, 'w') as f: + f.writelines(lines) + print(f"doc/doxygen/taler.doxy {version}") + +def git_release_workflow(version, dry_run=False): + """Handles committing, pushing, tagging, and pushing the tag safely.""" + print("\n--- Git Release Workflow ---") + + if dry_run: + print(f"[DRY RUN] Would commit, push, tag, and push tag for version {version}") + return + + # 1. Commit locally + if confirm(f"Stage changed files and commit as 'Bump version to v{version}'?"): + existing_files = [f for f in FILES_TO_UPDATE if os.path.exists(f)] + run_cmd(["git", "add"] + existing_files, capture=False) + run_cmd(["git", "commit", "-m", f"Bump version to v{version}"], capture=False) + print("Commit successful.") + else: + print("Skipping commit. (Aborting remaining git operations)") + return + + # 2. Push the commit BEFORE tagging + push_successful = False + if confirm("Push commit to remote?"): + print("Pushing commit...") + try: + run_cmd(["git", "push"], capture=False) + print("Push successful.") + push_successful = True + except subprocess.CalledProcessError: + print("\nError: Push failed! You likely need to pull or rebase.") + print("Aborting the tagging process to prevent an orphaned local tag.") + return + else: + print("Skipping push.") + + # 3. Tag (Only if push succeeded or user explicitly skipped push but still wants to tag) + tag_name = f"v{version}" + if confirm(f"Create git tag '{tag_name}'?"): + run_cmd(["git", "tag", "-a", tag_name, "-m", f"Release {version}"], capture=False) + print(f"Tag {tag_name} created locally.") + + # 4. Push the Tag + if push_successful or confirm("The main commit wasn't pushed. Push the tag anyway?"): + if confirm(f"Push tag {tag_name} to remote?"): + print(f"Pushing tag {tag_name}...") + run_cmd(["git", "push", "origin", tag_name], capture=False) + print("Tag pushed successfully.") + else: + print("Skipping tag push.") + else: + print("Skipping tag.") + +def main(): + parser = argparse.ArgumentParser(description="Update and bump project version strings.") + + parser.add_argument('--dry-run', action='store_true', help="Simulate the changes without modifying files") + parser.add_argument('--commit', action='store_true', help="Interactively commit, push, and tag the version bump") + + group = parser.add_mutually_exclusive_group() + group.add_argument('--major', action='store_true', help="Increment the major version (X.0.0)") + group.add_argument('--minor', action='store_true', help="Increment the minor version (x.Y.0)") + group.add_argument('--patch', action='store_true', help="Increment the patch version (x.y.Z)") + + parser.add_argument('version', nargs='?', help="Explicitly specify the new version string (e.g., 1.2.3)") + + args = parser.parse_args() + + # Check for a clean git directory before making ANY changes + # We bypass this strictly if doing a dry-run so you can test bumping safely + if not args.dry_run: + check_git_clean() + + bump_requested = args.major or args.minor or args.patch + + if bump_requested and args.version: + parser.error("You cannot specify both a specific version string and a bump flag.") + elif not bump_requested and not args.version: + parser.error("You must specify either a specific version string or a bump flag (--major/--minor/--patch).") + + if bump_requested: + current_version = get_current_version() + bump_type = 'major' if args.major else 'minor' if args.minor else 'patch' + version = bump_version(current_version, bump_type) + if args.dry_run: + print(f"[DRY RUN] Detected current version: {current_version}") + print(f"Bumping {bump_type} version: {current_version} -> {version}") + else: + version = args.version + + date_str = formatdate(localtime=True) + try: + git_user = run_cmd(["git", "config", "user.name"]) + git_email = run_cmd(["git", "config", "user.email"]) + except subprocess.CalledProcessError: + print("Error: Could not retrieve git user details. Are you in a git repository?", file=sys.stderr) + sys.exit(1) + + # Execute file updates + configure_ac(version, dry_run=args.dry_run) + debian_changelog(version, date_str, git_user, git_email, dry_run=args.dry_run) + doc_doxygen_taler_doxy(version, dry_run=args.dry_run) + + # Execute git workflow if requested + if args.commit: + git_release_workflow(version, dry_run=args.dry_run) + +if __name__ == '__main__': + main()