Skip to content

Commit ea9da1e

Browse files
authored
Merge pull request #50 from Integration-Automation/dev
Split publish into its own workflow without GraphQL
2 parents f74cb6d + d6a95c8 commit ea9da1e

File tree

3 files changed

+98
-196
lines changed

3 files changed

+98
-196
lines changed

.github/workflows/ci-stable.yml

Lines changed: 0 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -58,197 +58,3 @@ jobs:
5858
with:
5959
name: coverage-xml
6060
path: coverage.xml
61-
62-
publish:
63-
needs: pytest
64-
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
65-
runs-on: ubuntu-latest
66-
permissions:
67-
contents: write
68-
pull-requests: write
69-
steps:
70-
- uses: actions/checkout@v4
71-
with:
72-
token: ${{ secrets.GITHUB_TOKEN }}
73-
- name: Set up Python
74-
uses: actions/setup-python@v5
75-
with:
76-
python-version: "3.12"
77-
cache: pip
78-
- name: Install build tools
79-
run: |
80-
python -m pip install --upgrade pip
81-
pip install build twine
82-
- name: Bump patch version in stable.toml and dev.toml
83-
id: version
84-
run: |
85-
python - <<'PY'
86-
import os
87-
import pathlib
88-
import re
89-
90-
def bump(path: pathlib.Path) -> str:
91-
text = path.read_text(encoding="utf-8")
92-
match = re.search(r'^version = "(\d+)\.(\d+)\.(\d+)"', text, re.MULTILINE)
93-
if match is None:
94-
raise SystemExit(f"no version line found in {path}")
95-
major, minor, patch = (int(g) for g in match.groups())
96-
new = f"{major}.{minor}.{patch + 1}"
97-
path.write_text(text.replace(match.group(0), f'version = "{new}"', 1), encoding="utf-8")
98-
return new
99-
100-
stable_version = bump(pathlib.Path("stable.toml"))
101-
dev_version = bump(pathlib.Path("dev.toml"))
102-
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fp:
103-
fp.write(f"version={stable_version}\n")
104-
fp.write(f"dev_version={dev_version}\n")
105-
print(f"stable.toml -> {stable_version}")
106-
print(f"dev.toml -> {dev_version}")
107-
PY
108-
- name: Push signed bump commit to a release branch
109-
id: bump_commit
110-
env:
111-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
112-
VERSION: ${{ steps.version.outputs.version }}
113-
run: |
114-
python - <<'PY'
115-
import base64
116-
import json
117-
import os
118-
import subprocess
119-
import urllib.error
120-
import urllib.request
121-
122-
head_oid = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
123-
repo = os.environ["GITHUB_REPOSITORY"]
124-
token = os.environ["GH_TOKEN"]
125-
version = os.environ["VERSION"]
126-
branch = f"release/bump-v{version}"
127-
128-
def api(path, method="GET", body=None):
129-
req = urllib.request.Request(
130-
f"https://api.github.com/{path}",
131-
data=(json.dumps(body).encode("utf-8") if body is not None else None),
132-
headers={
133-
"Authorization": f"Bearer {token}",
134-
"Accept": "application/vnd.github+json",
135-
"Content-Type": "application/json",
136-
},
137-
method=method,
138-
)
139-
with urllib.request.urlopen(req) as resp:
140-
raw = resp.read()
141-
return json.loads(raw) if raw else None
142-
143-
# Delete any pre-existing release branch (from a prior partial run)
144-
# so the bump always starts from main's HEAD. Avoids STALE_DATA from
145-
# createCommitOnBranch and prevents stacking multiple bump commits.
146-
# Any open PR on the stale branch gets auto-closed when the branch
147-
# is deleted, which is fine — we always open a fresh PR below.
148-
try:
149-
api(f"repos/{repo}/git/refs/heads/{branch}", method="DELETE")
150-
print(f"deleted stale branch: {branch}")
151-
except urllib.error.HTTPError as error:
152-
if error.code not in (404, 422): # ref doesn't exist; nothing to clean
153-
raise
154-
155-
api(
156-
f"repos/{repo}/git/refs",
157-
method="POST",
158-
body={"ref": f"refs/heads/{branch}", "sha": head_oid},
159-
)
160-
expected_oid = head_oid
161-
162-
def b64(path: str) -> str:
163-
with open(path, "rb") as fp:
164-
return base64.b64encode(fp.read()).decode("ascii")
165-
166-
mutation = """
167-
mutation($input: CreateCommitOnBranchInput!) {
168-
createCommitOnBranch(input: $input) {
169-
commit { oid url }
170-
}
171-
}
172-
"""
173-
174-
payload = {
175-
"query": mutation,
176-
"variables": {
177-
"input": {
178-
"branch": {
179-
"repositoryNameWithOwner": repo,
180-
"branchName": branch,
181-
},
182-
"message": {
183-
"headline": f"Bump version to v{version} [skip ci]",
184-
},
185-
"expectedHeadOid": expected_oid,
186-
"fileChanges": {
187-
"additions": [
188-
{"path": "stable.toml", "contents": b64("stable.toml")},
189-
{"path": "dev.toml", "contents": b64("dev.toml")},
190-
]
191-
},
192-
}
193-
},
194-
}
195-
196-
request = urllib.request.Request(
197-
"https://api.github.com/graphql",
198-
data=json.dumps(payload).encode("utf-8"),
199-
headers={
200-
"Authorization": f"Bearer {token}",
201-
"Accept": "application/vnd.github+json",
202-
"Content-Type": "application/json",
203-
},
204-
method="POST",
205-
)
206-
with urllib.request.urlopen(request) as resp:
207-
body = json.loads(resp.read())
208-
if body.get("errors"):
209-
raise SystemExit(f"GraphQL error: {body['errors']}")
210-
commit = body["data"]["createCommitOnBranch"]["commit"]
211-
print(f"bump commit: {commit['oid']} -> {commit['url']}")
212-
213-
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fp:
214-
fp.write(f"branch={branch}\n")
215-
fp.write(f"oid={commit['oid']}\n")
216-
PY
217-
- name: Open PR for the version bump
218-
env:
219-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
220-
VERSION: ${{ steps.version.outputs.version }}
221-
BRANCH: ${{ steps.bump_commit.outputs.branch }}
222-
run: |
223-
gh pr create \
224-
--base main \
225-
--head "$BRANCH" \
226-
--title "Bump version to v${VERSION} [skip ci]" \
227-
--body "Automated patch bump emitted by the stable publish workflow. [skip ci]"
228-
- name: Try to auto-merge the bump PR
229-
env:
230-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
231-
BRANCH: ${{ steps.bump_commit.outputs.branch }}
232-
run: |
233-
gh pr merge "$BRANCH" --squash --auto --delete-branch \
234-
|| echo "auto-merge unavailable; PR left open for manual merge"
235-
- name: Use stable.toml as pyproject.toml
236-
run: cp stable.toml pyproject.toml
237-
- name: Build sdist and wheel
238-
run: python -m build
239-
- name: Twine check
240-
run: twine check dist/*
241-
- name: Twine upload to PyPI
242-
env:
243-
TWINE_USERNAME: __token__
244-
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
245-
run: twine upload --non-interactive dist/*
246-
- name: Create GitHub Release
247-
env:
248-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
249-
BUMP_OID: ${{ steps.bump_commit.outputs.oid }}
250-
run: |
251-
gh release create "v${{ steps.version.outputs.version }}" dist/* \
252-
--title "v${{ steps.version.outputs.version }}" \
253-
--generate-notes \
254-
--target "$BUMP_OID"

.github/workflows/publish.yml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
7+
permissions:
8+
contents: write
9+
10+
concurrency:
11+
group: publish-pypi
12+
cancel-in-progress: false
13+
14+
jobs:
15+
publish:
16+
runs-on: ubuntu-latest
17+
if: "!contains(github.event.head_commit.message, 'chore: bump version')"
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.12"
27+
28+
- name: Install build tools
29+
run: |
30+
python -m pip install --upgrade pip
31+
pip install build twine
32+
33+
- name: Bump patch version in stable.toml and dev.toml
34+
id: bump
35+
run: |
36+
python <<'EOF'
37+
import os
38+
import pathlib
39+
import re
40+
41+
def bump(path: pathlib.Path) -> str:
42+
text = path.read_text(encoding="utf-8")
43+
match = re.search(r'^version\s*=\s*"(\d+)\.(\d+)\.(\d+)"', text, re.MULTILINE)
44+
if not match:
45+
raise SystemExit(f"Could not find version in {path}")
46+
major, minor, patch = (int(part) for part in match.groups())
47+
new_version = f"{major}.{minor}.{patch + 1}"
48+
new_text = re.sub(
49+
r'^(version\s*=\s*)"\d+\.\d+\.\d+"',
50+
rf'\1"{new_version}"',
51+
text,
52+
count=1,
53+
flags=re.MULTILINE,
54+
)
55+
path.write_text(new_text, encoding="utf-8")
56+
return new_version
57+
58+
stable_version = bump(pathlib.Path("stable.toml"))
59+
dev_version = bump(pathlib.Path("dev.toml"))
60+
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
61+
output.write(f"new_version={stable_version}\n")
62+
output.write(f"dev_version={dev_version}\n")
63+
print(f"stable.toml -> {stable_version}")
64+
print(f"dev.toml -> {dev_version}")
65+
EOF
66+
67+
- name: Use stable.toml as pyproject.toml
68+
run: cp stable.toml pyproject.toml
69+
70+
- name: Build distribution
71+
run: python -m build
72+
73+
- name: Publish to PyPI
74+
env:
75+
TWINE_USERNAME: __token__
76+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
77+
run: twine upload --non-interactive dist/*
78+
79+
- name: Commit and tag version bump
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 stable.toml dev.toml
84+
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }}"
85+
git tag "v${{ steps.bump.outputs.new_version }}"
86+
git push origin main
87+
git push origin "v${{ steps.bump.outputs.new_version }}"
88+
89+
- name: Create GitHub Release
90+
env:
91+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
92+
run: |
93+
gh release create "v${{ steps.bump.outputs.new_version }}" \
94+
dist/* \
95+
--title "v${{ steps.bump.outputs.new_version }}" \
96+
--generate-notes

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ automation_file/
9595
- `main` branch: stable releases, publishes `automation_file` to PyPI (version in `stable.toml`).
9696
- `dev` branch: development, publishes `automation_file_dev` to PyPI (version in `dev.toml`).
9797
- Keep `dependencies` and `[project.optional-dependencies]` (`dev`) in sync across both TOMLs. Backends (`boto3`, `azure-storage-blob`, `dropbox`, `paramiko`) and `PySide6` are first-class runtime deps — do not move them back under extras.
98-
- **Version bumping is automatic.** The stable publish job bumps the patch in both `stable.toml` and `dev.toml`, commits the bump back to `main` with `[skip ci]`, then builds and releases. Do not hand-bump before merging to `main`.
98+
- **Version bumping is automatic.** A dedicated publish workflow bumps the patch in both `stable.toml` and `dev.toml`, builds, uploads to PyPI, then commits the bump back to `main` tagged as `vX.Y.Z`. Do not hand-bump before merging to `main`. The next publish run is skipped via a commit-message guard (`chore: bump version`), so the bump itself never re-triggers publishing.
9999
- CI: GitHub Actions (Windows, Python 3.10 / 3.11 / 3.12) — one matrix workflow per branch: `.github/workflows/ci-dev.yml`, `.github/workflows/ci-stable.yml`.
100100
- CI steps: `lint` (ruff check + ruff format --check + mypy) → `pytest` with coverage → uploads `coverage.xml` as an artifact.
101-
- Stable branch additionally runs a `publish` job on push to `main`: auto-bumps the patch in both TOMLs and commits back, then builds the sdist + wheel, `twine check`, `twine upload` using `PYPI_API_TOKEN`, then `gh release create v<version> --generate-notes`.
101+
- Publishing lives in a separate workflow (`.github/workflows/publish.yml`) that runs on push to `main`: bumps both TOMLs, copies `stable.toml` to `pyproject.toml`, builds the sdist + wheel, `twine upload` via `PYPI_API_TOKEN`, then commits + tags + pushes and creates `gh release create v<version> --generate-notes`.
102102
- `pre-commit` is configured (`.pre-commit-config.yaml`): trailing-whitespace, eof-fixer, check-yaml, check-toml, check-added-large-files, ruff, ruff-format, mypy. Install with `pre-commit install` after cloning.
103103

104104
## Development

0 commit comments

Comments
 (0)