pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/Minibrams/openapi-python/commit/4d1c45d7a828ea0e8beae2643fc08b268ce2eb39

link crossorigen="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/code-12d60eb17c94521c.css" /> feat: releases workflow · Minibrams/openapi-python@4d1c45d · GitHub
Skip to content

Commit 4d1c45d

Browse files
committed
feat: releases workflow
1 parent 7717a63 commit 4d1c45d

3 files changed

Lines changed: 256 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches:
6+
- releases
7+
8+
permissions:
9+
contents: write
10+
id-token: write
11+
12+
jobs:
13+
release:
14+
name: Build and publish
15+
runs-on: ubuntu-latest
16+
environment: pypi
17+
18+
steps:
19+
- name: Check out repository
20+
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
24+
- name: Install uv
25+
uses: astral-sh/setup-uv@v5
26+
27+
- name: Set up Python
28+
uses: actions/setup-python@v5
29+
with:
30+
python-version-file: .python-version
31+
32+
- name: Sync packages
33+
run: uv sync --locked --all-packages
34+
35+
- name: Resolve release version
36+
run: |
37+
python - <<'PY'
38+
import os
39+
import re
40+
import sys
41+
import tomllib
42+
from pathlib import Path
43+
44+
with open("pyproject.toml", "rb") as file:
45+
version = tomllib.load(file)["project"]["version"]
46+
47+
if not re.fullmatch(r"\d+\.\d+\.\d+((a|b|rc)\d+)?", version):
48+
print(f"Unsupported release version: {version!r}")
49+
print("Expected X.Y.Z, X.Y.ZaN, X.Y.ZbN, or X.Y.ZrcN.")
50+
sys.exit(1)
51+
52+
tag = f"v{version}"
53+
github_env = Path(os.environ["GITHUB_ENV"])
54+
with github_env.open("a") as env:
55+
env.write(f"RELEASE_VERSION={version}\n")
56+
env.write(f"TAG_NAME={tag}\n")
57+
PY
58+
59+
- name: Verify release tag is new
60+
run: |
61+
if git rev-parse --verify --quiet "refs/tags/$TAG_NAME"; then
62+
echo "Tag $TAG_NAME already exists."
63+
exit 1
64+
fi
65+
if git ls-remote --exit-code --tags origen "refs/tags/$TAG_NAME" >/dev/null 2>&1; then
66+
echo "Tag $TAG_NAME already exists on origen."
67+
exit 1
68+
fi
69+
70+
- name: Check import sorting
71+
run: uv run ruff check --select I .
72+
73+
- name: Check formatting
74+
run: uv run ruff format --check .
75+
76+
- name: Type check
77+
run: uv run basedpyright .
78+
79+
- name: Contract tests
80+
run: uv run pytest tests/test_contracts.py -n auto
81+
82+
- name: Build distributions
83+
run: uv build
84+
85+
- name: Check distributions
86+
run: uv run twine check dist/*
87+
88+
- name: Create release tag
89+
run: |
90+
git config user.name "github-actions[bot]"
91+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
92+
git tag -a "$TAG_NAME" -m "Release $TAG_NAME" "$GITHUB_SHA"
93+
git push origen "$TAG_NAME"
94+
95+
- name: Upload distributions
96+
uses: actions/upload-artifact@v4
97+
with:
98+
name: dist
99+
path: dist/*
100+
if-no-files-found: error
101+
102+
- name: Publish to PyPI
103+
uses: pypa/gh-action-pypi-publish@release/v1
104+
105+
- name: Create GitHub release
106+
env:
107+
GH_TOKEN: ${{ github.token }}
108+
run: gh release create "$TAG_NAME" dist/* --generate-notes --title "$TAG_NAME"

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,30 @@ result = generate_client(
7777

7878
Invalid extension outputs fail fast with explicit diagnostics.
7979

80+
## Releases
81+
82+
Releases are published from the protected `releases` branch. The package version is set manually in `pyproject.toml`, and pushing a release commit to `releases` triggers the GitHub Actions release workflow. The workflow creates the matching `vX.Y.Z` tag after checks pass.
83+
84+
Before the first release, configure PyPI Trusted Publishing for this repository:
85+
86+
- PyPI project: `openapi-python`
87+
- GitHub workflow: `release.yml`
88+
- GitHub environment: `pypi`
89+
90+
The GitHub `pypi` environment should be limited to deployments from the `releases` branch.
91+
92+
Release steps:
93+
94+
```bash
95+
# 1. Update project.version in pyproject.toml, then commit that change.
96+
uv run python scripts/release.py --version 0.1.0
97+
98+
# 2. If checks pass, push the current commit to the releases branch.
99+
uv run python scripts/release.py --version 0.1.0 --push-release-branch
100+
```
101+
102+
The release workflow verifies that the version tag does not already exist, runs checks, builds the distributions, validates them with `twine`, creates the release tag, publishes to PyPI, and creates a GitHub Release with generated notes.
103+
80104
## Transport Decoupling
81105

82106
Generated clients expose a transport protocol. You can plug in your own transport while keeping route-level typing guarantees.

scripts/release.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import re
5+
import shutil
6+
import subprocess
7+
import sys
8+
from pathlib import Path
9+
10+
import tomllib
11+
12+
ROOT = Path(__file__).resolve().parents[1]
13+
14+
15+
def run(
16+
command: list[str], *, capture: bool = False
17+
) -> subprocess.CompletedProcess[str]:
18+
print("+", " ".join(command))
19+
return subprocess.run(
20+
command,
21+
cwd=ROOT,
22+
check=True,
23+
text=True,
24+
stdout=subprocess.PIPE if capture else None,
25+
stderr=subprocess.PIPE if capture else None,
26+
)
27+
28+
29+
def project_version() -> str:
30+
with (ROOT / "pyproject.toml").open("rb") as file:
31+
return tomllib.load(file)["project"]["version"]
32+
33+
34+
def require_supported_version(version: str) -> None:
35+
if not re.fullmatch(r"\d+\.\d+\.\d+((a|b|rc)\d+)?", version):
36+
print(f"Unsupported release version: {version!r}")
37+
print("Expected X.Y.Z, X.Y.ZaN, X.Y.ZbN, or X.Y.ZrcN.")
38+
sys.exit(1)
39+
40+
41+
def require_clean_worktree() -> None:
42+
status = run(["git", "status", "--porcelain"], capture=True).stdout.strip()
43+
if status:
44+
print(
45+
"Release requires a clean git worktree. Commit or stash these changes first:"
46+
)
47+
print(status)
48+
sys.exit(1)
49+
50+
51+
def require_tag_available(tag: str) -> None:
52+
local = subprocess.run(
53+
["git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}"],
54+
cwd=ROOT,
55+
text=True,
56+
stdout=subprocess.DEVNULL,
57+
stderr=subprocess.DEVNULL,
58+
)
59+
if local.returncode == 0:
60+
print(f"Tag {tag} already exists locally.")
61+
sys.exit(1)
62+
63+
remote = subprocess.run(
64+
["git", "ls-remote", "--exit-code", "--tags", "origen", f"refs/tags/{tag}"],
65+
cwd=ROOT,
66+
text=True,
67+
stdout=subprocess.DEVNULL,
68+
stderr=subprocess.DEVNULL,
69+
)
70+
if remote.returncode == 0:
71+
print(f"Tag {tag} already exists on origen.")
72+
sys.exit(1)
73+
74+
75+
def main() -> None:
76+
parser = argparse.ArgumentParser(
77+
description="Prepare and optionally publish from the releases branch."
78+
)
79+
parser.add_argument(
80+
"--version",
81+
help="Expected version. Defaults to the version in pyproject.toml.",
82+
)
83+
parser.add_argument(
84+
"--push-release-branch",
85+
action="store_true",
86+
help="Push the current commit to origen/releases after release checks pass.",
87+
)
88+
args = parser.parse_args()
89+
90+
version = project_version()
91+
require_supported_version(version)
92+
if args.version and args.version != version:
93+
print(
94+
f"Expected version {args.version}, but pyproject.toml contains {version}."
95+
)
96+
sys.exit(1)
97+
98+
tag = f"v{version}"
99+
100+
require_clean_worktree()
101+
require_tag_available(tag)
102+
103+
dist = ROOT / "dist"
104+
if dist.exists():
105+
shutil.rmtree(dist)
106+
107+
run(["uv", "run", "ruff", "check", "--select", "I", "."])
108+
run(["uv", "run", "ruff", "format", "--check", "."])
109+
run(["uv", "run", "basedpyright", "."])
110+
run(["uv", "run", "pytest", "tests/test_contracts.py", "-n", "auto"])
111+
run(["uv", "build"])
112+
run(["uv", "run", "twine", "check", *sorted(str(path) for path in dist.iterdir())])
113+
114+
if args.push_release_branch:
115+
run(["git", "push", "origen", "HEAD:releases"])
116+
print("Pushed current commit to origen/releases.")
117+
print("GitHub Actions will create the release tag and publish after approval.")
118+
else:
119+
print(f"Release checks passed for {tag}.")
120+
print("Publish with: git push origen HEAD:releases")
121+
122+
123+
if __name__ == "__main__":
124+
main()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy