Skip to content

Commit 2d23e48

Browse files
authored
Merge pull request #2452 from JarryShaw/feat/gate-release-on-dev-version
ci: gate create-release.yml's publishers on a dev-release version
2 parents 058ef5b + 0c56095 commit 2d23e48

6 files changed

Lines changed: 371 additions & 10 deletions

File tree

‎.github/workflows/create-release.yml‎

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

127149
jobs:
128150
# #1748: a Vendor Update raised by a Unit Tests verdict for another commit
@@ -226,6 +248,17 @@ jobs:
226248
run: |
227249
python -m pip install -U pip setuptools wheel packaging
228250
251+
# Owner ruling, 2026-10-30: during active development on a not-yet-released
252+
# version, ``__version__`` carries a PEP 440 dev-release suffix
253+
# (``X.Y.Z.devN``) as a work-in-progress signal. ``PCAPKIT_DEV_RELEASE`` is
254+
# ``is_devrelease``, not ``is_prerelease`` -- the latter is also ``true``
255+
# for a deliberately-tagged alpha/beta/rc (e.g. ``1.5.0b14``, a real
256+
# release), and gating on it would silently block one. ``github``, ``tag``,
257+
# ``pypi`` and ``conda`` all read this below so that a scheduled Vendor
258+
# Update bumping a dev version never tags or publishes it on the strength
259+
# of ``github-release``'s one reviewer alone, who can miss a stray
260+
# ``.devN`` in an otherwise-unrelated approval; see
261+
# docs/source/contributing/releasing.rst.
229262
- name: Get Version
230263
id: get_version
231264
run: |
@@ -236,6 +269,7 @@ jobs:
236269
{
237270
echo "PCAPKIT_VERSION=$(python -c 'import pcapkit; print(pcapkit.__version__)')"
238271
echo "PCAPKIT_PRERELEASE=$(python -c 'from packaging.version import Version; import pcapkit; print(str(Version(pcapkit.__version__).is_prerelease).lower())')"
272+
echo "PCAPKIT_DEV_RELEASE=$(python -c 'from packaging.version import Version; import pcapkit; print(str(Version(pcapkit.__version__).is_devrelease).lower())')"
239273
echo "PCAPKIT_CONDA_LABEL=$(python -c 'from packaging.version import Version; import pcapkit; print("dev" if Version(pcapkit.__version__).is_prerelease else "main")')"
240274
} >> "$GITHUB_OUTPUT"
241275
@@ -310,6 +344,7 @@ jobs:
310344
outputs:
311345
PCAPKIT_VERSION: ${{ steps.get_version.outputs.PCAPKIT_VERSION }}
312346
PCAPKIT_PRERELEASE: ${{ steps.get_version.outputs.PCAPKIT_PRERELEASE }}
347+
PCAPKIT_DEV_RELEASE: ${{ steps.get_version.outputs.PCAPKIT_DEV_RELEASE }}
313348
PCAPKIT_TAG_EXISTS: ${{ steps.check_tag.outputs.exists }}
314349
PCAPKIT_CONDA_TAG_EXISTS: ${{ steps.check_conda_tag.outputs.exists }}
315350
PCAPKIT_PYPI_COMPLETE: ${{ steps.check_pypi.outputs.PCAPKIT_PYPI_COMPLETE }}
@@ -324,12 +359,18 @@ jobs:
324359

325360
# No ``environment:``: no outward effect, and a job calling a reusable
326361
# workflow may not declare one. Runs exactly when a publisher could (#1052,
327-
# #1748); see the note above ``jobs:``.
362+
# #1748, and, since the dev-release gate below, owner ruling 2026-10-30);
363+
# see the note above ``jobs:``. ``PCAPKIT_DEV_RELEASE`` carries none of the
364+
# other clauses' ``startsWith(github.ref_name, 'v')`` exemption, matching
365+
# ``github``, ``tag``, ``pypi`` and ``conda`` below: a dev version can never
366+
# be published regardless of how this run started, so spending the ~20
367+
# minutes here on one is the same waste #1052 added this job to avoid.
328368
unit-tests:
329369
name: Release test gate
330370
needs: [ version_check ]
331371
if: >-
332372
${{ needs.version_check.result == 'success' &&
373+
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
333374
(startsWith(github.ref_name, 'v') ||
334375
needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
335376
(startsWith(github.ref_name, 'v') ||
@@ -360,6 +401,7 @@ jobs:
360401
needs: [ unit-tests, version_check ]
361402
if: >-
362403
${{ (startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
404+
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
363405
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_TAG_EXISTS == 'false') }}
364406
steps:
365407
- name: Checkout code
@@ -494,6 +536,7 @@ jobs:
494536
if: >-
495537
${{ !cancelled() && needs.version_check.result == 'success' &&
496538
needs.unit-tests.result == 'success' &&
539+
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
497540
(needs.github.result == 'success' || needs.github.result == 'skipped') &&
498541
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
499542
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_CONDA_TAG_EXISTS == 'false') }}
@@ -580,6 +623,7 @@ jobs:
580623
if: >-
581624
${{ !cancelled() && needs.version_check.result == 'success' &&
582625
needs.unit-tests.result == 'success' &&
626+
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
583627
(needs.github.result == 'success' || needs.github.result == 'skipped') &&
584628
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
585629
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_PYPI_COMPLETE == 'false') }}
@@ -738,6 +782,7 @@ jobs:
738782
if: >-
739783
${{ !cancelled() && needs.version_check.result == 'success' &&
740784
needs.unit-tests.result == 'success' &&
785+
needs.version_check.outputs.PCAPKIT_DEV_RELEASE != 'true' &&
741786
(needs.github.result == 'success' || needs.github.result == 'skipped') &&
742787
(needs.tag.result == 'success' || needs.tag.result == 'skipped') &&
743788
(startsWith(github.ref_name, 'v') || needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE != 'true') &&
@@ -994,6 +1039,7 @@ jobs:
9941039
PCAPKIT_CONDA_COMPLETE: ${{ needs.version_check.outputs.PCAPKIT_CONDA_COMPLETE }}
9951040
PCAPKIT_RELEASED_AT: ${{ needs.version_check.outputs.PCAPKIT_RELEASED_AT }}
9961041
PCAPKIT_RELEASED_ELSEWHERE: ${{ needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE }}
1042+
PCAPKIT_DEV_RELEASE: ${{ needs.version_check.outputs.PCAPKIT_DEV_RELEASE }}
9971043
PCAPKIT_PYPI_KTLO_310: ${{ needs.pypi.outputs.ktlo_310 }}
9981044
PCAPKIT_PYPI_KTLO_PYPY310: ${{ needs.pypi.outputs.ktlo_pypy310 }}
9991045
run: python3 util/release_status.py

‎docs/source/contributing/releasing.rst‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ starts the automated path described below.
4646
The same script is also the manual entry point: run
4747
``python util/bump_version.py`` and commit the result to ``main`` for a
4848
release that is not a vendor-registry refresh -- a feature or a fix that has
49-
landed and is ready to go out.
49+
landed and is ready to go out. Running it while ``main``'s version is still a
50+
``.devN`` one only advances that counter (``1.6.0.dev2`` to ``1.6.0.dev3``) --
51+
see the dev-release note below -- so it produces another blocked dev version,
52+
not a release.
5053

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

78+
Owner ruling, 2026-10-30: while a version is still under active development,
79+
``__version__`` should carry that :pep:`440` ``devN`` suffix itself, as a plain
80+
work-in-progress signal distinct from a deliberately-tagged alpha, beta or
81+
release candidate -- this project has shipped those for real (e.g.
82+
``1.5.0b14``), and they must stay publishable. ``version_check``'s
83+
``PCAPKIT_DEV_RELEASE`` output reads ``Version(__version__).is_devrelease``,
84+
never ``is_prerelease`` (which a real beta sets too), and ``github``, ``tag``,
85+
``pypi`` and ``conda`` all refuse to run while it is ``true`` -- so a scheduled
86+
Vendor Update reaching this workflow (see below) can never tag or publish a dev
87+
version on the strength of ``github-release``'s one reviewer approval alone
88+
(:issue:`887`, below). That approval still exists and still matters, but it is one
89+
person approving a diff for reasons usually unrelated to the version string, so
90+
a stray ``.devN`` is easy to miss there; and a re-run of a job that already
91+
skipped ``github`` once does not ask for a fresh approval at all. This gate
92+
closes that in code rather than leaving it to review.
93+
94+
The suffix comes off only by hand: :file:`util/bump_version.py` has no option
95+
to drop it -- run on a dev version it only advances the ``devN`` counter
96+
(``1.6.0.dev2`` to ``1.6.0.dev3``), never past it. Finishing a dev version
97+
means editing the ``__version__`` assignment in :file:`pcapkit/__init__.py`
98+
to the real final string by hand, and moving :file:`CITATION.cff`'s matching
99+
``version`` field in the same commit, once that version is actually ready to
100+
ship.
101+
75102
Ways to Start a Release
76103
-----------------------
77104

‎tests/project/test_python_matrix_tiers_1633.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def test_release_status_reports_a_failed_ktlo_leg_without_warning(self) -> None:
441441
'PCAPKIT_PYPI_JOB': 'success', 'PCAPKIT_CONDA_JOB': 'success',
442442
'PCAPKIT_VERSION': '1.5.0', 'PCAPKIT_TAG_EXISTS': 'false',
443443
'PCAPKIT_CONDA_TAG_EXISTS': 'false', 'PCAPKIT_PYPI_COMPLETE': 'false',
444-
'PCAPKIT_CONDA_COMPLETE': 'false'}
444+
'PCAPKIT_CONDA_COMPLETE': 'false', 'PCAPKIT_DEV_RELEASE': 'false'}
445445
clean, failed = (subprocess.run(command, env=dict(os.environ, **env), capture_output=True, text=True,
446446
check=False, timeout=60)
447447
for env in (base, dict(base, PCAPKIT_PYPI_KTLO_310='failure',

0 commit comments

Comments
 (0)