55 branches : [ main ]
66
77permissions :
8- contents : write
8+ contents : read
99 id-token : write
1010
1111jobs :
1212 build-and-publish :
1313 runs-on : ubuntu-latest
1414
1515 steps :
16- - name : Check out repo (full history for diff & tags )
16+ - name : Check out repo (full history for diff)
1717 uses : actions/checkout@v4
1818 with :
1919 fetch-depth : 0
@@ -32,40 +32,48 @@ jobs:
3232 run : |
3333 set -euo pipefail
3434 python - << 'PY'
35- import io, os, subprocess, sys, re, textwrap, datetime
35+ import io, os, subprocess, sys
3636 try:
3737 import tomllib # py311+
38- except Exception as e:
39- print("tomllib missing", file=sys.stderr); raise
38+ except Exception:
39+ print("tomllib missing", file=sys.stderr)
40+ sys.exit(1)
4041
4142 def load_version_from_bytes(b: bytes):
4243 return tomllib.load(io.BytesIO(b))["project"]["version"]
4344
44- # Current version (from workspace)
45- with open("pyproject.toml","rb") as f:
45+ # current version
46+ with open("pyproject.toml", "rb") as f:
4647 current = load_version_from_bytes(f.read())
4748
48- # Previous version ( from commit before this push)
49+ # previous version from the commit before this push (GitHub provides it )
4950 before = os.environ.get("BEFORE_SHA") or ""
5051 prev = None
5152 if before and before != "0000000000000000000000000000000000000000":
5253 try:
53- blob = subprocess.check_output(["git","show",f"{before}:pyproject.toml"])
54+ blob = subprocess.check_output(["git", "show", f"{before}:pyproject.toml"])
5455 prev = load_version_from_bytes(blob)
5556 except subprocess.CalledProcessError:
56- prev = None
57+ prev = None # file may not have existed
5758
5859 changed = (prev is None) or (current != prev)
5960
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}")
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}")
6669 PY
6770
68- # ---------- Build & Publish to PyPI (only if version changed) ----------
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+
6977 - name : Build sdist + wheel
7078 if : steps.ver.outputs.changed == 'true'
7179 run : |
@@ -77,123 +85,4 @@ jobs:
7785 if : steps.ver.outputs.changed == 'true'
7886 uses : pypa/gh-action-pypi-publish@release/v1
7987 with :
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
88+ skip-existing : true
0 commit comments