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
48 changes: 47 additions & 1 deletion .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,28 @@ concurrency:
# gate and every publisher, and ``release_status`` reports, green, whether that
# release is complete. A ``v*`` push still meets the #1634 check. The
# ``vendor_update`` job is the issue's other half.
#
# Owner ruling, 2026-10-30: ``__version__`` carries a PEP 440 dev-release
# suffix (``X.Y.Z.devN``) while a version is still under active development, a
# work-in-progress signal with no gate of its own until now -- and the
# ``workflow_run`` trigger above means a scheduled Vendor Update can reach this
# workflow and carry that dev version all the way to ``github``'s one reviewer
# approval (#887, below). That approval is not nothing, but it is one person
# approving a diff for reasons that are usually unrelated to the version
# string, so a stray ``.devN`` is easy to miss there; and a re-run of a job
# that already skipped ``github`` once does not ask for a fresh approval at
# all. ``PCAPKIT_DEV_RELEASE`` closes that gap in code rather than leaving it
# to review. ``PCAPKIT_DEV_RELEASE`` (``version_check``'s ``is_devrelease``)
# is read by ``github``, ``tag``, ``pypi`` and ``conda`` alike, deliberately not
# ``PCAPKIT_PRERELEASE`` (``is_prerelease``): the latter is also ``true`` for a
# deliberately-tagged alpha/beta/rc this project has shipped for real (e.g.
# ``1.5.0b14``), and gating on it would silently block one.
# tests/project/test_release_gates.py's ``test_all_four_publishers_gate_on_dev_release``
# checks this text, and ``gate_rows``/``GITHUB_ROWS`` evaluate it. ``release_status``
# also reads it: with the gate skipping all four publishers, their evidence
# outputs stay ``false``, which would otherwise read as #888's stranded partial
# release. util/release_status.py's ``dev_release_skipped()`` reports that
# combination as a quiet, deliberate no-op instead.

jobs:
# #1748: a Vendor Update raised by a Unit Tests verdict for another commit
Expand Down Expand Up @@ -226,6 +248,17 @@ jobs:
run: |
python -m pip install -U pip setuptools wheel packaging

# Owner ruling, 2026-10-30: during active development on a not-yet-released
# version, ``__version__`` carries a PEP 440 dev-release suffix
# (``X.Y.Z.devN``) as a work-in-progress signal. ``PCAPKIT_DEV_RELEASE`` is
# ``is_devrelease``, not ``is_prerelease`` -- the latter is also ``true``
# for a deliberately-tagged alpha/beta/rc (e.g. ``1.5.0b14``, a real
# release), and gating on it would silently block one. ``github``, ``tag``,
# ``pypi`` and ``conda`` all read this below so that a scheduled Vendor
# Update bumping a dev version never tags or publishes it on the strength
# of ``github-release``'s one reviewer alone, who can miss a stray
# ``.devN`` in an otherwise-unrelated approval; see
# docs/source/contributing/releasing.rst.
- name: Get Version
id: get_version
run: |
Expand All @@ -236,6 +269,7 @@ jobs:
{
echo "PCAPKIT_VERSION=$(python -c 'import pcapkit; print(pcapkit.__version__)')"
echo "PCAPKIT_PRERELEASE=$(python -c 'from packaging.version import Version; import pcapkit; print(str(Version(pcapkit.__version__).is_prerelease).lower())')"
echo "PCAPKIT_DEV_RELEASE=$(python -c 'from packaging.version import Version; import pcapkit; print(str(Version(pcapkit.__version__).is_devrelease).lower())')"
echo "PCAPKIT_CONDA_LABEL=$(python -c 'from packaging.version import Version; import pcapkit; print("dev" if Version(pcapkit.__version__).is_prerelease else "main")')"
} >> "$GITHUB_OUTPUT"

Expand Down Expand Up @@ -310,6 +344,7 @@ jobs:
outputs:
PCAPKIT_VERSION: ${{ steps.get_version.outputs.PCAPKIT_VERSION }}
PCAPKIT_PRERELEASE: ${{ steps.get_version.outputs.PCAPKIT_PRERELEASE }}
PCAPKIT_DEV_RELEASE: ${{ steps.get_version.outputs.PCAPKIT_DEV_RELEASE }}
PCAPKIT_TAG_EXISTS: ${{ steps.check_tag.outputs.exists }}
PCAPKIT_CONDA_TAG_EXISTS: ${{ steps.check_conda_tag.outputs.exists }}
PCAPKIT_PYPI_COMPLETE: ${{ steps.check_pypi.outputs.PCAPKIT_PYPI_COMPLETE }}
Expand All @@ -324,12 +359,18 @@ jobs:

# No ``environment:``: no outward effect, and a job calling a reusable
# workflow may not declare one. Runs exactly when a publisher could (#1052,
# #1748); see the note above ``jobs:``.
# #1748, and, since the dev-release gate below, owner ruling 2026-10-30);
# see the note above ``jobs:``. ``PCAPKIT_DEV_RELEASE`` carries none of the
# other clauses' ``startsWith(github.ref_name, 'v')`` exemption, matching
# ``github``, ``tag``, ``pypi`` and ``conda`` below: a dev version can never
# be published regardless of how this run started, so spending the ~20
# minutes here on one is the same waste #1052 added this job to avoid.
unit-tests:
name: Release test gate
needs: [ version_check ]
if: >-
${{ needs.version_check.result == 'success' &&
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
(startsWith(github.ref_name, 'v') ||
needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
(startsWith(github.ref_name, 'v') ||
Expand Down Expand Up @@ -360,6 +401,7 @@ jobs:
needs: [ unit-tests, version_check ]
if: >-
${{ (startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_TAG_EXISTS == 'false') }}
steps:
- name: Checkout code
Expand Down Expand Up @@ -494,6 +536,7 @@ jobs:
if: >-
${{ !cancelled() && needs.version_check.result == 'success' &&
needs.unit-tests.result == 'success' &&
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
(needs.github.result == 'success' || needs.github.result == 'skipped') &&
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_CONDA_TAG_EXISTS == 'false') }}
Expand Down Expand Up @@ -580,6 +623,7 @@ jobs:
if: >-
${{ !cancelled() && needs.version_check.result == 'success' &&
needs.unit-tests.result == 'success' &&
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
(needs.github.result == 'success' || needs.github.result == 'skipped') &&
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_PYPI_COMPLETE == 'false') }}
Expand Down Expand Up @@ -738,6 +782,7 @@ jobs:
if: >-
${{ !cancelled() && needs.version_check.result == 'success' &&
needs.unit-tests.result == 'success' &&
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
(needs.github.result == 'success' || needs.github.result == 'skipped') &&
(needs.tag.result == 'success' || needs.tag.result == 'skipped') &&
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
Expand Down Expand Up @@ -994,6 +1039,7 @@ jobs:
PCAPKIT_CONDA_COMPLETE: ${{ needs.version_check.outputs.PCAPKIT_CONDA_COMPLETE }}
PCAPKIT_RELEASED_AT: ${{ needs.version_check.outputs.PCAPKIT_RELEASED_AT }}
PCAPKIT_RELEASED_ELSEWHERE: ${{ needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE }}
PCAPKIT_DEV_RELEASE: ${{ needs.version_check.outputs.PCAPKIT_DEV_RELEASE }}
PCAPKIT_PYPI_KTLO_310: ${{ needs.pypi.outputs.ktlo_310 }}
PCAPKIT_PYPI_KTLO_PYPY310: ${{ needs.pypi.outputs.ktlo_pypy310 }}
run: python3 util/release_status.py
29 changes: 28 additions & 1 deletion docs/source/contributing/releasing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ starts the automated path described below.
The same script is also the manual entry point: run
``python util/bump_version.py`` and commit the result to ``main`` for a
release that is not a vendor-registry refresh -- a feature or a fix that has
landed and is ready to go out.
landed and is ready to go out. Running it while ``main``'s version is still a
``.devN`` one only advances that counter (``1.6.0.dev2`` to ``1.6.0.dev3``) --
see the dev-release note below -- so it produces another blocked dev version,
not a release.

Editing ``__version__`` by hand without also moving :file:`CITATION.cff`'s
``version`` field to match is **not** a safe shortcut: it blocks the release.
Expand All @@ -72,6 +75,30 @@ warns, without failing, if that entry's heading still reads "unreleased". Each
enforces what the changelog convention
already asks for.

Owner ruling, 2026-10-30: while a version is still under active development,
``__version__`` should carry that :pep:`440` ``devN`` suffix itself, as a plain
work-in-progress signal distinct from a deliberately-tagged alpha, beta or
release candidate -- this project has shipped those for real (e.g.
``1.5.0b14``), and they must stay publishable. ``version_check``'s
``PCAPKIT_DEV_RELEASE`` output reads ``Version(__version__).is_devrelease``,
never ``is_prerelease`` (which a real beta sets too), and ``github``, ``tag``,
``pypi`` and ``conda`` all refuse to run while it is ``true`` -- so a scheduled
Vendor Update reaching this workflow (see below) can never tag or publish a dev
version on the strength of ``github-release``'s one reviewer approval alone
(:issue:`887`, below). That approval still exists and still matters, but it is one
person approving a diff for reasons usually unrelated to the version string, so
a stray ``.devN`` is easy to miss there; and a re-run of a job that already
skipped ``github`` once does not ask for a fresh approval at all. This gate
closes that in code rather than leaving it to review.

The suffix comes off only by hand: :file:`util/bump_version.py` has no option
to drop it -- run on a dev version it only advances the ``devN`` counter
(``1.6.0.dev2`` to ``1.6.0.dev3``), never past it. Finishing a dev version
means editing the ``__version__`` assignment in :file:`pcapkit/__init__.py`
to the real final string by hand, and moving :file:`CITATION.cff`'s matching
``version`` field in the same commit, once that version is actually ready to
ship.

Ways to Start a Release
-----------------------

Expand Down
2 changes: 1 addition & 1 deletion tests/project/test_python_matrix_tiers_1633.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ def test_release_status_reports_a_failed_ktlo_leg_without_warning(self) -> None:
'PCAPKIT_PYPI_JOB': 'success', 'PCAPKIT_CONDA_JOB': 'success',
'PCAPKIT_VERSION': '1.5.0', 'PCAPKIT_TAG_EXISTS': 'false',
'PCAPKIT_CONDA_TAG_EXISTS': 'false', 'PCAPKIT_PYPI_COMPLETE': 'false',
'PCAPKIT_CONDA_COMPLETE': 'false'}
'PCAPKIT_CONDA_COMPLETE': 'false', 'PCAPKIT_DEV_RELEASE': 'false'}
clean, failed = (subprocess.run(command, env=dict(os.environ, **env), capture_output=True, text=True,
check=False, timeout=60)
for env in (base, dict(base, PCAPKIT_PYPI_KTLO_310='failure',
Expand Down
Loading
Loading