Switch to static versioning and two-workflow release model
Replace hatch-vcs dynamic versioning with static version in _version.py and pyproject.toml. Split CI into build.yml (test + stage draft release on main) and release.yml (publish from draft on tag push). Docker images are built on main pushes and re-tagged with the release version on tag push without rebuilding.
This commit is contained in:
+22
-58
@@ -9,8 +9,6 @@ on:
|
|||||||
- ci
|
- ci
|
||||||
- release/*
|
- release/*
|
||||||
- feature/*
|
- feature/*
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- README*
|
- README*
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -34,8 +32,6 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v7
|
||||||
@@ -112,8 +108,6 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
|
||||||
|
|
||||||
- name: Install Homebrew deps
|
- name: Install Homebrew deps
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@@ -176,8 +170,6 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v7
|
||||||
@@ -216,8 +208,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v7
|
uses: astral-sh/setup-uv@v7
|
||||||
@@ -235,64 +225,42 @@ jobs:
|
|||||||
./dist/*.whl
|
./dist/*.whl
|
||||||
./dist/*.tar.gz
|
./dist/*.tar.gz
|
||||||
|
|
||||||
upload_pypi:
|
stage_release:
|
||||||
name: Deploy artifacts to PyPI
|
name: Stage release artifacts
|
||||||
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
needs: [wheel_sdist_linux, test_linux, test_macos, test_windows]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: release
|
if: github.ref == 'refs/heads/main'
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write # mandatory for PyPI publishing
|
|
||||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: artifact
|
|
||||||
path: dist
|
|
||||||
|
|
||||||
- name: Publish to PyPI
|
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
|
||||||
|
|
||||||
create_release:
|
|
||||||
name: Create GitHub release
|
|
||||||
needs: [upload_pypi]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
|
|
||||||
permissions:
|
|
||||||
# Required to create a release
|
|
||||||
contents: write
|
contents: write
|
||||||
id-token: write
|
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- uses: actions/download-artifact@v7
|
- uses: actions/download-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: artifact
|
name: artifact
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Sign the dists with Sigstore
|
- name: Read version from source
|
||||||
uses: sigstore/gh-action-sigstore-python@v3.2.0
|
id: version
|
||||||
with:
|
run: |
|
||||||
inputs: |
|
VERSION=$(python3 -c "exec(open('src/ocrmypdf/_version.py').read()); print(__version__)")
|
||||||
./dist/*.tar.gz
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
./dist/*.whl
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create or update draft release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
run: >-
|
run: |
|
||||||
gh release create
|
TAG="v${{ steps.version.outputs.version }}"
|
||||||
"$GITHUB_REF_NAME"
|
|
||||||
--repo "$GITHUB_REPOSITORY"
|
|
||||||
--notes ""
|
|
||||||
|
|
||||||
- name: Upload artifact signatures to GitHub Release
|
# Delete existing draft release if it exists (ignore errors)
|
||||||
env:
|
gh release delete "$TAG" --yes 2>/dev/null || true
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
# Upload to GitHub Release using the `gh` CLI.
|
# Create new draft release with all artifacts
|
||||||
# `dist/` contains the built packages, and the
|
gh release create "$TAG" \
|
||||||
# sigstore-produced signatures and certificates.
|
--draft \
|
||||||
run: >-
|
--title "$TAG" \
|
||||||
gh release upload
|
--notes "Draft release - will be updated when tag is pushed" \
|
||||||
"$GITHUB_REF_NAME" dist/**
|
dist/*
|
||||||
--repo "$GITHUB_REPOSITORY"
|
|
||||||
|
|
||||||
docker_ubuntu:
|
docker_ubuntu:
|
||||||
name: Build Ubuntu-based Docker image
|
name: Build Ubuntu-based Docker image
|
||||||
@@ -314,8 +282,6 @@ jobs:
|
|||||||
run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV
|
run: echo "DOCKER_IMAGE_NAME=ocrmypdf" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -362,8 +328,6 @@ jobs:
|
|||||||
run: echo "DOCKER_IMAGE_NAME=ocrmypdf-alpine" >> $GITHUB_ENV
|
run: echo "DOCKER_IMAGE_NAME=ocrmypdf-alpine" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
|
||||||
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2022 James R. Barlow
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
name: Publish Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Publish release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: release
|
||||||
|
url: https://pypi.org/p/ocrmypdf
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Download artifacts from draft release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
gh release download "$GITHUB_REF_NAME" --dir dist --pattern '*.whl'
|
||||||
|
gh release download "$GITHUB_REF_NAME" --dir dist --pattern '*.tar.gz'
|
||||||
|
|
||||||
|
- name: Publish to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
||||||
|
# PyPI doesn't support sigstore publishing, so generate after publishing to PyPI
|
||||||
|
- name: Sign the dists with Sigstore
|
||||||
|
uses: sigstore/gh-action-sigstore-python@v3.2.0
|
||||||
|
with:
|
||||||
|
inputs: |
|
||||||
|
./dist/*.tar.gz
|
||||||
|
./dist/*.whl
|
||||||
|
|
||||||
|
- name: Extract release notes
|
||||||
|
run: |
|
||||||
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
|
MAJOR="${VERSION%%.*}"
|
||||||
|
MAJOR_PADDED=$(printf "%02d" "$MAJOR")
|
||||||
|
RELEASE_FILE="docs/releasenotes/version${MAJOR_PADDED}.md"
|
||||||
|
|
||||||
|
python3 << EOF
|
||||||
|
import re
|
||||||
|
|
||||||
|
version = "${VERSION}"
|
||||||
|
release_file = "${RELEASE_FILE}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(release_file) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Find the section for this version
|
||||||
|
# Match from "## vX.Y.Z" until the next "## v" or end of file
|
||||||
|
pattern = rf"## v{re.escape(version)}\n(.*?)(?=\n## v|\Z)"
|
||||||
|
match = re.search(pattern, content, re.DOTALL)
|
||||||
|
notes = match.group(1).strip() if match else ""
|
||||||
|
except FileNotFoundError:
|
||||||
|
notes = ""
|
||||||
|
|
||||||
|
with open("release_notes.md", "w") as f:
|
||||||
|
f.write(notes)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Publish release (convert draft to published)
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
# Update release: remove draft status, add release notes
|
||||||
|
gh release edit "$GITHUB_REF_NAME" \
|
||||||
|
--draft=false \
|
||||||
|
--notes-file release_notes.md
|
||||||
|
|
||||||
|
# Upload signatures to the release
|
||||||
|
gh release upload "$GITHUB_REF_NAME" dist/*.sigstore.json --clobber
|
||||||
|
|
||||||
|
docker_tag:
|
||||||
|
name: Tag Docker images with release version
|
||||||
|
needs: [publish]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: jbarlow83
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Tag ocrmypdf (Ubuntu) image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag "jbarlow83/ocrmypdf:$GITHUB_REF_NAME" \
|
||||||
|
"jbarlow83/ocrmypdf:latest"
|
||||||
|
|
||||||
|
- name: Tag ocrmypdf-ubuntu image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag "jbarlow83/ocrmypdf-ubuntu:$GITHUB_REF_NAME" \
|
||||||
|
"jbarlow83/ocrmypdf-ubuntu:latest"
|
||||||
|
|
||||||
|
- name: Tag ocrmypdf-alpine image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag "jbarlow83/ocrmypdf-alpine:$GITHUB_REF_NAME" \
|
||||||
|
"jbarlow83/ocrmypdf-alpine:latest"
|
||||||
@@ -44,7 +44,6 @@ docs/_build/
|
|||||||
docs/_static/
|
docs/_static/
|
||||||
docs/_templates/
|
docs/_templates/
|
||||||
docs/Makefile
|
docs/Makefile
|
||||||
src/ocrmypdf/_version.py
|
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
.aider*
|
.aider*
|
||||||
|
|||||||
@@ -0,0 +1,397 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# SPDX-FileCopyrightText: 2017-2019 Joe Rickerby and contributors
|
||||||
|
# SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
|
||||||
|
"""Bump the version number in all the right places."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cyclopts
|
||||||
|
from packaging.version import InvalidVersion, Version
|
||||||
|
|
||||||
|
try:
|
||||||
|
from github import Github, GithubException
|
||||||
|
except ImportError:
|
||||||
|
Github = None # type: ignore
|
||||||
|
GithubException = Exception # type: ignore
|
||||||
|
|
||||||
|
import ocrmypdf
|
||||||
|
|
||||||
|
config = [
|
||||||
|
# file path, version find/replace format
|
||||||
|
("src/ocrmypdf/_version.py", '__version__ = "{}"'),
|
||||||
|
("pyproject.toml", 'version = "{}"'),
|
||||||
|
]
|
||||||
|
|
||||||
|
RED = "\u001b[31m"
|
||||||
|
GREEN = "\u001b[32m"
|
||||||
|
YELLOW = "\u001b[33m"
|
||||||
|
OFF = "\u001b[0m"
|
||||||
|
|
||||||
|
REPO_NAME = "ocrmypdf/OCRmyPDF"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_release_notes(new_version: str) -> bool:
|
||||||
|
"""Check that the version appears in the release notes.
|
||||||
|
|
||||||
|
Returns True if the version is found, False otherwise.
|
||||||
|
"""
|
||||||
|
version_obj = Version(new_version)
|
||||||
|
major = version_obj.major
|
||||||
|
release_notes_path = Path(f"docs/releasenotes/version{major:02d}.md")
|
||||||
|
|
||||||
|
if not release_notes_path.exists():
|
||||||
|
print(f"{RED}error:{OFF} Release notes file not found: {release_notes_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
content = release_notes_path.read_text(encoding="utf8")
|
||||||
|
version_header = f"## v{new_version}"
|
||||||
|
|
||||||
|
if version_header not in content:
|
||||||
|
print(
|
||||||
|
f"{RED}error:{OFF} Version v{new_version} not found in {release_notes_path}"
|
||||||
|
)
|
||||||
|
print(f" Expected to find: {version_header}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"{GREEN}Found v{new_version} in {release_notes_path}{OFF}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_client():
|
||||||
|
"""Get an authenticated GitHub client."""
|
||||||
|
if Github is None:
|
||||||
|
print(f"{RED}error:{OFF} PyGithub is not installed")
|
||||||
|
print(" Install with: pip install PyGithub")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Try GITHUB_TOKEN env var first
|
||||||
|
token = os.environ.get("GITHUB_TOKEN")
|
||||||
|
|
||||||
|
# Fall back to gh CLI
|
||||||
|
if not token:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["gh", "auth", "token"],
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf8",
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
token = result.stdout.strip()
|
||||||
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
||||||
|
print(f"{RED}error:{OFF} No GitHub authentication found")
|
||||||
|
print(" Set GITHUB_TOKEN env var or run: gh auth login")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Github(token)
|
||||||
|
except GithubException as e:
|
||||||
|
print(f"{RED}error:{OFF} Failed to authenticate with GitHub: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_ci_completion(commit_sha: str, timeout_minutes: int = 30) -> bool:
|
||||||
|
"""Wait for CI to complete on the given commit.
|
||||||
|
|
||||||
|
Returns True if CI passed, False otherwise.
|
||||||
|
"""
|
||||||
|
gh = get_github_client()
|
||||||
|
if gh is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
repo = gh.get_repo(REPO_NAME)
|
||||||
|
except GithubException as e:
|
||||||
|
print(f"{RED}error:{OFF} Failed to access repository: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
workflow_name = "Test and deploy"
|
||||||
|
start_time = time.time()
|
||||||
|
timeout_seconds = timeout_minutes * 60
|
||||||
|
poll_interval = 30 # seconds
|
||||||
|
|
||||||
|
print(f"Waiting for CI workflow '{workflow_name}' on commit {commit_sha[:8]}...")
|
||||||
|
|
||||||
|
# First, wait for the workflow run to appear
|
||||||
|
run = None
|
||||||
|
while time.time() - start_time < timeout_seconds:
|
||||||
|
try:
|
||||||
|
runs = repo.get_workflow_runs(head_sha=commit_sha)
|
||||||
|
for r in runs:
|
||||||
|
if r.name == workflow_name:
|
||||||
|
run = r
|
||||||
|
break
|
||||||
|
if run:
|
||||||
|
break
|
||||||
|
except GithubException as e:
|
||||||
|
print(f"{YELLOW}Warning:{OFF} Error checking workflow runs: {e}")
|
||||||
|
|
||||||
|
elapsed = int(time.time() - start_time)
|
||||||
|
print(f" Waiting for workflow to start... ({elapsed}s)")
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
|
||||||
|
if not run:
|
||||||
|
print(
|
||||||
|
f"{RED}error:{OFF} Workflow run not found within {timeout_minutes} minutes"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f" Found workflow run #{run.run_number} (ID: {run.id})")
|
||||||
|
|
||||||
|
# Now wait for the workflow to complete
|
||||||
|
while time.time() - start_time < timeout_seconds:
|
||||||
|
try:
|
||||||
|
run = repo.get_workflow_run(run.id) # Refresh the run
|
||||||
|
except GithubException as e:
|
||||||
|
print(f"{YELLOW}Warning:{OFF} Error refreshing workflow run: {e}")
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
continue
|
||||||
|
|
||||||
|
status = run.status
|
||||||
|
conclusion = run.conclusion
|
||||||
|
|
||||||
|
elapsed = int(time.time() - start_time)
|
||||||
|
if status == "completed":
|
||||||
|
if conclusion == "success":
|
||||||
|
print(f"{GREEN}CI passed!{OFF} (took {elapsed}s)")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"{RED}CI failed!{OFF} Conclusion: {conclusion}")
|
||||||
|
print(f" View details: {run.html_url}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print(f" Status: {status} ({elapsed}s elapsed)")
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
|
||||||
|
print(f"{RED}error:{OFF} CI did not complete within {timeout_minutes} minutes")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def push_and_wait_for_ci(branch: str) -> bool:
|
||||||
|
"""Push to remote and wait for CI tests to pass."""
|
||||||
|
print("Pushing to GitHub...")
|
||||||
|
|
||||||
|
push_result = subprocess.run(
|
||||||
|
["git", "push", "origin", branch],
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf8",
|
||||||
|
)
|
||||||
|
|
||||||
|
if push_result.returncode != 0:
|
||||||
|
print(f"{RED}error:{OFF} Failed to push: {push_result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get the commit SHA we just pushed
|
||||||
|
sha_result = subprocess.run(
|
||||||
|
["git", "rev-parse", "HEAD"],
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf8",
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
commit_sha = sha_result.stdout.strip()
|
||||||
|
|
||||||
|
print(f"Pushed commit {commit_sha[:8]}")
|
||||||
|
|
||||||
|
return wait_for_ci_completion(commit_sha)
|
||||||
|
|
||||||
|
|
||||||
|
def push_tag(tag: str) -> bool:
|
||||||
|
"""Push the tag to trigger release workflow."""
|
||||||
|
print(f"Pushing tag {tag} to trigger release...")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "push", "origin", tag],
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf8",
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"{RED}error:{OFF} Failed to push tag: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"{GREEN}Tag {tag} pushed successfully!{OFF}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def bump_version() -> None:
|
||||||
|
"""Bump the version number in all the right places."""
|
||||||
|
current_version = ocrmypdf.__version__ # type: ignore
|
||||||
|
try:
|
||||||
|
commit_date_str = subprocess.run(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"show",
|
||||||
|
"--no-patch",
|
||||||
|
"--pretty=format:%ci",
|
||||||
|
f"v{current_version}^{{commit}}",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf8",
|
||||||
|
).stdout
|
||||||
|
cd_date, cd_time, cd_tz = commit_date_str.split(" ")
|
||||||
|
|
||||||
|
url_opts = urllib.parse.urlencode(
|
||||||
|
{"q": f"is:pr merged:>{cd_date}T{cd_time}{cd_tz}"}
|
||||||
|
)
|
||||||
|
url = f"https://github.com/{REPO_NAME}/pulls?{url_opts}"
|
||||||
|
|
||||||
|
print(f"PRs merged since last release:\n {url}")
|
||||||
|
print()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(e)
|
||||||
|
print("Failed to get previous version tag information.")
|
||||||
|
print("Is the virtual environment active?")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
git_changes_result = subprocess.run(["git diff-index --quiet HEAD --"], shell=True)
|
||||||
|
repo_has_uncommitted_changes = git_changes_result.returncode != 0
|
||||||
|
|
||||||
|
if repo_has_uncommitted_changes:
|
||||||
|
print("error: Uncommitted changes detected.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
print( 'Current version:', current_version)
|
||||||
|
new_version = input(' New version: ').strip()
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
try:
|
||||||
|
Version(new_version)
|
||||||
|
except InvalidVersion:
|
||||||
|
print("error: This version doesn't conform to PEP440")
|
||||||
|
print(" https://www.python.org/dev/peps/pep-0440/")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Validate release notes contain this version
|
||||||
|
if not validate_release_notes(new_version):
|
||||||
|
print()
|
||||||
|
print("Please add release notes for this version before proceeding.")
|
||||||
|
print(f"Edit: docs/releasenotes/version{Version(new_version).major:02d}.md")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
actions = []
|
||||||
|
|
||||||
|
for path_pattern, version_pattern in config:
|
||||||
|
paths = [Path(p) for p in glob.glob(path_pattern)]
|
||||||
|
|
||||||
|
if not paths:
|
||||||
|
print(f"error: Pattern {path_pattern} didn't match any files")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
find_pattern = version_pattern.format(current_version)
|
||||||
|
replace_pattern = version_pattern.format(new_version)
|
||||||
|
found_at_least_one_file_needing_update = False
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
contents = path.read_text(encoding="utf8")
|
||||||
|
if find_pattern in contents:
|
||||||
|
found_at_least_one_file_needing_update = True
|
||||||
|
actions.append(
|
||||||
|
(
|
||||||
|
path,
|
||||||
|
find_pattern,
|
||||||
|
replace_pattern,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not found_at_least_one_file_needing_update:
|
||||||
|
print(
|
||||||
|
f'''error: Didn't find any occurrences of "{find_pattern}" in "{path_pattern}"'''
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Here's the plan:")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for action in actions:
|
||||||
|
path, find, replace = action
|
||||||
|
print(f"{path} {RED}{find}{OFF} → {GREEN}{replace}{OFF}")
|
||||||
|
|
||||||
|
print(f"Then commit, and tag as v{new_version}")
|
||||||
|
|
||||||
|
answer = input("Proceed? [y/N] ").strip()
|
||||||
|
|
||||||
|
if answer != "y":
|
||||||
|
print("Aborted")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for path, find, replace in actions:
|
||||||
|
contents = path.read_text(encoding="utf8")
|
||||||
|
contents = contents.replace(find, replace)
|
||||||
|
path.write_text(contents, encoding="utf8")
|
||||||
|
|
||||||
|
print("Files updated.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
while input('Type "done" to continue: ').strip().lower() != "done":
|
||||||
|
pass
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"commit",
|
||||||
|
"--all",
|
||||||
|
f"--message=Bump version: v{new_version}",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"tag",
|
||||||
|
"--annotate",
|
||||||
|
f"--message=v{new_version}",
|
||||||
|
f"v{new_version}",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Commit and tag created locally.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Get current branch
|
||||||
|
branch_result = subprocess.run(
|
||||||
|
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||||
|
capture_output=True,
|
||||||
|
encoding="utf8",
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
branch = branch_result.stdout.strip()
|
||||||
|
|
||||||
|
# Push commit and wait for CI
|
||||||
|
if not push_and_wait_for_ci(branch):
|
||||||
|
print()
|
||||||
|
print(f"{RED}CI failed. The tag was NOT pushed.{OFF}")
|
||||||
|
print("Fix the issues, then manually push the tag:")
|
||||||
|
print(f" git push origin v{new_version}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Push tag to trigger release
|
||||||
|
if not push_tag(f"v{new_version}"):
|
||||||
|
print(f"{RED}Failed to push tag.{OFF} Push manually:")
|
||||||
|
print(f" git push origin v{new_version}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"{GREEN}Done! Release workflow has been triggered.{OFF}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
release_url = f"https://github.com/{REPO_NAME}/releases/tag/v{new_version}"
|
||||||
|
print("Monitor the release at:")
|
||||||
|
print(f" {release_url}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.chdir(Path(__file__).parent.resolve())
|
||||||
|
cyclopts.run(bump_version)
|
||||||
+2
-8
@@ -1,12 +1,12 @@
|
|||||||
# SPDX-FileCopyrightText: 2022 James R. Barlow
|
# SPDX-FileCopyrightText: 2022 James R. Barlow
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling", "hatch-vcs"]
|
requires = ["hatchling"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ocrmypdf"
|
name = "ocrmypdf"
|
||||||
dynamic = ["version"]
|
version = "17.2.0"
|
||||||
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
|
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -59,12 +59,6 @@ webservice = ["streamlit>=1.41.0"]
|
|||||||
[project.scripts]
|
[project.scripts]
|
||||||
ocrmypdf = "ocrmypdf.__main__:run"
|
ocrmypdf = "ocrmypdf.__main__:run"
|
||||||
|
|
||||||
[tool.hatch.version]
|
|
||||||
source = "vcs"
|
|
||||||
|
|
||||||
[tool.hatch.build.hooks.vcs]
|
|
||||||
version-file = "src/ocrmypdf/_version.py"
|
|
||||||
|
|
||||||
[tool.distutils.bdist_wheel]
|
[tool.distutils.bdist_wheel]
|
||||||
python-tag = "py311"
|
python-tag = "py311"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2022 James R. Barlow
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
__version__ = "17.2.0"
|
||||||
Reference in New Issue
Block a user