Skip to content

ci: harden release workflow with version-exists gate, success() guard, and Sigstore attestations#411

Open
erichare wants to merge 8 commits into
mainfrom
feat-release-workflow
Open

ci: harden release workflow with version-exists gate, success() guard, and Sigstore attestations#411
erichare wants to merge 8 commits into
mainfrom
feat-release-workflow

Conversation

@erichare

@erichare erichare commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Makes the PyPI release pipeline (release.yml + the reusable _test_release.yml) safer and more robust. Three independent hardening changes:

  1. Version-exists gate — short-circuit the whole release when the version is already on PyPI.
  2. success() guard — restore GitHub's implicit needs-success gating that the new if: conditions would otherwise silently drop.
  3. Re-enable Sigstore attestations — turn on attestations: true for both prod and Test PyPI publishes.

This PR contains only the release-robustness portion of #411. The unrelated CI-tooling migration (shared uv_setup composite action + uv sync --dev across lint/local/main/unit/codecov workflows) is split into a separate PR.

Changes

1. Version-exists gate (release.yml)

  • The build job gains a new check-pypi step (shell: python) that queries https://pypi.org/pypi/{pkg}/{version}/json:
    • HTTP 200 → version already published → version-exists=true
    • HTTP 404 → not published → version-exists=false (proceed)
    • any other HTTP error → re-raised so the workflow fails loudly rather than guessing
  • Exposed as a new job output version-exists.
  • Every downstream job (test-pypi-publish, pre-release-checks, pre-release-unit-lowest-python, publish, mark-release) now carries if: success() && needs.build.outputs.version-exists == 'false', so re-running the workflow against an already-released version is a clean no-op instead of a failed double-publish attempt.
  • Added a comment on the publish job noting that legacy PyPI API tokens for astrapy should be revoked once trusted publishing is verified against real releases.

2. success() guard (release.yml) — why it matters

Adding an explicit if: to a job replaces GitHub's default condition (success()), which otherwise implicitly requires every entry in needs: to have succeeded. Without re-adding success(), an if: needs.build.outputs.version-exists == 'false' would evaluate to true even when an upstream needed job failed, because the bare expression only checks the version output and no longer waits on needs-success.

Concretely: publish (production PyPI) lists test-pypi-publish in its needs:. If test-pypi-publish fails, the implicit success gate is what stops production publish from running. Replacing that gate with a version-only condition would let a failed Test PyPI publish slip through and still push to production PyPI. Prepending success() && restores the AND with needs-success, so a failed test-pypi-publish (or any upstream job) blocks production publish while still honoring the version-exists short-circuit.

3. Re-enable Sigstore attestations

  • release.yml publish step: attestations: falseattestations: true.
  • _test_release.yml publish step: attestations: falseattestations: true (TODO comment removed).
  • id-token: write permission required for attestations is already present on both publish jobs (no permission changes needed). Generates signed provenance/attestations via pypa/gh-action-pypi-publish for both prod and Test PyPI uploads.

Verification

  • YAML parse: both files load cleanly via python3 -c "import yaml; yaml.safe_load(...)".
  • actionlint: run over both files. The only finding is a pre-existing SC2001 shellcheck style suggestion at release.yml:98 inside the unchanged pre-release-checks job — it is present on main and is not introduced by this PR. The new check-pypi Python step and all four added if: success() && ... conditions lint clean.
  • Expression review: confirmed id-token: write is present on both publish jobs (attestations prerequisite) and that every short-circuited downstream job now ANDs success() with the version check.

Notes / follow-ups

  • The version-exists check hits the public PyPI JSON API at workflow time; transient PyPI outages surface as a non-404 HTTPError and (intentionally) fail the build rather than silently proceeding.
  • First real release on this branch should be watched to confirm attestations upload successfully before legacy PyPI API tokens are revoked (per the added comment).

erichare added 5 commits June 10, 2026 14:27
…or to uv_setup

The explicit `if: needs.build.outputs.version-exists == 'false'` added to the
release.yml downstream jobs replaced GitHub Actions' implicit success() check
on `needs`, so a failed test-pypi-publish no longer blocked pre-release-checks,
pre-release-unit-lowest-python, publish, or mark-release. A transient TestPyPI
failure could therefore lead to a production PyPI publish that skipped the
test-publish gate.

Re-add success() to every gated job (`if: success() && needs.build.outputs.
version-exists == 'false'`). success() automatically requires all of each job's
`needs` to have succeeded, restoring the prior safety semantics while keeping
the skip-when-already-published behavior, and stays correct if dependencies are
added later.

Also migrate codecov_aggregator.yml off the legacy setup-python + pipx + make
venv pattern onto the shared ./.github/actions/uv_setup composite action +
`uv sync --dev`, matching lint/local/main/unit.
Revert the uv_setup composite-action migration of the lint, local, main, unit,
and codecov_aggregator workflows from this branch; that mechanical migration
now lives in its own PR (branch ci-uv-setup-migration).

This PR is narrowed to the release-workflow hardening only:
  - release.yml: check-pypi version-exists gate + success() guard + attestations
  - _test_release.yml: attestations
@erichare erichare changed the title feat: More robust release workflow for PyPi ci: harden release workflow with version-exists gate, success() guard, and Sigstore attestations Jun 15, 2026
@github-actions

Copy link
Copy Markdown

Coverage report

for commit: 2ffe26460564b694f563ac3bffb3f36b3bfaf70e.
download detailed report here.

                                                File   Stmts   Miss      Cover     Delta
----------------------------------------------------------------------------------------
                                     astrapy/repl.py      77     77      0.00%     0.00%
                              astrapy/admin/admin.py     763    361     52.69%     0.00%
                               astrapy/utils/meta.py      38     15     60.53%     0.00%
                                 astrapy/__init__.py      26     10     61.54%     0.00%
                                  astrapy/results.py      50     16     68.00%     0.00%
              astrapy/exceptions/table_exceptions.py      27      8     70.37%     0.00%
         astrapy/exceptions/collection_exceptions.py      40     11     72.50%     0.00%
                  astrapy/data_types/data_api_set.py      90     23     74.44%     0.00%
                         astrapy/exceptions/utils.py      53     11     79.25%     0.00%
                  astrapy/data/cursors/pagination.py      21      4     80.95%     0.00%
                           astrapy/authentication.py     138     24     82.61%     0.00%
          astrapy/data/info/collection_descriptor.py     197     33     83.25%     0.00%
                      astrapy/exceptions/__init__.py     108     18     83.33%     0.00%
                        astrapy/utils/user_agents.py      18      3     83.33%     0.00%
                                   astrapy/client.py      68     11     83.82%     0.00%
               astrapy/data_types/data_api_vector.py      44      7     84.09%     0.00%
                      astrapy/data/info/reranking.py     122     18     85.25%     0.00%
                            astrapy/utils/parsing.py       7      1     85.71%     0.00%
                  astrapy/data_types/data_api_map.py      59      8     86.44%     0.00%
 astrapy/data/info/table_descriptor/table_listing.py      49      6     87.76%     0.00%
                            astrapy/data/database.py     657     75     88.58%     0.00%
 astrapy/data/info/table_descriptor/table_indexes.py     230     26     88.70%     0.00%
            astrapy/data_types/data_api_timestamp.py      98     11     88.78%     0.00%
                      astrapy/data/info/vectorize.py     131     14     89.31%     0.00%
                  astrapy/data/info/database_info.py     104     11     89.42%     0.00%
    astrapy/settings/definitions/definitions_data.py      38      4     89.47%     0.00%
  astrapy/data/info/table_descriptor/type_listing.py      39      4     89.74%     0.00%
             astrapy/data_types/data_api_dict_udt.py      10      1     90.00%     0.00%
                              astrapy/utils/unset.py      10      1     90.00%     0.00%
                        astrapy/utils/api_options.py     206     20     90.29%     0.00%
astrapy/data/info/table_descriptor/table_altering.py     114     11     90.35%     0.00%
             astrapy/data_types/data_api_duration.py      55      5     90.91%     0.00%
                      astrapy/utils/api_commander.py     248     22     91.13%     0.00%
                               astrapy/data/table.py     738     65     91.19%     0.00%
                          astrapy/admin/endpoints.py      36      3     91.67%     0.00%
 astrapy/data/info/table_descriptor/table_columns.py     204     17     91.67%     0.00%
 astrapy/data/info/table_descriptor/type_altering.py      85      7     91.76%     0.00%
                 astrapy/utils/duration_std_utils.py      92      7     92.39%     0.00%
         astrapy/exceptions/devops_api_exceptions.py      79      6     92.41%     0.00%
                          astrapy/data/collection.py     752     57     92.42%     0.00%
                astrapy/data/cursors/query_engine.py     214     16     92.52%     0.00%
         astrapy/event_observers/context_managers.py      27      2     92.59%     0.00%
                 astrapy/data_types/data_api_time.py      98      7     92.86%     0.00%
                 astrapy/data_types/data_api_date.py      89      6     93.26%     0.00%
 astrapy/data/info/table_descriptor/type_creation.py      32      2     93.75%     0.00%
             astrapy/exceptions/error_descriptors.py      53      3     94.34%     0.00%
   astrapy/settings/definitions/definitions_types.py      19      1     94.74%     0.00%
         astrapy/data/utils/collection_converters.py      80      4     95.00%     0.00%
              astrapy/data/utils/table_converters.py     411     20     95.13%     0.00%
                 astrapy/data/cursors/find_cursor.py     631     30     95.25%     0.00%
astrapy/data/info/table_descriptor/table_creation.py      49      2     95.92%     0.00%
                astrapy/event_observers/observers.py      30      1     96.67%     0.00%
                      astrapy/data/cursors/cursor.py      92      3     96.74%     0.00%
                 astrapy/data/cursors/farr_cursor.py     350     11     96.86%     0.00%
                           astrapy/utils/str_enum.py      32      1     96.88%     0.00%
                   astrapy/utils/duration_c_utils.py      66      2     96.97%     0.00%
                         astrapy/utils/date_utils.py      80      2     97.50%     0.00%
   astrapy/settings/definitions/definitions_admin.py      41      1     97.56%     0.00%
           astrapy/exceptions/data_api_exceptions.py      84      1     98.81%     0.00%
           astrapy/data/utils/distinct_extractors.py     104      1     99.04%     0.00%
                           astrapy/admin/__init__.py       3      0    100.00%     0.00%
                              astrapy/api_options.py       3      0    100.00%     0.00%
                               astrapy/collection.py       2      0    100.00%     0.00%
                                astrapy/constants.py       5      0    100.00%     0.00%
                                  astrapy/cursors.py       7      0    100.00%     0.00%
                            astrapy/data/__init__.py       0      0    100.00%     0.00%
                    astrapy/data/cursors/__init__.py       1      0    100.00%     0.00%
             astrapy/data/cursors/reranked_result.py       8      0    100.00%     0.00%
                      astrapy/data/utils/__init__.py       0      0    100.00%     0.00%
      astrapy/data/utils/extended_json_converters.py      28      0    100.00%     0.00%
                   astrapy/data/utils/table_types.py      36      0    100.00%     0.00%
               astrapy/data/utils/vector_coercion.py      13      0    100.00%     0.00%
                      astrapy/data_types/__init__.py      10      0    100.00%     0.00%
                                 astrapy/database.py       2      0    100.00%     0.00%
                 astrapy/event_observers/__init__.py       5      0    100.00%     0.00%
                   astrapy/event_observers/events.py      50      0    100.00%     0.00%
                                      astrapy/ids.py       5      0    100.00%     0.00%
                                     astrapy/info.py      15      0    100.00%     0.00%
                        astrapy/settings/__init__.py       0      0    100.00%     0.00%
                        astrapy/settings/defaults.py      44      0    100.00%     0.00%
            astrapy/settings/definitions/__init__.py       0      0    100.00%     0.00%
                  astrapy/settings/error_messages.py       2      0    100.00%     0.00%
                                    astrapy/table.py       2      0    100.00%     0.00%
                           astrapy/utils/__init__.py       0      0    100.00%     0.00%
                     astrapy/utils/document_paths.py      46      0    100.00%     0.00%
                     astrapy/utils/python_version.py       5      0    100.00%     0.00%
                      astrapy/utils/request_tools.py      32      0    100.00%     0.00%
----------------------------------------------------------------------------------------
                                              totals    8827   1148     86.99%     0.00%

@erichare erichare linked an issue Jun 17, 2026 that may be closed by this pull request
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.

Improvements in the release workflow (and workflows in general)

1 participant