committed by
GitHub
9 changed files with 666 additions and 3 deletions
@ -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)" |
||||
@ -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 |
||||
@ -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() |
||||
@ -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" |
||||
Loading…
Reference in new issue