Skip to content

Tag releases again - #326

Merged
vahid-ahmadi merged 2 commits into
mainfrom
fix/git-tags-on-release
Sep 18, 2026
Merged

vahid-ahmadi merged 2 commits into
mainfrom
fix/git-tags-on-release

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Releases have been going to PyPI untagged since v0.4.4, while the version reached 1.5.2 — roughly thirty releases with no tag behind them. No tags means no GitHub releases, which means Zenodo has nothing to archive and cannot mint the DOI the JOSS submission needs.

Cause

.github/publish-git-tag.sh was:

git tag `python .github/fetch_version.py`  # create a new tag
git push --tags || true  # update the repository version

.github/fetch_version.py has never existed in this repository. git log --all -- .github/fetch_version.py returns nothing. So the command substitution failed, git tag received an empty argument and errored, and || true discarded it. The step has been reporting success while doing nothing for about two years.

The change

  • Read the version from pyproject.toml using the same regex .github/bump_version.py already uses, rather than a helper that does not exist.
  • Drop || true from the workflow step, so a failure to tag fails the job instead of passing silently.
  • Exit cleanly when the tag already exists locally or on the remote, so reruns and backfilling old versions are safe.
  • Resolve the interpreter with command -v python || command -v python3.
  • Add permissions: contents: write to the publish job, which had no permissions block at all and so depended on whatever the repository default happens to be. Pushing a tag needs it.

Disclosure: I pushed the v1.5.2 tag by accident

While testing the script's idempotency I ran it from a scratch copy of this repository that still had origin pointing at PolicyEngine/microdf. The second run took the "tag does not exist" branch and did what it is designed to do — it pushed. That was an unintended write to a shared repository, and the test should have pointed at a throwaway remote.

The tag it created is correct: v1.5.2 points at de08443, "Update package version", which is the commit that set the version to 1.5.2 and is the current tip of main, matching what is on PyPI. So the repository now has its first correct release tag in thirty releases, by accident rather than by design.

If you would rather the workflow created it, git push --delete origin v1.5.2 removes it and the next release will tag normally. Either way the code change stands on its own.

Verification

Both early-exit paths tested:

=== tag already exists locally:
Tag v1.5.2 already exists locally; nothing to do.
exit=0

and the push path is what produced the tag described above, so it is confirmed working rather than merely reviewed.

After this merges

  1. Backfill tags for past versions, or accept that history starts here.
  2. Enable the Zenodo–GitHub integration for this repository.
  3. Create a GitHub release from a tag; Zenodo then mints the DOI.

publish-git-tag.sh called 'python .github/fetch_version.py'. That file
has never existed in this repository, so the command failed, git tag
received an empty argument, and '|| true' discarded the error. Releases
have gone to PyPI untagged since v0.4.4 while the version reached 1.5.2.

Reads the version from pyproject.toml with the same regex bump_version.py
uses, and drops the '|| true' so a failure surfaces. The script now exits
cleanly when the tag already exists locally or on the remote, so reruns
and backfills are safe.

The publish job also had no permissions block, so it relied on whatever
the repository default happens to be; contents: write is now explicit.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed the diagnosis and checked the CI preconditions the new script depends on, not just the script.

Diagnosis. The last real publish job (105179078388, the 1.5.2 release) logged python: can't open file '.../.github/fetch_version.py' followed by Everything up-to-date, so the tag step has been a silent no-op. The accidental v1.5.2 tag points at de08443, which is the tip of main and the commit that set the version to 1.5.2, so it is correct. I would keep it.

Script. Ran it against a throwaway bare remote from a full clone and from a depth-1 clone. It pushes when the tag is absent, exits 0 when the tag exists locally or only on the remote, works with only python3 on PATH and with no git identity configured, and exits 1 when pyproject.toml has no version line. The regex matches the one in bump_version.py.

CI preconditions.

  • The publish job's checkout is actions/checkout@v6 with no token override. v6 still defaults persist-credentials to true, so the default token is in the git config and the push authenticates. fetch-depth: 0 also fetches tags, so the local check sees existing ones.
  • Job-level contents: write is what the push needs and overrides the org default.
  • The only ruleset on the repo is main (branch target, deletion and non-fast-forward). No tag rulesets and no legacy tag protection.
  • The bump commit is pushed with the App token, so it re-triggers the workflow and the publish job checks out that exact commit. The tag lands on the "Update package version" commit, as v1.5.2 does now.
  • skip-existing: true on the PyPI step plus the script's early exits make a rerun after a partial failure safe in either direction.

One suggestion, not blocking: run the tag step after the PyPI upload. As ordered now, a tag failure blocks the release. Everything above checks out, but the tag push is the one step not yet proven in CI, and moving it after pypa/gh-action-pypi-publish costs nothing: with skip-existing: true a rerun is still safe, and a surprise can never hold back a release.

Merge order. Suggest merging this first and alone, so the first real run tags 1.5.3 with nothing user-facing in it, and waiting for both workflow runs to finish before merging #324 or #325. Two Versioning runs in flight at once race on the same version number.

Per review: as ordered, a tag failure blocked the release. The tag push
is the one step not yet exercised in CI, and skip-existing on the upload
keeps a rerun safe either way.
@vahid-ahmadi

Copy link
Copy Markdown
Contributor Author

Taken — the tag step now runs after pypa/gh-action-pypi-publish, with a comment recording why. You are right that it is the one step not yet exercised in CI, and there is no reason for it to be able to hold back an upload.

Thank you for checking the CI preconditions rather than just the script; persist-credentials defaulting to true on checkout@v6 and the absence of any tag ruleset were both load-bearing assumptions I had not verified.

Keeping v1.5.2, as you suggest.

On merge order: agreed, and I will not merge this myself — @vahid-ahmadi is reviewing. Noting here for whoever does that this should go in first and alone, and both Versioning runs should finish before #324 or #325 follow, so the two do not race on the same version number.

@vahid-ahmadi
vahid-ahmadi merged commit df01f2b into main Sep 18, 2026
8 checks passed
@vahid-ahmadi
vahid-ahmadi deleted the fix/git-tags-on-release branch September 18, 2026 11:10
vahid-ahmadi added a commit that referenced this pull request Sep 18, 2026
Per review: the release step sat after the two early exits, so it ran
only when the script also pushed a new tag. Whenever the tag already
existed the script exited 0 before reaching it - which meant a rerun
after a failed release create went green having done nothing, the same
silent failure #326 removed for tags, reintroduced for releases.

The tag section is now an if/else that skips tagging rather than
exiting, so the release check is always reached. Simulated against a
bare remote with a stub gh: a fresh tag pushes then creates; an existing
tag with no release still creates; both present exits 0 after one
release view; a failing create turns the job red.

Adds --verify-tag, so gh aborts if the tag is absent from the remote
rather than creating one itself at the default branch head.

Release notes now come from the towncrier CHANGELOG section for the
version, falling back to --generate-notes when that section is absent.
vahid-ahmadi added a commit that referenced this pull request Sep 18, 2026
* Create the GitHub release automatically, not just the tag

Zenodo archives on the GitHub release, not on the tag, so a tag alone
leaves the DOI pointing at whatever was last released by hand. v1.5.5 was
created manually for the JOSS submission; without this, the next archived
version would be whenever someone remembered to click the button, while
PyPI moved ahead.

The tag step now creates the release too, skipping if one already exists,
so reruns and the existing v1.5.5 are safe. The job already declares
contents: write, which is what gh release create needs; it just needs
GH_TOKEN in the environment.

* Reach the release check on every run, not only on a fresh tag

Per review: the release step sat after the two early exits, so it ran
only when the script also pushed a new tag. Whenever the tag already
existed the script exited 0 before reaching it - which meant a rerun
after a failed release create went green having done nothing, the same
silent failure #326 removed for tags, reintroduced for releases.

The tag section is now an if/else that skips tagging rather than
exiting, so the release check is always reached. Simulated against a
bare remote with a stub gh: a fresh tag pushes then creates; an existing
tag with no release still creates; both present exits 0 after one
release view; a failing create turns the job red.

Adds --verify-tag, so gh aborts if the tag is absent from the remote
rather than creating one itself at the default branch head.

Release notes now come from the towncrier CHANGELOG section for the
version, falling back to --generate-notes when that section is absent.

* Decide tagging on the remote, not the local tag store

gh release create --verify-tag reads the tag from the remote, so a tag
that exists only locally must still be pushed; the old local-first branch
skipped the push and made the release step abort.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants