Skip to content

Commit d090553

Browse files
author
Anonymous Committer
committed
Automate OpenAPI sync releases
1 parent 056148d commit d090553

4 files changed

Lines changed: 158 additions & 17 deletions

File tree

.github/workflows/release.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@ on:
44
push:
55
tags:
66
- 'v*'
7+
workflow_dispatch:
8+
inputs:
9+
tag:
10+
description: 'Release tag to publish, for example v3.0.6'
11+
required: true
12+
repository_dispatch:
13+
types:
14+
- openapi-sdk-release
15+
16+
env:
17+
RELEASE_TAG: ${{ github.event.inputs.tag || github.event.client_payload.tag || github.ref_name }}
718

819
jobs:
920
build:
1021
runs-on: ubuntu-latest
1122

1223
steps:
1324
- uses: actions/checkout@v4
25+
with:
26+
ref: ${{ github.event.inputs.tag || github.event.client_payload.tag || github.ref_name }}
1427

1528
- uses: actions/setup-python@v5
1629
with:
@@ -24,13 +37,13 @@ jobs:
2437

2538
- name: Verify tag matches package version
2639
env:
27-
GITHUB_REF_NAME: ${{ github.ref_name }}
40+
RELEASE_TAG: ${{ env.RELEASE_TAG }}
2841
run: |
2942
python - <<'PY'
3043
import os
3144
from pathlib import Path
3245
33-
tag = os.environ["GITHUB_REF_NAME"]
46+
tag = os.environ["RELEASE_TAG"]
3447
if not tag.startswith("v"):
3548
raise SystemExit(f"Release tags must start with 'v': {tag}")
3649
@@ -93,7 +106,7 @@ jobs:
93106
- name: Create GitHub release
94107
uses: softprops/action-gh-release@v2
95108
with:
96-
tag_name: ${{ github.ref_name }}
97-
name: ${{ github.ref_name }}
109+
tag_name: ${{ env.RELEASE_TAG }}
110+
name: ${{ env.RELEASE_TAG }}
98111
files: dist/*
99112
generate_release_notes: true

.github/workflows/sync-openapi.yml

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,33 @@ name: sync-openapi
22

33
on:
44
workflow_dispatch:
5+
repository_dispatch:
6+
types:
7+
- openapi-updated
58
schedule:
6-
- cron: '0 2 * * *'
9+
- cron: "*/15 * * * *"
10+
11+
permissions:
12+
contents: write
13+
14+
concurrency:
15+
group: sync-openapi
16+
cancel-in-progress: false
717

818
jobs:
919
sync:
1020
if: github.repository == 'justoneapi/justoneapi-python'
1121
runs-on: ubuntu-latest
12-
permissions:
13-
contents: write
14-
pull-requests: write
1522

1623
steps:
1724
- uses: actions/checkout@v4
25+
with:
26+
ref: main
27+
fetch-depth: 0
1828

1929
- uses: actions/setup-python@v5
2030
with:
21-
python-version: '3.11'
31+
python-version: "3.11"
2232

2333
- name: Install dependencies
2434
run: python -m pip install -e '.[dev]'
@@ -44,11 +54,72 @@ jobs:
4454
- name: Build sync summary
4555
run: python scripts/diff_openapi.py "$RUNNER_TEMP/public-api.previous.json" openapi/public-api.json --output "$RUNNER_TEMP/openapi-sync-summary.md"
4656

47-
- name: Create pull request
48-
uses: peter-evans/create-pull-request@v7
49-
with:
50-
branch: codex/openapi-sync
51-
delete-branch: true
52-
title: 'chore: sync OpenAPI spec and generated SDK'
53-
commit-message: 'chore: sync OpenAPI spec and generated SDK'
54-
body-path: ${{ runner.temp }}/openapi-sync-summary.md
57+
- name: Detect SDK changes
58+
id: changes
59+
run: |
60+
if git diff --quiet -- openapi/public-api.json openapi/public-api.normalized.json justoneapi/generated; then
61+
echo "changed=false" >> "$GITHUB_OUTPUT"
62+
echo "OpenAPI spec and generated SDK are already up to date."
63+
else
64+
echo "changed=true" >> "$GITHUB_OUTPUT"
65+
fi
66+
67+
- name: Bump package version
68+
if: steps.changes.outputs.changed == 'true'
69+
id: version
70+
run: |
71+
version="$(python scripts/bump_version.py)"
72+
echo "version=${version}" >> "$GITHUB_OUTPUT"
73+
echo "tag=v${version}" >> "$GITHUB_OUTPUT"
74+
echo "Bumped package version to ${version}"
75+
76+
- name: Commit generated SDK update
77+
if: steps.changes.outputs.changed == 'true'
78+
env:
79+
VERSION: ${{ steps.version.outputs.version }}
80+
run: |
81+
git config user.name "github-actions[bot]"
82+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
83+
git add openapi/public-api.json openapi/public-api.normalized.json justoneapi/generated justoneapi/_version.py
84+
{
85+
echo "chore: sync OpenAPI spec and generated SDK v${VERSION}"
86+
echo
87+
cat "$RUNNER_TEMP/openapi-sync-summary.md"
88+
} > "$RUNNER_TEMP/commit-message.txt"
89+
git commit -F "$RUNNER_TEMP/commit-message.txt"
90+
91+
- name: Push main and release tag
92+
if: steps.changes.outputs.changed == 'true'
93+
env:
94+
TAG: ${{ steps.version.outputs.tag }}
95+
run: |
96+
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
97+
echo "Tag ${TAG} already exists on origin." >&2
98+
exit 1
99+
fi
100+
for attempt in 1 2 3; do
101+
git fetch --tags origin main
102+
git rebase origin/main
103+
git tag -f -a "${TAG}" -m "Release ${TAG}"
104+
if git push --atomic origin HEAD:main "refs/tags/${TAG}"; then
105+
exit 0
106+
fi
107+
git tag -d "${TAG}" || true
108+
sleep 10
109+
done
110+
echo "Failed to push main and ${TAG} after 3 attempts." >&2
111+
exit 1
112+
113+
- name: Trigger release workflow
114+
if: steps.changes.outputs.changed == 'true'
115+
env:
116+
GITHUB_TOKEN: ${{ github.token }}
117+
TAG: ${{ steps.version.outputs.tag }}
118+
run: |
119+
curl --fail-with-body --silent --show-error \
120+
--request POST \
121+
--header "Accept: application/vnd.github+json" \
122+
--header "Authorization: Bearer ${GITHUB_TOKEN}" \
123+
--header "X-GitHub-Api-Version: 2022-11-28" \
124+
"https://api.github.com/repos/${GITHUB_REPOSITORY}/dispatches" \
125+
--data "{\"event_type\":\"openapi-sdk-release\",\"client_payload\":{\"tag\":\"${TAG}\"}}"

scripts/bump_version.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import re
6+
from pathlib import Path
7+
8+
DEFAULT_VERSION_PATH = Path("justoneapi/_version.py")
9+
VERSION_RE = re.compile(r'^__version__ = "(\d+)\.(\d+)\.(\d+)"\s*$')
10+
11+
12+
def bump_patch_version(path: Path) -> str:
13+
content = path.read_text()
14+
match = VERSION_RE.fullmatch(content.strip())
15+
if not match:
16+
raise ValueError(
17+
f"{path} must contain a single __version__ = \"X.Y.Z\" assignment"
18+
)
19+
20+
major, minor, patch = (int(part) for part in match.groups())
21+
next_version = f"{major}.{minor}.{patch + 1}"
22+
path.write_text(f'__version__ = "{next_version}"\n')
23+
return next_version
24+
25+
26+
def main() -> int:
27+
parser = argparse.ArgumentParser(description="Bump the package patch version.")
28+
parser.add_argument("--path", type=Path, default=DEFAULT_VERSION_PATH)
29+
args = parser.parse_args()
30+
31+
print(bump_patch_version(args.path))
32+
return 0
33+
34+
35+
if __name__ == "__main__":
36+
raise SystemExit(main())

tests/test_bump_version.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from scripts.bump_version import bump_patch_version
6+
7+
8+
def test_bump_patch_version_updates_version_file(tmp_path):
9+
version_path = tmp_path / "_version.py"
10+
version_path.write_text('__version__ = "3.0.5"\n')
11+
12+
assert bump_patch_version(version_path) == "3.0.6"
13+
assert version_path.read_text() == '__version__ = "3.0.6"\n'
14+
15+
16+
def test_bump_patch_version_rejects_unsupported_format(tmp_path):
17+
version_path = tmp_path / "_version.py"
18+
version_path.write_text('__version__ = "3.0.5.dev0"\n')
19+
20+
with pytest.raises(ValueError):
21+
bump_patch_version(version_path)

0 commit comments

Comments
 (0)