Skip to content
55 changes: 55 additions & 0 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,55 @@ jobs:
# ref: v${{ needs.version_check.outputs.PCAPKIT_VERSION }}
fetch-depth: 0

# ``CHANGELOG.md`` is generated by ``util/changelog_md.py`` from the newest
# entry in the ``docs/source/changelog.rst`` toctree, so it describes the
# version being *prepared* -- which is not always the version being
# *released*. The two agree on a normal release and disagree on every
# automated one, because ``util/bump_version.py`` bumps without ever adding
# an entry: ``1.5.0b3`` becomes ``1.5.0b4``, and a final release becomes
# ``.post1``. A ``.postN`` is the weekly vendor registry refresh, carries no
# library change, and is deliberately recorded as an "Also released" line
# inside its base version's entry rather than as an entry of its own.
#
# Handing the file over unconditionally would therefore republish the base
# version's notes as the body of a release that changed no library code. So
# the body is used only when the generated file's own heading names the
# version being released, and otherwise the release keeps the generated notes
# alone -- which for a vendor refresh is the honest summary, since the commit
# list is the only thing that did change. Both outcomes are announced in the
# log rather than left to be inferred from the published result.
#
# Only the version token is compared, not the rest of the heading, so an
# entry still reading "-- unreleased" is matched and dated-or-not does not
# change the decision. That case gets its own warning below instead.
- name: Select release body
id: release_body
shell: bash
env:
PCAPKIT_VERSION: ${{ needs.version_check.outputs.PCAPKIT_VERSION }}
run: |
set -euo pipefail

if [ ! -f CHANGELOG.md ]; then
echo "::warning title=Release body generated::CHANGELOG.md is absent, so this release carries the automatically generated notes alone. The file is generated by util/changelog_md.py and the 'Changelog drift' job gates it on the release path, so reaching this branch means that gate did not run."
exit 0
fi

# The generated file opens with ``## <version> -- <date-or-unreleased>``.
heading="$(sed -n '1s/^## \([^[:space:]]*\).*/\1/p' CHANGELOG.md)"

if [ "$heading" != "$PCAPKIT_VERSION" ]; then
echo "::notice title=Release body generated::CHANGELOG.md holds the entry for '${heading:-<no parseable heading>}', not ${PCAPKIT_VERSION}, so this release carries the automatically generated notes alone. Expected for a pre-release and for a .postN vendor refresh, neither of which has a changelog entry of its own."
exit 0
fi

echo 'path=CHANGELOG.md' >> "$GITHUB_OUTPUT"
echo "::notice title=Release body from CHANGELOG.md::The changelog entry for ${PCAPKIT_VERSION} leads the release body; the generated pull-request list follows it."

if sed -n '1p' CHANGELOG.md | grep -qi 'unreleased'; then
echo "::warning title=Changelog entry is undated::The entry for ${PCAPKIT_VERSION} still reads 'unreleased', so the release body says so too. Date the heading in docs/source/changelog/${PCAPKIT_VERSION}.rst, regenerate CHANGELOG.md and the body can be corrected in place."
fi

- name: Create Release
id: create_release
uses: softprops/action-gh-release@v3.0.2
Expand All @@ -106,6 +155,12 @@ jobs:
# allowUpdates: true
# artifacts: |
# dist/*
# An empty ``body_path`` is skipped by the action's own truthy guard, so
# an unset output here means "no body" rather than "read the file ''".
# ``generate_release_notes`` stays on either way: the action fetches the
# generated notes itself and joins them as ``<body>\n\n<generated>``, so
# the hand-written entry leads and the pull-request list follows.
body_path: ${{ steps.release_body.outputs.path }}
generate_release_notes: true
# makeLatest: true
name: PyPCAPKit v${{ needs.version_check.outputs.PCAPKIT_VERSION }}
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,47 @@ jobs:
- name: Run full test suite
run: python -m pytest -q

# ``CHANGELOG.md`` is generated from the newest entry under
# ``docs/source/changelog/`` by ``util/changelog_md.py``, so it falls out of step
# the moment an entry is edited without regenerating it. That is worth its own
# job rather than leaving it to review, because ``Create Release`` feeds the file
# to the GitHub Release body: a drifted copy is not merely wrong in the tree, it
# is published.
#
# Deliberately *not* gated on ``gate-only``, unlike the three jobs above, and
# that is the point of putting it here at all. ``create-release.yml`` calls this
# workflow as its release gate, so an ungated job runs on the release path and
# the release body cannot be built from a file that has drifted. The matrix is
# skipped per caller because it is expensive and already ran for the commit;
# this is neither expensive nor covered elsewhere on that path.
#
# ``tests/project/test_changelog_md.py`` asserts the same identity, so the
# matrix covers it too -- but that test ``skipTest``s when either file is
# absent, and reports a mismatch as two 17 KB strings. ``--check`` cannot skip,
# exits 1 when the file is missing, and prints a unified diff.
changelog:
name: Changelog drift
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: "3.14"

# No install step: the generator imports argparse, difflib, pathlib, re, sys
# and typing and nothing else, so it runs against a bare interpreter.
# Verified with ``python -S``.
- name: Check CHANGELOG.md is in step with its source entry
shell: bash
run: |
if ! python util/changelog_md.py --check; then
echo "::error title=CHANGELOG.md has drifted::CHANGELOG.md is generated from the newest entry under docs/source/changelog/ and no longer matches it. Regenerate it with 'python util/changelog_md.py' and commit the result. The unified diff above shows what moved."
exit 1
fi

# The verdict a shipping workflow asks for: the full suite, fixtures and
# all, on one interpreter. It is deliberately not the matrix -- the matrix
# has already run against this very commit, from this workflow's own push or
Expand Down
Loading