Skip to content

Commit d110371

Browse files
committed
ci: update release
1 parent 7b43140 commit d110371

File tree

1 file changed

+138
-27
lines changed

1 file changed

+138
-27
lines changed

.github/workflows/publish.yaml

Lines changed: 138 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ name: Publish to PyPI on version change
22

33
on:
44
push:
5-
branches: [ main ]
5+
branches: [ main , dev]
66

77
permissions:
8-
contents: read
8+
contents: write
99
id-token: write
1010

1111
jobs:
1212
build-and-publish:
1313
runs-on: ubuntu-latest
1414

1515
steps:
16-
- name: Check out repo (full history for diff)
16+
- name: Check out repo (full history for diff & tags)
1717
uses: actions/checkout@v4
1818
with:
1919
fetch-depth: 0
@@ -32,48 +32,40 @@ jobs:
3232
run: |
3333
set -euo pipefail
3434
python - << 'PY'
35-
import io, os, subprocess, sys
35+
import io, os, subprocess, sys, re, textwrap, datetime
3636
try:
3737
import tomllib # py311+
38-
except Exception:
39-
print("tomllib missing", file=sys.stderr)
40-
sys.exit(1)
38+
except Exception as e:
39+
print("tomllib missing", file=sys.stderr); raise
4140
4241
def load_version_from_bytes(b: bytes):
4342
return tomllib.load(io.BytesIO(b))["project"]["version"]
4443
45-
# current version
46-
with open("pyproject.toml", "rb") as f:
44+
# Current version (from workspace)
45+
with open("pyproject.toml","rb") as f:
4746
current = load_version_from_bytes(f.read())
4847
49-
# previous version from the commit before this push (GitHub provides it)
48+
# Previous version (from commit before this push)
5049
before = os.environ.get("BEFORE_SHA") or ""
5150
prev = None
5251
if before and before != "0000000000000000000000000000000000000000":
5352
try:
54-
blob = subprocess.check_output(["git", "show", f"{before}:pyproject.toml"])
53+
blob = subprocess.check_output(["git","show",f"{before}:pyproject.toml"])
5554
prev = load_version_from_bytes(blob)
5655
except subprocess.CalledProcessError:
57-
prev = None # file may not have existed
56+
prev = None
5857
5958
changed = (prev is None) or (current != prev)
6059
61-
with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
62-
fh.write(f"current={current}\n")
63-
fh.write(f"previous={prev or ''}\n")
64-
fh.write(f"changed={'true' if changed else 'false'}\n")
65-
66-
print(f"Current version: {current}")
67-
print(f"Previous version: {prev}")
68-
print(f"Changed: {changed}")
60+
# Stash values for later steps
61+
with open(os.environ["GITHUB_OUTPUT"],"a") as out:
62+
out.write(f"current={current}\n")
63+
out.write(f"previous={prev or ''}\n")
64+
out.write(f"changed={'true' if changed else 'false'}\n")
65+
print(f"Current version: {current}\nPrevious version: {prev}\nChanged: {changed}")
6966
PY
7067
71-
- name: Show decision
72-
run: |
73-
echo "current=${{ steps.ver.outputs.current }}"
74-
echo "previous=${{ steps.ver.outputs.previous }}"
75-
echo "changed=${{ steps.ver.outputs.changed }}"
76-
68+
# ---------- Build & Publish to PyPI (only if version changed) ----------
7769
- name: Build sdist + wheel
7870
if: steps.ver.outputs.changed == 'true'
7971
run: |
@@ -85,4 +77,123 @@ jobs:
8577
if: steps.ver.outputs.changed == 'true'
8678
uses: pypa/gh-action-pypi-publish@release/v1
8779
with:
88-
skip-existing: true
80+
skip-existing: true
81+
82+
# ---------- Prepare CHANGELOG update & release notes ----------
83+
- name: Prepare CHANGELOG update & release notes
84+
id: cl
85+
if: steps.ver.outputs.changed == 'true'
86+
env:
87+
VERSION: ${{ steps.ver.outputs.current }}
88+
PREV_VERSION: ${{ steps.ver.outputs.previous }}
89+
REPO: ${{ github.repository }}
90+
run: |
91+
set -euo pipefail
92+
python - << 'PY'
93+
import os, re, sys, datetime, pathlib
94+
95+
repo = os.environ["REPO"] # e.g., org/repo
96+
version = os.environ["VERSION"] # e.g., 0.1.2
97+
prev = os.environ.get("PREV_VERSION") or "" # e.g., 0.1.1 or ""
98+
99+
changelog_path = pathlib.Path("CHANGELOG.md")
100+
if not changelog_path.exists():
101+
# Some repos use Changelog.md
102+
alt = pathlib.Path("Changelog.md")
103+
if alt.exists():
104+
changelog_path = alt
105+
else:
106+
print("No CHANGELOG.md found", file=sys.stderr)
107+
sys.exit(1)
108+
109+
text = changelog_path.read_text(encoding="utf-8")
110+
111+
# Grab the [Unreleased] block (non-greedy until the next version header or EOF)
112+
unreleased_re = re.compile(r"^## \\[Unreleased\\]\\s*(.*?)\\s*(?=^## \\[|\\Z)", re.S | re.M)
113+
m = unreleased_re.search(text)
114+
unreleased_body = (m.group(1).strip() if m else "").strip()
115+
if not unreleased_body:
116+
unreleased_body = "_No notable changes._"
117+
118+
# Build the new version section
119+
today = datetime.date.today().isoformat() # YYYY-MM-DD
120+
new_section = f"## [{version}] - {today}\\n{unreleased_body}\\n\\n"
121+
122+
# Replace the Unreleased section with an empty placeholder
123+
if m:
124+
start, end = m.span()
125+
prefix = text[:m.start()]
126+
suffix = text[m.end():]
127+
# Minimal placeholder keeps it tidy
128+
unreleased_placeholder = "## [Unreleased]\\n\\n"
129+
text = prefix + unreleased_placeholder + suffix
130+
else:
131+
# If there was no [Unreleased], put one at the top under the main title
132+
text = text.replace("## Changelog", "## Changelog\\n\\n## [Unreleased]\\n")
133+
134+
# Insert the new version section right after [Unreleased]
135+
insert_point = text.find("## [Unreleased]")
136+
if insert_point != -1:
137+
# Find end of that line to insert after
138+
insert_point = text.find("\n", insert_point)
139+
if insert_point == -1:
140+
insert_point = len(text)
141+
text = text[:insert_point+1] + new_section + text[insert_point+1:]
142+
else:
143+
# Fallback: prepend
144+
text = new_section + text
145+
146+
# ----- Update link reference section at the bottom -----
147+
# Ensure we have lines for [Unreleased] and the new version comparing prev..current
148+
# Remove any existing [Unreleased]: line to replace cleanly
149+
lines = text.rstrip() .splitlines()
150+
# Find if there's an existing reference block (last contiguous lines that look like "[x]: url")
151+
# We'll just rebuild/append ours safely.
152+
def set_ref(name, url):
153+
ref = f"[{name}]: {url}"
154+
# replace existing or append
155+
for i, ln in enumerate(lines):
156+
if ln.startswith(f"[{name}]"):
157+
lines[i] = ref
158+
return
159+
lines.append(ref)
160+
161+
base = f"https://github.com/{repo}"
162+
# previous version for compare link; if unknown, compare from first commit
163+
prev_tag = f"v{prev}" if prev else "v0.0.0"
164+
set_ref("Unreleased", f"{base}/compare/v{version}...HEAD")
165+
set_ref(version, f"{base}/compare/{prev_tag}...v{version}")
166+
167+
new_text = "\n".join(lines) + "\n"
168+
169+
# Write a temp file for release notes (just the body we moved)
170+
pathlib.Path("RELEASE_NOTES.md").write_text(unreleased_body + "\n", encoding="utf-8")
171+
# And write the updated changelog to a temp file (we only commit after publish)
172+
pathlib.Path("CHANGELOG.updated.md").write_text(new_text, encoding="utf-8")
173+
PY
174+
175+
# ---------- Commit CHANGELOG, tag, and create GitHub Release ----------
176+
- name: Commit updated CHANGELOG
177+
if: steps.ver.outputs.changed == 'true'
178+
run: |
179+
mv CHANGELOG.updated.md CHANGELOG.md
180+
git config user.name "github-actions[bot]"
181+
git config user.email "github-actions[bot]@users.noreply.github.com"
182+
git add CHANGELOG.md
183+
git commit -m "chore(release): v${{ steps.ver.outputs.current }} [skip ci]"
184+
git push
185+
186+
- name: Create and push tag
187+
if: steps.ver.outputs.changed == 'true'
188+
run: |
189+
git tag -a "v${{ steps.ver.outputs.current }}" -m "v${{ steps.ver.outputs.current }}"
190+
git push origin "v${{ steps.ver.outputs.current }}"
191+
192+
- name: Create GitHub Release with notes from CHANGELOG
193+
if: steps.ver.outputs.changed == 'true'
194+
env:
195+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
196+
run: |
197+
gh release create "v${{ steps.ver.outputs.current }}" \
198+
--title "v${{ steps.ver.outputs.current }}" \
199+
--notes-file RELEASE_NOTES.md

0 commit comments

Comments
 (0)