mirror of
https://github.com/fastapi/fastapi.git
synced 2026-06-01 20:25:27 -04:00
👷 Automate release preparation (#15661)
This commit is contained in:
committed by
GitHub
parent
ee22a4b8ca
commit
b4d58fddee
56
.github/workflows/create-draft-release.yml
vendored
Normal file
56
.github/workflows/create-draft-release.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Create Draft Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
create-draft-release:
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
PREPARE_RELEASE_VERSION_FILE: fastapi/__init__.py
|
||||
PREPARE_RELEASE_RELEASE_NOTES_FILE: docs/en/docs/release-notes.md
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: true
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
|
||||
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
|
||||
version: "0.11.4"
|
||||
- name: Extract release details
|
||||
id: release-details
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="$(uv run python scripts/prepare_release.py current-version)"
|
||||
uv run python scripts/prepare_release.py release-notes > draft-release-notes.md
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
- name: Create draft release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ steps.release-details.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release create "$VERSION" \
|
||||
--draft \
|
||||
--title "$VERSION" \
|
||||
--notes-file draft-release-notes.md \
|
||||
--target "$(git rev-parse HEAD)"
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -33,5 +33,5 @@ jobs:
|
||||
steps:
|
||||
- uses: agilepathway/label-checker@c3d16ad512e7cea5961df85ff2486bb774caf3c5 # v1.6.65
|
||||
with:
|
||||
one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal
|
||||
one_of: breaking,security,feature,bug,refactor,upgrade,docs,lang-all,internal,release
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/latest-changes.yml
vendored
2
.github/workflows/latest-changes.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
||||
with:
|
||||
limit-access-to-actor: true
|
||||
- uses: tiangolo/latest-changes@c9d329cb147f0ddf4fb631214e3f838ff17ccbbd # 0.4.1
|
||||
- uses: tiangolo/latest-changes@eb3f6e7ff0073896ecb561e774a121de9418fa06 # 0.5.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
latest_changes_file: docs/en/docs/release-notes.md
|
||||
|
||||
80
.github/workflows/prepare-release.yml
vendored
Normal file
80
.github/workflows/prepare-release.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Prepare Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: Release bump
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
date:
|
||||
description: Release date in YYYY-MM-DD format. Defaults to today.
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
env:
|
||||
PREPARE_RELEASE_VERSION_FILE: fastapi/__init__.py
|
||||
PREPARE_RELEASE_RELEASE_NOTES_FILE: docs/en/docs/release-notes.md
|
||||
steps:
|
||||
- name: Dump GitHub context
|
||||
env:
|
||||
GITHUB_CONTEXT: ${{ toJson(github) }}
|
||||
run: echo "$GITHUB_CONTEXT"
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
token: ${{ secrets.FASTAPI_LATEST_CHANGES }} # zizmor: ignore[secrets-outside-env]
|
||||
persist-credentials: true
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
|
||||
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
|
||||
version: "0.11.4"
|
||||
- name: Prepare release
|
||||
env:
|
||||
PREPARE_RELEASE_BUMP: ${{ inputs.bump }}
|
||||
PREPARE_RELEASE_DATE: ${{ inputs.date }}
|
||||
run: uv run python scripts/prepare_release.py prepare
|
||||
- name: Get release version
|
||||
id: release-version
|
||||
run: |
|
||||
version="$(uv run python scripts/prepare_release.py current-version)"
|
||||
echo "$version"
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
- name: Create release pull request
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.FASTAPI_LATEST_CHANGES }}
|
||||
VERSION: ${{ steps.release-version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
branch="release-${VERSION}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git switch -c "$branch"
|
||||
git add $PREPARE_RELEASE_VERSION_FILE $PREPARE_RELEASE_RELEASE_NOTES_FILE
|
||||
git commit -m "🔖 Release version ${VERSION}"
|
||||
git push --set-upstream origin "$branch"
|
||||
gh pr create \
|
||||
--base master \
|
||||
--head "$branch" \
|
||||
--title "🔖 Release version ${VERSION}" \
|
||||
--body "Prepare release ${VERSION}." \
|
||||
--label release
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -3,7 +3,7 @@ name: Publish
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
- published
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
@@ -173,6 +173,7 @@ tests = [
|
||||
"sqlmodel >=0.0.31",
|
||||
"strawberry-graphql >=0.200.0,<1.0.0",
|
||||
"ty>=0.0.25",
|
||||
"typer >=0.24.1",
|
||||
"a2wsgi >=1.9.0,<=2.0.0",
|
||||
"pytest-xdist[psutil]>=2.5.0",
|
||||
"pytest-cov>=4.0.0",
|
||||
|
||||
216
scripts/prepare_release.py
Normal file
216
scripts/prepare_release.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""Prepare a release by updating the package version and release notes."""
|
||||
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Literal
|
||||
|
||||
import typer
|
||||
|
||||
VERSION_PATTERN = re.compile(r'(?m)^__version__ = "(\d+\.\d+\.\d+)"$')
|
||||
VERSION_HEADING_PATTERN = re.compile(r"(?m)^## (\d+\.\d+\.\d+)(?: \([^)]+\))?$")
|
||||
RELEASE_NOTES_HEADER = """---
|
||||
hide:
|
||||
- navigation
|
||||
---
|
||||
|
||||
# Release Notes
|
||||
|
||||
"""
|
||||
LATEST_CHANGES_HEADER = "## Latest Changes"
|
||||
BumpType = Literal["major", "minor", "patch"]
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
def parse_version(version: str) -> tuple[int, int, int]:
|
||||
match = re.fullmatch(r"\d+\.\d+\.\d+", version)
|
||||
if not match:
|
||||
raise ValueError(f"Invalid version: {version!r}. Expected format: X.Y.Z")
|
||||
major, minor, patch = version.split(".")
|
||||
return int(major), int(minor), int(patch)
|
||||
|
||||
|
||||
def get_current_version(content: str, version_file: Path) -> str:
|
||||
matches = list(VERSION_PATTERN.finditer(content))
|
||||
if len(matches) != 1:
|
||||
raise RuntimeError(
|
||||
f"Expected exactly one __version__ assignment in {version_file}, "
|
||||
f"found {len(matches)}"
|
||||
)
|
||||
return matches[0].group(1)
|
||||
|
||||
|
||||
def bump_version(version: str, bump: BumpType) -> str:
|
||||
major, minor, patch = parse_version(version)
|
||||
if bump == "major":
|
||||
return f"{major + 1}.0.0"
|
||||
if bump == "minor":
|
||||
return f"{major}.{minor + 1}.0"
|
||||
return f"{major}.{minor}.{patch + 1}"
|
||||
|
||||
|
||||
def update_version_file(content: str, version: str, version_file: Path) -> str:
|
||||
current_version = get_current_version(content, version_file)
|
||||
if parse_version(version) <= parse_version(current_version):
|
||||
raise RuntimeError(
|
||||
f"New version {version} must be greater than current version {current_version}"
|
||||
)
|
||||
return VERSION_PATTERN.sub(f'__version__ = "{version}"', content, count=1)
|
||||
|
||||
|
||||
def update_release_notes(
|
||||
content: str, version: str, release_date: date, release_notes_file: Path
|
||||
) -> str:
|
||||
if not content.startswith(RELEASE_NOTES_HEADER):
|
||||
raise RuntimeError(
|
||||
f"{release_notes_file} must start with {RELEASE_NOTES_HEADER!r}"
|
||||
)
|
||||
if re.search(rf"^## {re.escape(version)}(?: \([^)]+\))?$", content, re.M):
|
||||
raise RuntimeError(f"Release notes already contain a section for {version}")
|
||||
|
||||
latest_header = f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n"
|
||||
if not content.startswith(latest_header):
|
||||
raise RuntimeError(f"{release_notes_file} must start with {latest_header!r}")
|
||||
|
||||
release_header = f"## {version} ({release_date.isoformat()})"
|
||||
return content.replace(
|
||||
latest_header,
|
||||
f"{RELEASE_NOTES_HEADER}{LATEST_CHANGES_HEADER}\n\n{release_header}\n",
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
def get_release_notes_body(content: str, version: str, release_notes_file: Path) -> str:
|
||||
version_heading = re.compile(rf"(?m)^## {re.escape(version)}(?: \([^)]+\))?$")
|
||||
match = version_heading.search(content)
|
||||
if not match:
|
||||
raise RuntimeError(
|
||||
f"Could not find release notes section for {version} in {release_notes_file}"
|
||||
)
|
||||
|
||||
next_match = VERSION_HEADING_PATTERN.search(content, match.end())
|
||||
end = next_match.start() if next_match else len(content)
|
||||
body = content[match.end() : end].strip()
|
||||
if not body:
|
||||
raise RuntimeError(
|
||||
f"Release notes section for {version} in {release_notes_file} is empty"
|
||||
)
|
||||
return f"{body}\n"
|
||||
|
||||
|
||||
@app.command()
|
||||
def prepare(
|
||||
bump: Annotated[
|
||||
BumpType,
|
||||
typer.Argument(
|
||||
envvar="PREPARE_RELEASE_BUMP",
|
||||
help="The release bump to make: major, minor, or patch.",
|
||||
),
|
||||
],
|
||||
version_file: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
envvar="PREPARE_RELEASE_VERSION_FILE",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
writable=True,
|
||||
help="Path to the Python file containing the __version__ assignment.",
|
||||
),
|
||||
],
|
||||
release_notes_file: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
envvar="PREPARE_RELEASE_RELEASE_NOTES_FILE",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
writable=True,
|
||||
help="Path to the release notes Markdown file.",
|
||||
),
|
||||
],
|
||||
release_date: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--date",
|
||||
envvar="PREPARE_RELEASE_DATE",
|
||||
help="Release date in YYYY-MM-DD format. Defaults to today.",
|
||||
),
|
||||
] = date.today().isoformat(),
|
||||
) -> None:
|
||||
parsed_release_date = date.fromisoformat(release_date or date.today().isoformat())
|
||||
|
||||
version_file_content = version_file.read_text()
|
||||
release_notes_content = release_notes_file.read_text()
|
||||
version = bump_version(
|
||||
get_current_version(version_file_content, version_file), bump
|
||||
)
|
||||
|
||||
version_file.write_text(
|
||||
update_version_file(version_file_content, version, version_file)
|
||||
)
|
||||
release_notes_file.write_text(
|
||||
update_release_notes(
|
||||
release_notes_content, version, parsed_release_date, release_notes_file
|
||||
)
|
||||
)
|
||||
|
||||
typer.echo(f"Prepared release {version} ({parsed_release_date.isoformat()})")
|
||||
|
||||
|
||||
@app.command()
|
||||
def current_version(
|
||||
version_file: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
envvar="PREPARE_RELEASE_VERSION_FILE",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
help="Path to the Python file containing the __version__ assignment.",
|
||||
),
|
||||
],
|
||||
) -> None:
|
||||
typer.echo(get_current_version(version_file.read_text(), version_file))
|
||||
|
||||
|
||||
@app.command()
|
||||
def release_notes(
|
||||
version_file: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
envvar="PREPARE_RELEASE_VERSION_FILE",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
help="Path to the Python file containing the __version__ assignment.",
|
||||
),
|
||||
],
|
||||
release_notes_file: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
envvar="PREPARE_RELEASE_RELEASE_NOTES_FILE",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
readable=True,
|
||||
help="Path to the release notes Markdown file.",
|
||||
),
|
||||
],
|
||||
) -> None:
|
||||
version = get_current_version(version_file.read_text(), version_file)
|
||||
typer.echo(
|
||||
get_release_notes_body(
|
||||
release_notes_file.read_text(), version, release_notes_file
|
||||
),
|
||||
nl=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
307
tests/test_prepare_release.py
Normal file
307
tests/test_prepare_release.py
Normal file
@@ -0,0 +1,307 @@
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from scripts.prepare_release import (
|
||||
RELEASE_NOTES_HEADER,
|
||||
BumpType,
|
||||
app,
|
||||
bump_version,
|
||||
get_release_notes_body,
|
||||
update_release_notes,
|
||||
update_version_file,
|
||||
)
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def release_notes_content(body: str) -> str:
|
||||
return f"{RELEASE_NOTES_HEADER}{body}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current_version", "bump", "new_version"),
|
||||
[
|
||||
("0.136.3", "major", "1.0.0"),
|
||||
("0.136.3", "minor", "0.137.0"),
|
||||
("0.136.3", "patch", "0.136.4"),
|
||||
],
|
||||
)
|
||||
def test_bump_version(current_version: str, bump: BumpType, new_version: str) -> None:
|
||||
assert bump_version(current_version, bump) == new_version
|
||||
|
||||
|
||||
def test_update_version_file() -> None:
|
||||
content = (
|
||||
'"""FastAPI framework, high performance, easy to learn, fast to code, '
|
||||
'ready for production"""\n\n__version__ = "0.136.3"\n'
|
||||
)
|
||||
|
||||
new_content = update_version_file(content, "0.136.4", Path("fastapi/__init__.py"))
|
||||
|
||||
assert new_content == (
|
||||
'"""FastAPI framework, high performance, easy to learn, fast to code, '
|
||||
'ready for production"""\n\n__version__ = "0.136.4"\n'
|
||||
)
|
||||
|
||||
|
||||
def test_update_version_file_requires_newer_version() -> None:
|
||||
content = '__version__ = "0.136.3"\n'
|
||||
|
||||
with pytest.raises(RuntimeError, match="must be greater"):
|
||||
update_version_file(content, "0.136.3", Path("fastapi/__init__.py"))
|
||||
|
||||
|
||||
def test_update_release_notes() -> None:
|
||||
content = release_notes_content(
|
||||
"""## Latest Changes
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix something.
|
||||
|
||||
## 0.136.3 (2026-05-23)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Previous fix.
|
||||
"""
|
||||
)
|
||||
|
||||
new_content = update_release_notes(
|
||||
content, "0.136.4", date(2026, 5, 30), Path("docs/en/docs/release-notes.md")
|
||||
)
|
||||
|
||||
assert new_content == release_notes_content(
|
||||
"""## Latest Changes
|
||||
|
||||
## 0.136.4 (2026-05-30)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix something.
|
||||
|
||||
## 0.136.3 (2026-05-23)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Previous fix.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def test_update_release_notes_rejects_existing_version() -> None:
|
||||
content = release_notes_content(
|
||||
"""## Latest Changes
|
||||
|
||||
## 0.136.4 (2026-05-30)
|
||||
"""
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="already contain"):
|
||||
update_release_notes(
|
||||
content, "0.136.4", date(2026, 5, 30), Path("docs/en/docs/release-notes.md")
|
||||
)
|
||||
|
||||
|
||||
def test_get_release_notes_body_with_dated_heading() -> None:
|
||||
content = release_notes_content(
|
||||
"""## Latest Changes
|
||||
|
||||
## 0.136.4 (2026-05-30)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix something.
|
||||
|
||||
## 0.136.3 (2026-05-23)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Previous fix.
|
||||
"""
|
||||
)
|
||||
|
||||
body = get_release_notes_body(
|
||||
content, "0.136.4", Path("docs/en/docs/release-notes.md")
|
||||
)
|
||||
|
||||
assert body == "### Fixes\n\n* Fix something.\n"
|
||||
|
||||
|
||||
def test_get_release_notes_body_with_plain_heading() -> None:
|
||||
content = release_notes_content(
|
||||
"""## Latest Changes
|
||||
|
||||
## 0.136.4
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix something.
|
||||
"""
|
||||
)
|
||||
|
||||
body = get_release_notes_body(
|
||||
content, "0.136.4", Path("docs/en/docs/release-notes.md")
|
||||
)
|
||||
|
||||
assert body == "### Fixes\n\n* Fix something.\n"
|
||||
|
||||
|
||||
def test_get_release_notes_body_allows_non_version_h2_content() -> None:
|
||||
content = release_notes_content(
|
||||
"""## Latest Changes
|
||||
|
||||
## 0.136.4
|
||||
|
||||
## Highlights
|
||||
|
||||
* Fix something.
|
||||
|
||||
## 0.136.3
|
||||
|
||||
* Previous fix.
|
||||
"""
|
||||
)
|
||||
|
||||
body = get_release_notes_body(
|
||||
content, "0.136.4", Path("docs/en/docs/release-notes.md")
|
||||
)
|
||||
|
||||
assert body == "## Highlights\n\n* Fix something.\n"
|
||||
|
||||
|
||||
def test_get_release_notes_body_requires_version_section() -> None:
|
||||
content = release_notes_content("## Latest Changes\n")
|
||||
|
||||
with pytest.raises(RuntimeError, match="Could not find"):
|
||||
get_release_notes_body(
|
||||
content, "0.136.4", Path("docs/en/docs/release-notes.md")
|
||||
)
|
||||
|
||||
|
||||
def test_get_release_notes_body_requires_non_empty_section() -> None:
|
||||
content = release_notes_content(
|
||||
"""## Latest Changes
|
||||
|
||||
## 0.136.4
|
||||
|
||||
## 0.136.3
|
||||
|
||||
* Previous fix.
|
||||
"""
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="is empty"):
|
||||
get_release_notes_body(
|
||||
content, "0.136.4", Path("docs/en/docs/release-notes.md")
|
||||
)
|
||||
|
||||
|
||||
def test_cli_updates_configured_files(tmp_path: Path) -> None:
|
||||
version_file = tmp_path / "fastapi" / "__init__.py"
|
||||
version_file.parent.mkdir()
|
||||
version_file.write_text('__version__ = "0.136.3"\n')
|
||||
release_notes_file = tmp_path / "release-notes.md"
|
||||
release_notes_file.write_text(
|
||||
release_notes_content(
|
||||
"""## Latest Changes
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix something.
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"prepare",
|
||||
"patch",
|
||||
"--version-file",
|
||||
str(version_file),
|
||||
"--release-notes-file",
|
||||
str(release_notes_file),
|
||||
"--date",
|
||||
"2026-05-30",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "Prepared release 0.136.4 (2026-05-30)" in result.output
|
||||
assert version_file.read_text() == '__version__ = "0.136.4"\n'
|
||||
assert "## 0.136.4 (2026-05-30)" in release_notes_file.read_text()
|
||||
|
||||
|
||||
def test_cli_accepts_env_vars(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
version_file = tmp_path / "fastapi" / "__init__.py"
|
||||
version_file.parent.mkdir()
|
||||
version_file.write_text('__version__ = "0.136.3"\n')
|
||||
release_notes_file = tmp_path / "docs" / "en" / "docs" / "release-notes.md"
|
||||
release_notes_file.parent.mkdir(parents=True)
|
||||
release_notes_file.write_text(release_notes_content("## Latest Changes\n"))
|
||||
monkeypatch.setenv("PREPARE_RELEASE_BUMP", "minor")
|
||||
monkeypatch.setenv("PREPARE_RELEASE_VERSION_FILE", str(version_file))
|
||||
monkeypatch.setenv("PREPARE_RELEASE_RELEASE_NOTES_FILE", str(release_notes_file))
|
||||
monkeypatch.setenv("PREPARE_RELEASE_DATE", "2026-05-30")
|
||||
|
||||
result = runner.invoke(app, ["prepare"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "Prepared release 0.137.0 (2026-05-30)" in result.output
|
||||
assert version_file.read_text() == '__version__ = "0.137.0"\n'
|
||||
assert "## 0.137.0 (2026-05-30)" in release_notes_file.read_text()
|
||||
|
||||
|
||||
def test_cli_prints_current_version(tmp_path: Path) -> None:
|
||||
version_file = tmp_path / "fastapi" / "__init__.py"
|
||||
version_file.parent.mkdir()
|
||||
version_file.write_text('__version__ = "0.136.3"\n')
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"current-version",
|
||||
"--version-file",
|
||||
str(version_file),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert result.output == "0.136.3\n"
|
||||
|
||||
|
||||
def test_cli_prints_release_notes(tmp_path: Path) -> None:
|
||||
version_file = tmp_path / "fastapi" / "__init__.py"
|
||||
version_file.parent.mkdir()
|
||||
version_file.write_text('__version__ = "0.136.4"\n')
|
||||
release_notes_file = tmp_path / "release-notes.md"
|
||||
release_notes_file.write_text(
|
||||
release_notes_content(
|
||||
"""## Latest Changes
|
||||
|
||||
## 0.136.4 (2026-05-30)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix something.
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"release-notes",
|
||||
"--version-file",
|
||||
str(version_file),
|
||||
"--release-notes-file",
|
||||
str(release_notes_file),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert result.output == "### Fixes\n\n* Fix something.\n"
|
||||
3
uv.lock
generated
3
uv.lock
generated
@@ -1236,6 +1236,7 @@ tests = [
|
||||
{ name = "sqlmodel" },
|
||||
{ name = "strawberry-graphql" },
|
||||
{ name = "ty" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython" },
|
||||
@@ -1320,6 +1321,7 @@ dev = [
|
||||
{ name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" },
|
||||
{ name = "ty", specifier = ">=0.0.25" },
|
||||
{ name = "typer", specifier = ">=0.21.1" },
|
||||
{ name = "typer", specifier = ">=0.24.1" },
|
||||
{ name = "zensical", specifier = ">=0.0.42" },
|
||||
{ name = "zizmor", specifier = ">=1.23.1" },
|
||||
]
|
||||
@@ -1377,6 +1379,7 @@ tests = [
|
||||
{ name = "sqlmodel", specifier = ">=0.0.31" },
|
||||
{ name = "strawberry-graphql", specifier = ">=0.200.0,<1.0.0" },
|
||||
{ name = "ty", specifier = ">=0.0.25" },
|
||||
{ name = "typer", specifier = ">=0.24.1" },
|
||||
]
|
||||
translations = [
|
||||
{ name = "gitpython", specifier = ">=3.1.46" },
|
||||
|
||||
Reference in New Issue
Block a user