Skip to content

Fix release workflow: full-history tag checkout and manual release of existing tags - #11

Merged
krishika08 merged 1 commit into
mainfrom
fix/release-workflow-full-history
Nov 12, 2026
Merged

krishika08 merged 1 commit into
mainfrom
fix/release-workflow-full-history

Conversation

@krishika08

Copy link
Copy Markdown
Owner

Summary

Fixes the release workflow failure for v1.2.0 and adds a safe way to release an existing tag with the fixed workflow. The v1.2.0 tag is left exactly where it is (aa7ba70).

This PR does not publish anything. It changes only .github/workflows/release.yml, adds a test for it and updates docs/pypi_trusted_publishing.md.

Root cause

Release run 43172834000 (tag push v1.2.0 → aa7ba70): validation failed with 454 passed, 20 failed and 10 skipped. Build and publish were skipped.

  • The validate job used actions/checkout@v4 with its defaults: fetch-depth: 1 and no tags.
  • All 20 failures are in tests/test_stage3_*, the history and provenance tests. They call git rev-parse v0.1.0-baseline^{commit}, git merge-base --is-ancestor 4f78cc3 … and similar, and fail with exit 128 because those objects aren't in the checkout.
  • ci.yml and the release build job already use fetch-depth: 0. That is why CI on aa7ba70 passed.

Reproduced locally with Python 3.11 and .[dev,mcp,api] installed, as in the release job:

Checkout of v1.2.0 Result
Shallow clone (depth 1, no tags), like the old job 20 failed, 451 passed, 13 skipped
Full clone (all history and tags), like the new job 471 passed, 0 failed, 13 skipped

The 13 skips are the 6 seen in CI for missing external benchmark data and corpus, plus 7 because Windows doesn't allow file-symlink creation. On Linux those 7 run, and the 4 Windows-only junction tests skip instead.

Workflow design

push tag v*  ─┐
              ├─► validate ──(validated commit)──► build ──(artifacts)──► publish (pypi env, OIDC)
dispatch(tag) ┘   contents:read                    contents:read         id-token:write
  • RELEASE_TAG (workflow env) is inputs.tag for a dispatch and github.ref_name for a tag push. Shell steps read it only through the environment, never through ${{ }} inside a script.
  • validate
    1. Before checkout: checks the tag format (vX.Y.Z, optionally aN/bN/rcN and .postN), then checks the trigger:

      • push: GITHUB_REF must equal refs/tags/$RELEASE_TAG;
      • dispatch: GITHUB_REF must equal refs/heads/main;
      • any other event is rejected.
    2. Checks out refs/tags/$RELEASE_TAG with fetch-depth: 0 and persist-credentials: false.

    3. Checks that the tag exists and that HEAD is the tag's commit, then:

      • push: also checks the tag still points to the pushed commit (GITHUB_SHA);
      • dispatch: requires the tag commit to be an ancestor of origin/main.

      It exports the commit SHA as a job output.

    4. Checks that the tag version, pyproject.toml and reponeedle.__version__ agree, then runs the full suite and the strict REPONEEDLE_FAIL_ON_SKIP=1 suites. These steps are unchanged except that they read RELEASE_TAG.

  • build
    • Checks out refs/tags/$RELEASE_TAG with fetch-depth: 0.
    • Refuses to build unless HEAD equals both the tag commit and the commit validate tested.
    • Build, twine check --strict, the artifact file-name version check (now using RELEASE_TAG) and SHA-256 checksums are unchanged.
  • publish: unchanged. It needs [validate, build], uses the pypi environment, and is the only job with id-token: write. It does not check out code; it downloads the build artifacts.
  • concurrency: release-<tag> (no cancellation) stops a tag-push run and a manual run of the same tag from overlapping.
  • The tag-push trigger v* is preserved.
    • The format check is new for tag pushes too. Historical non-release tags such as v0.1.0-baseline already failed the version check; they now fail earlier.
    • The ancestor-of-main requirement applies to manual dispatch only.

Why a dispatch from main, not a re-run

Re-running run 43172834000 would reuse the workflow file stored at the tag, which still has the shallow checkout. A dispatch from main runs the fixed workflow from main but builds the tag's code. Every check is written inline in the workflow, not in a repository script, because the tag's tree doesn't contain new scripts.

Trusted Publishing compatibility

  • PyPI publisher. The provenance for the 1.1.0 upload shows the configured publisher as {repository: krishika08/RepoNeedle, workflow: release.yml, environment: pypi}. A dispatched run sends the same repository, workflow file and environment in its OIDC token. PyPI's GitHub publisher does not restrict the event or ref, so no trust setting needs to change or be relaxed.
  • GitHub environment. The pypi environment currently has a required reviewer (krishika08) and no branch or tag rule, so a run from main can reach the deployment gate and still waits for approval.
    • The doc used to recommend a tags-only v* rule, which would block dispatches.
    • It now recommends allowing v* tags and the main branch.

Validation

Check Result
actionlint 1.7.12 on release.yml and ci.yml No issues
tests/test_release_workflow.py (new, 11 tests) Passed. Covers: triggers, required input, RELEASE_TAG expression, tag checkout with full history in both jobs, guard ordering and patterns, validated-commit handoff, version checks, no ${{ }} in scripts, permissions, needs, pypi environment, strict suites, twine, checksums, and publish not checking out code
The workflow's own guard scripts, taken verbatim from the YAML and run with bash -eo pipefail against a fresh clone of this repository 20/20 scenarios as expected (see below)
Version-check step at the v1.2.0 checkout RELEASE_TAG=v1.2.0 passes (1.2.0 = 1.2.0 = 1.2.0). RELEASE_TAG=v1.1.0 is rejected (mismatch)
Build at v1.2.0 → twine check --strict → artifact-name step reponeedle-1.2.0 wheel and sdist; PASSED; PASSED (wheel Version: 1.2.0)
Full suite on this branch (Windows, Python 3.12) 506 passed, 7 skipped (file-symlink creation not permitted on this host)
Guard scenarios (20/20)
Scenario Expected Result
Tag push v1.2.0 accept accept
Dispatch v1.2.0 from main accept accept
Dispatch from a feature branch reject reject (must be dispatched from main)
Dispatch from a tag ref reject reject
Dispatch with tag main, 1.2.0, v1.2.0;id or empty reject reject (format)
Push of v0.1.0-baseline reject reject (format)
Push where GITHUB_REF ≠ tag reject reject
Push: HEAD = tag = pushed commit accept accept, outputs aa7ba70…
Dispatch v1.2.0: HEAD = tag, on main accept accept, outputs aa7ba70…
Dispatch of nonexistent v9.9.9 reject reject (does not exist)
HEAD is not the tag commit reject reject
Push: tag moved after the push reject reject
Dispatch: tag commit not on main (local-only test tag) reject reject (is not on main)
Build: HEAD = tag = validated commit accept accept
Build: validated commit differs, is missing, or HEAD ≠ tag reject reject (3 cases)

Not done: no live dispatch was run. Before merge, a dispatch would run the old definition or need an untested ref, and main is the only ref allowed to publish. The live checks are the CI run on this PR and, after merge, the manual release itself.

After merge (not part of this PR)

gh workflow run release.yml --ref main -f tag=v1.2.0

It then needs approval at the pypi environment gate.

Not changed

  • The v1.2.0 tag: not moved, deleted, recreated or force-updated.
  • Package version 1.2.0, the retrieval code, benchmarks, ci.yml, the Hugging Face Space and the demo API.
  • No PyPI publication. The old failed run was not re-run.

🤖 Generated with Claude Code

…isting tags

The v1.2.0 tag-push run failed in validation (20 failed) because the validate
job used the default shallow checkout (fetch-depth 1, no tags). The test suite
checks historical tags and commits such as v0.1.0-baseline, which a shallow
checkout lacks. The tagged code itself passes: CI on aa7ba70 uses full history.

- validate and build check out refs/tags/<tag> with fetch-depth 0, so all
  history and tags are present, and without persisted credentials.
- Add workflow_dispatch with a required `tag` input, so an existing tag can be
  released with the current workflow definition instead of the one stored in
  the tag. Dispatch is accepted only from main. The tag must match
  vX.Y.Z[aN|bN|rcN][.postN], exist, and point to a commit on main.
- validate verifies HEAD equals the tag commit (and the pushed commit for tag
  pushes) and exports it. build refuses to build anything but that commit.
- Version validation and artifact checks use the release tag from either
  event (RELEASE_TAG), passed to scripts only through the environment.
- Serialise runs per tag with a concurrency group.

Unchanged: tag-push trigger, read-only validate/build permissions, publish
needs validate and build, id-token: write only in publish, pypi environment,
REPONEEDLE_FAIL_ON_SKIP strict suites, twine --strict, artifact version
checks and SHA-256 checksums.

tests/test_release_workflow.py checks these invariants.
docs/pypi_trusted_publishing.md describes the three-job pipeline and the
manual release path, and notes that a pypi environment branch rule must
allow main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@krishika08
krishika08 merged commit 1091bf8 into main Nov 12, 2026
5 checks passed
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.

1 participant