Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 46 additions & 15 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
name: dist
path: dist/

constraints:
release:
name: Attach locked constraints to release
runs-on: ubuntu-latest
permissions:
Expand All @@ -35,6 +35,8 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Need history back to the previous tag to build the compare link.
fetch-depth: 0

- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
Expand All @@ -50,24 +52,53 @@ jobs:
uv export --frozen --no-emit-project --no-hashes --format requirements.txt \
-o "constraints-${VERSION}.txt"

# Runs independently of the PyPI publish so a transient release-API delay
# (semantic-release pushes the tag just before creating the release) can
# never block publishing. Retry tolerates that race; --clobber is idempotent.
- name: Attach constraints file to the release
# Reproduces the body semantic-release attached via --vcs-release: the
# CHANGELOG.md section for this version (including its "## vX.Y.Z (date)"
# heading), plus a compare-link footer. The heading is matched literally
# (index/==) so version numbers are never treated as regex patterns.
- name: Build release notes from changelog
run: |
awk -v tag="v${VERSION}" '
index($0, "## " tag " ") == 1 || $0 == "## " tag {found=1; print; next}
found && /^## v/ {exit}
found {print}
' CHANGELOG.md > release-notes.md
Comment on lines +61 to +65
# An empty file means the heading was not found (e.g. changelog format
# drift); fail loudly rather than publish a blank-body release.
if [ ! -s release-notes.md ]; then
echo "::error::No CHANGELOG.md section found for ${GITHUB_REF_NAME}; check heading format"
exit 1
fi
prev=$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" 2>/dev/null || true)
if [ -n "$prev" ]; then
printf '\n---\n\n**Detailed Changes**: [%s...%s](https://github.com/%s/compare/%s...%s)\n' \
"$prev" "${GITHUB_REF_NAME}" "${GITHUB_REPOSITORY}" "$prev" "${GITHUB_REF_NAME}" \
>> release-notes.md
fi

# Immutable releases lock their assets at publish time, so the constraints
# file must be attached while the release is still a draft, then published.
# semantic-release no longer creates the release (--no-vcs-release in
# release.yml); this job owns it end to end. The create step is idempotent
# so a re-run after a partial failure refreshes the draft's asset instead
# of erroring on an already-existing release, and the asset is verified
# present before the release is published and locked.
- name: Publish release with constraints attached
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF_NAME}"
file="constraints-${VERSION}.txt"
for attempt in 1 2 3 4 5; do
if gh release upload "${GITHUB_REF_NAME}" "$file" --clobber; then
echo "Attached $file to ${GITHUB_REF_NAME}"
exit 0
fi
echo "Release ${GITHUB_REF_NAME} not ready (attempt $attempt/5); waiting 15s..."
sleep 15
done
echo "::error::Failed to attach $file to ${GITHUB_REF_NAME} after 5 attempts"
exit 1
if gh release view "$tag" >/dev/null 2>&1; then
gh release upload "$tag" "$file" --clobber
else
gh release create "$tag" "$file" \
--draft \
--title "$tag" \
--notes-file release-notes.md
fi
gh release view "$tag" --json assets --jq '.assets[].name' | grep -qxF "$file"
gh release edit "$tag" --draft=false

publish:
name: Publish to PyPI
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ jobs:
- name: Install python-semantic-release
run: pip install python-semantic-release==9.21.0

# --no-vcs-release: the GitHub release is created by publish.yml (on the
# tag push) so the locked constraints file can be attached before the
# release is published and locked by immutable releases.
- name: Run semantic-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: semantic-release version --push --tag --changelog --vcs-release
run: semantic-release version --push --tag --changelog --no-vcs-release