Skip to content
Merged
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
44 changes: 32 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ on:
workflow_dispatch:
inputs:
version_bump:
description: "Version bump type. `none` publishes the version already in the repo, which is what the first release needs."
description: "Version bump type."
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
- none
pre_release:
description: "Publish a release candidate (`<version>rcN`) instead of the version itself. Dispatch again with this off to promote the same version to stable."
description: "Publish a release candidate (`<version>rcN`) instead of the version itself. Dispatch again with the same bump and this off to promote it to stable."
required: false
default: false
type: boolean
Expand All @@ -31,8 +30,8 @@ jobs:
release-and-publish:
runs-on: ubuntu-latest
# Dispatchable from any branch by anyone who can dispatch it, and a
# candidate or a `none` bump reaches PyPI without a commit to review: the
# published artefact must come from the reviewed branch.
# candidate reaches PyPI without a commit to review: the published artefact
# must come from the reviewed branch.
if: github.ref == 'refs/heads/main'
permissions:
contents: write
Expand Down Expand Up @@ -150,25 +149,21 @@ jobs:

# Publishing is the one step that cannot be undone, so it runs last, and
# the release is created as a draft so a publish that fails leaves
# nothing public. What a failure before the publish leaves behind, in
# cleanup order: the draft release, the tag, the release branch, and the
# bump commit on main.
# nothing public. Until then the bump commit exists only in the tag, so
# a failed publish leaves main untouched.
- name: Commit version bump and create draft release
env:
RELEASE_NOTES: ${{ github.event.inputs.release_notes }}
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
NEW_VERSION="${{ steps.version.outputs.version }}"

# A pre-release leaves the committed version alone, and `none`
# publishes the version already in the file, so both reach here with
# nothing to commit.
# A pre-release leaves the committed version alone.
if [ "${{ github.event.inputs.pre_release }}" = "true" ]; then
git checkout -- src/unstract_cli/__init__.py
elif ! git diff --quiet; then
git add src/unstract_cli/__init__.py
git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
git push origin main
fi

git tag "v$NEW_VERSION"
Expand All @@ -194,6 +189,31 @@ jobs:
- name: Publish to PyPI
run: uv publish

# Everything the run created before the publish, so a rerun after the
# cause is fixed starts clean rather than on the tag guard. Each removal
# tolerates its target being absent: the failure may have come partway
# through creating them.
- name: Remove the tag and draft release a failed publish leaves behind
if: failure() && steps.version.outcome == 'success'
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
NEW_VERSION="${{ steps.version.outputs.version }}"
# The wheel and the sdist upload separately, so a failed publish can
# still have put the version on PyPI; then the tag has to stay.
if curl -fsS -o /dev/null "https://pypi.org/pypi/unstract-cli/$NEW_VERSION/json"; then
echo "PyPI serves $NEW_VERSION; keeping the tag and draft release." >&2
exit 0
fi
gh release delete "v$NEW_VERSION" --yes || true
git push origin ":refs/tags/v$NEW_VERSION" || true
Comment thread
greptile-apps[bot] marked this conversation as resolved.

# If this fails the published version is on PyPI and in the tag but not
# on main; `git push origin v<version>:main` finishes the job by hand.
- name: Push version bump
if: github.event.inputs.pre_release != 'true'
run: git push origin main

- name: Publish release
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
Expand Down
2 changes: 1 addition & 1 deletion src/unstract_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Unstract CLI."""

__version__ = "0.1.0"
__version__ = "0.0.0"
Comment thread
chandrasekharan-zipstack marked this conversation as resolved.
36 changes: 36 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,39 @@ def test_the_release_goes_public_only_once_the_package_is_on_pypi() -> None:
assert creates.count("gh release create") == creates.count("--draft")
assert "--draft=false" in by_name["Publish release"]["run"]
assert names.index("Publish release") > names.index("Publish to PyPI")


def _release_steps() -> list[dict]:
return _load(Path(__file__).resolve().parents[1] / ".github/workflows/release.yml")[
"jobs"
]["release-and-publish"]["steps"]


def test_main_moves_only_once_the_package_is_on_pypi() -> None:
"""A bump commit pushed before the publish is the one leftover of a failed
run that cannot be deleted without rewriting main."""
steps = _release_steps()
names = [step.get("name", "") for step in steps]
by_name = {step.get("name", ""): step for step in steps}

assert (
"git push origin main"
not in by_name["Commit version bump and create draft release"]["run"]
)
assert names.index("Push version bump") > names.index("Publish to PyPI")


def test_a_failed_publish_removes_what_it_created_and_nothing_published() -> None:
"""The cleanup runs on failure and sits directly after the publish: a step
that fails later, with the version already on PyPI, must not delete the
tag that names it."""
steps = _release_steps()
names = [step.get("name", "") for step in steps]
cleanup = "Remove the tag and draft release a failed publish leaves behind"

assert "failure()" in steps[names.index(cleanup)]["if"]
assert names.index(cleanup) == names.index("Publish to PyPI") + 1
run = steps[names.index(cleanup)]["run"]
assert "gh release delete" in run
assert ":refs/tags/" in run
assert run.index("pypi.org/pypi/unstract-cli/") < run.index("gh release delete")
Loading