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
184 changes: 27 additions & 157 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,41 +136,34 @@ jobs:
# in that run's place, so the displaced run's release waits for the next run
# on ``main``: the tip's own Vendor Update if the tip's Unit Tests run has not
# completed yet, else the next push's or Saturday's.
#
# #1967: util/release_vendor_update.py holds the step's shell, so the job
# checks out util/ alone, for it and util/_actions.py, which is all
# ``contents: read`` is for.
vendor_update:
name: Triggering Vendor Update
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' }}
timeout-minutes: 5
permissions:
actions: read
contents: read
outputs:
updated: ${{ steps.updated.outputs.updated }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
sparse-checkout: util
persist-credentials: false

- name: Read whether its vendor-update job ran
id: updated
env:
GH_TOKEN: ${{ github.token }}
PCAPKIT_VENDOR_RUN_ID: ${{ github.event.workflow_run.id }}
run: |
set -uo pipefail

if ! result="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${PCAPKIT_VENDOR_RUN_ID}/jobs" \
--paginate --jq '.jobs[] | select(.name == "vendor-update") | .conclusion')"; then
echo "::warning title=Vendor Update not read (#1748)::could not list the jobs of Vendor Update run ${PCAPKIT_VENDOR_RUN_ID}, so this run treats it as one that updated."
exit 0
fi
case "$result" in
success)
echo "updated=true" >> "$GITHUB_OUTPUT"
;;
skipped)
echo "updated=false" >> "$GITHUB_OUTPUT"
echo "Vendor Update run ${PCAPKIT_VENDOR_RUN_ID} skipped vendor-update: its verdict did not cover the commit it checked out."
;;
*)
echo "::warning title=Vendor Update not read (#1748)::Vendor Update run ${PCAPKIT_VENDOR_RUN_ID} reports vendor-update as '${result//$'\n'/ }', so this run treats it as one that updated."
;;
esac
run: python3 util/release_vendor_update.py

version_check:
name: Check Version
Expand Down Expand Up @@ -1199,15 +1192,25 @@ jobs:
# chosen over ``always()`` so that cancelling the run cancels this report too
# rather than leaving it to describe a run it cannot fully see.
#
# No ``environment:`` and ``permissions: {}``: it only annotates, from the
# ``needs:`` context and the trigger event.
# No ``environment:``: it only annotates, from the ``needs:`` context and the
# trigger event. #1967: util/release_status.py holds the step's shell, so the
# job checks out util/ alone, for it and util/_actions.py, which is all
# ``contents: read`` is for.
release_status:
name: Release status
runs-on: ubuntu-latest
needs: [ vendor_update, unit-tests, version_check, github, tag, pypi, conda ]
if: ${{ !cancelled() }}
permissions: {}
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
sparse-checkout: util
persist-credentials: false

- name: Report why this run did or did not release anything
env:
PCAPKIT_EVENT_NAME: ${{ github.event_name }}
Expand All @@ -1231,137 +1234,4 @@ jobs:
PCAPKIT_RELEASED_ELSEWHERE: ${{ needs.version_check.outputs.PCAPKIT_RELEASED_ELSEWHERE }}
PCAPKIT_PYPI_KTLO_310: ${{ needs.pypi.outputs.ktlo_310 }}
PCAPKIT_PYPI_KTLO_PYPY310: ${{ needs.pypi.outputs.ktlo_pypy310 }}
run: |
set -euo pipefail

# #1633: pypi's KTLO legs reach neither needs.pypi.result
# (continue-on-error) nor PCAPKIT_PYPI_COMPLETE's count, so each is
# only reported here, as a notice, from its own job output.
ktlo_310="${PCAPKIT_PYPI_KTLO_310:-}"
ktlo_pypy310="${PCAPKIT_PYPI_KTLO_PYPY310:-}"
if [ -n "${ktlo_310}${ktlo_pypy310}" ]; then
echo "::notice title=KTLO legs (non-blocking)::pypi 3.10=${ktlo_310:-not run} pypy3.10=${ktlo_pypy310:-not run}. End-of-life Python, built and published but never blocking (#1633); neither result changes this run's verdict."
fi

# Case 1: version_check never ran. On `workflow_run`, Vendor Update did
# not conclude `success`: `skipped`, nothing to update, gets a quiet
# notice; anything else a warning, as Vendor Update needs attention --
# but no tag exists to strand, so it is not #888. On a push it is quiet
# only for the release App's own tag (#1634); an unset bot id never
# matches. #1748: a `success` that never ran vendor-update is quiet too.
if [ "$PCAPKIT_VERSION_CHECK" = "skipped" ]; then
if [ "$PCAPKIT_EVENT_NAME" = "push" ] && [ -n "$PCAPKIT_RELEASE_APP_BOT_ID" ] \
&& [ "$PCAPKIT_ACTOR_ID" = "$PCAPKIT_RELEASE_APP_BOT_ID" ]; then
echo "::notice title=Nothing to release::the release App's bot pushed this tag from the Create Release run that released its commit, so this run correctly released nothing (#1634)."
elif [ "$PCAPKIT_EVENT_NAME" = "workflow_run" ] && [ "$PCAPKIT_WORKFLOW_RUN_CONCLUSION" = "skipped" ]; then
echo "::notice title=Nothing to release::the triggering Vendor Update run found nothing to update, so this run correctly released nothing."
elif [ "$PCAPKIT_EVENT_NAME" = "workflow_run" ] && [ "$PCAPKIT_WORKFLOW_RUN_CONCLUSION" = "success" ] \
&& [ "${PCAPKIT_VENDOR_UPDATED:-}" = "false" ]; then
echo "::notice title=Nothing to release::the triggering Vendor Update run skipped vendor-update, since its Unit Tests verdict did not cover the commit it checked out, so it updated nothing and this run correctly released nothing (#1748)."
elif [ "$PCAPKIT_EVENT_NAME" = "workflow_run" ]; then
echo "::warning title=Release skipped::the triggering Vendor Update run concluded '${PCAPKIT_WORKFLOW_RUN_CONCLUSION}', not 'success', so version_check, the release test gate and every release job were skipped. Not #888 -- no tag was created that could be left stranded -- but Vendor Update likely needs attention."
else
echo "::warning title=Release skipped for an unexpected reason::version_check reported 'skipped' with event_name='${PCAPKIT_EVENT_NAME}', which is not the workflow_run non-success case this job otherwise expects. Check the version_check job's own if: condition."
fi
exit 0
fi

# version_check ran and did not succeed: a workflow failure, not #888.
if [ "$PCAPKIT_VERSION_CHECK" != "success" ]; then
echo "::warning title=Release blocked before it could start::version_check did not succeed (result: '${PCAPKIT_VERSION_CHECK}'), so neither the release test gate nor any release job ran. This is a workflow failure, not #888."
exit 0
fi

# The gate did not pass, so every release job skipped: a test failure,
# not #888, and already red in this run's checks.
if [ "$PCAPKIT_UNIT_TESTS" = "failure" ] || [ "$PCAPKIT_UNIT_TESTS" = "cancelled" ]; then
echo "::warning title=Release blocked by the test gate::the release test gate concluded '${PCAPKIT_UNIT_TESTS}', so no release job ran for ${PCAPKIT_VERSION}. This is a test failure, not #888."
exit 0
fi

# #1748: tagged at another commit, with the gate and every publisher
# skipped for it, which happens only off a v* ref. That release is that
# commit's (#1634): a notice if complete, else a warning naming what it
# lacks, both green. A publisher that ran on a v* push meets #1634's
# check and is reported below.
if [ "${PCAPKIT_RELEASED_ELSEWHERE:-}" = "true" ] && [ "$PCAPKIT_GITHUB_JOB" = "skipped" ] \
&& [ "$PCAPKIT_TAG_JOB" = "skipped" ] && [ "$PCAPKIT_PYPI_JOB" = "skipped" ] \
&& [ "$PCAPKIT_CONDA_JOB" = "skipped" ]; then
missing=""
[ "$PCAPKIT_CONDA_TAG_EXISTS" = "true" ] || missing="${missing:+${missing}, }the conda-${PCAPKIT_VERSION}+0 tag"
[ "$PCAPKIT_PYPI_COMPLETE" = "true" ] || missing="${missing:+${missing}, }some of its PyPI files"
[ "$PCAPKIT_CONDA_COMPLETE" = "true" ] || missing="${missing:+${missing}, }some of its Anaconda files"
if [ -z "$missing" ]; then
echo "::notice title=Nothing to release::v${PCAPKIT_VERSION} names ${PCAPKIT_RELEASED_AT}, not this run's commit ${PCAPKIT_RUN_SHA}, and that release is complete, so this run correctly released nothing (#1748)."
else
echo "::warning title=v${PCAPKIT_VERSION} is unfinished (#1634)::v${PCAPKIT_VERSION} names ${PCAPKIT_RELEASED_AT}, not this run's commit ${PCAPKIT_RUN_SHA}, and still lacks ${missing}. Only that commit's run may finish it (#1634), so this run released nothing: re-run the Create Release run created for ${PCAPKIT_RELEASED_AT}, or release the next version."
fi
exit 0
fi

# From here the evidence outputs are meaningful. A skipped gate (#1052)
# falls through on purpose: it skips only when every evidence output
# reads 'true' off a v* ref, reported as "Nothing to release" below; if
# its predicate drifts, every publisher skips behind it and this
# reaches the loud #888 branch instead.
#
# Reconcile each job against its OWN evidence, not against each other:
# a self-heal mixes jobs that skipped because their artefact existed
# with jobs that ran, and demanding uniformity would read that mix as
# #888 on the run that finished the release. Reconciled = succeeded, or
# skipped with its own evidence saying done.
reconciled_github=false
[ "$PCAPKIT_GITHUB_JOB" = "success" ] && reconciled_github=true
[ "$PCAPKIT_GITHUB_JOB" = "skipped" ] && [ "$PCAPKIT_TAG_EXISTS" = "true" ] && reconciled_github=true

reconciled_tag=false
[ "$PCAPKIT_TAG_JOB" = "success" ] && reconciled_tag=true
[ "$PCAPKIT_TAG_JOB" = "skipped" ] && [ "$PCAPKIT_CONDA_TAG_EXISTS" = "true" ] && reconciled_tag=true

reconciled_pypi=false
[ "$PCAPKIT_PYPI_JOB" = "success" ] && reconciled_pypi=true
[ "$PCAPKIT_PYPI_JOB" = "skipped" ] && [ "$PCAPKIT_PYPI_COMPLETE" = "true" ] && reconciled_pypi=true

reconciled_conda=false
[ "$PCAPKIT_CONDA_JOB" = "success" ] && reconciled_conda=true
[ "$PCAPKIT_CONDA_JOB" = "skipped" ] && [ "$PCAPKIT_CONDA_COMPLETE" = "true" ] && reconciled_conda=true

all_reconciled=false
if [ "$reconciled_github" = "true" ] && [ "$reconciled_tag" = "true" ] \
&& [ "$reconciled_pypi" = "true" ] && [ "$reconciled_conda" = "true" ]; then
all_reconciled=true
fi

all_skipped=true
any_broken=false
for result in "$PCAPKIT_GITHUB_JOB" "$PCAPKIT_TAG_JOB" "$PCAPKIT_PYPI_JOB" "$PCAPKIT_CONDA_JOB"; do
[ "$result" = "skipped" ] || all_skipped=false
if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then
any_broken=true
fi
done

# Case 2: every target reconciled by skipping -- the usual steady state.
if [ "$all_reconciled" = "true" ] && [ "$all_skipped" = "true" ]; then
echo "::notice title=Nothing to release::pypcapkit ${PCAPKIT_VERSION} is already fully released -- the v* tag, the conda-*+0 tag and every PyPI and Anaconda file version_check expects are present. This run correctly released nothing."
exit 0
fi

# Every target reconciled and at least one ran: a fresh release or a
# self-heal, both quiet.
if [ "$all_reconciled" = "true" ]; then
echo "::notice title=Release complete::pypcapkit ${PCAPKIT_VERSION} is now fully released -- github=${PCAPKIT_GITHUB_JOB} tag=${PCAPKIT_TAG_JOB} pypi=${PCAPKIT_PYPI_JOB} conda=${PCAPKIT_CONDA_JOB}. A 'skipped' entry there means that target's own artefact already existed before this run, not that anything was missed."
exit 0
fi

if [ "$any_broken" = "true" ]; then
echo "::warning title=Release did not complete::github=${PCAPKIT_GITHUB_JOB} tag=${PCAPKIT_TAG_JOB} pypi=${PCAPKIT_PYPI_JOB} conda=${PCAPKIT_CONDA_JOB} for ${PCAPKIT_VERSION}. At least one release job failed or was cancelled outright -- see its own log; this run's checks already show that failure in red."
exit 0
fi

# Case 3, the #888 shape: a target skipped despite its own evidence
# saying incomplete, with nothing failed or cancelled to explain it.
# The evidence-based `if:`s should make this unreachable, so reaching
# it means that assumption broke: loud, and never green.
echo "::error title=Stranded partial release (#888)::${PCAPKIT_VERSION} is only partially published (tag exists=${PCAPKIT_TAG_EXISTS}, conda tag exists=${PCAPKIT_CONDA_TAG_EXISTS}, pypi complete=${PCAPKIT_PYPI_COMPLETE}, conda complete=${PCAPKIT_CONDA_COMPLETE}) but github=${PCAPKIT_GITHUB_JOB} tag=${PCAPKIT_TAG_JOB} pypi=${PCAPKIT_PYPI_JOB} conda=${PCAPKIT_CONDA_JOB} -- at least one target skipped despite its own evidence saying it is not done. Re-running this workflow should let the incomplete jobs self-heal; if this recurs on the next run too, the evidence checks in version_check are the first thing to check."
exit 1
run: python3 util/release_status.py
9 changes: 9 additions & 0 deletions docs/source/contributing/workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,15 @@ does; each has tests under :file:`tests/project/` that run it.
* - :file:`util/release_upload.py`
- ``Upload package to GitHub Release`` in ``create-release.yml``'s ``pypi`` and
``conda`` (`#1640 <https://github.com/JarryShaw/PyPCAPKit/issues/1640>`__).
* - :file:`util/release_status.py`
- ``Report why this run did or did not release anything`` in
``create-release.yml``'s ``release_status``, on the image's ``python3``
after a checkout of :file:`util/` alone (`#888
<https://github.com/JarryShaw/PyPCAPKit/issues/888>`__).
* - :file:`util/release_vendor_update.py`
- ``Read whether its vendor-update job ran`` in ``create-release.yml``'s
``vendor_update``, on the image's ``python3`` after a checkout of
:file:`util/` alone (`#1748 <https://github.com/JarryShaw/PyPCAPKit/issues/1748>`__).

The summaries run on the image's ``/usr/bin/python3`` rather than the leg's
interpreter, which may have failed to set up or be too broken to run anything, so
Expand Down
7 changes: 5 additions & 2 deletions tests/project/test_actions_helper_2047.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@
HELPER = ROOT / 'util' / '_actions.py'
#: The scripts that import the helper.
SCRIPTS = ('apt_packages', 'check_suite_report', 'ci_timing', 'classify_changes', 'codeql_report',
'coverage_report', 'leg_summary', 'pip_install', 'regenerate_samples', 'release_tags', 'release_upload',
'update_python_versions', 'vendor_code_changed', 'vendor_test_gate')
'coverage_report', 'leg_summary', 'pip_install', 'regenerate_samples', 'release_status', 'release_tags',
'release_upload', 'release_vendor_update', 'update_python_versions', 'vendor_code_changed',
'vendor_test_gate')
#: The top-level modules the helper may import, each in Python 3.7's standard
#: library: the running interpreter's :data:`sys.stdlib_module_names` would
#: pass ``tomllib``, which 3.7 lacks.
Expand All @@ -58,8 +59,10 @@
'leg_summary': (['ktlo'], {'LEG': 'Python 3.8 (KTLO, non-blocking)', 'STEPS': '{}'}),
'pip_install': (['--help'], {}),
'regenerate_samples': (['--help'], {}),
'release_status': (['--help'], {}),
'release_tags': (['--help'], {}),
'release_upload': (['--help'], {}),
'release_vendor_update': (['--help'], {}),
'update_python_versions': (['--help'], {}),
'vendor_code_changed': (['--help'], {}),
'vendor_test_gate': (['--help'], {}),
Expand Down
5 changes: 4 additions & 1 deletion tests/project/test_discussion_report_2030.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,10 @@ def test_create_release_still_reads_vendor_update_alone(self) -> None:
spec = load('cron-vendor.yml')
self.assertNotIn('name', spec['jobs']['vendor-update'])
self.assertEqual([job.get('name', job_id) for job_id, job in spec['jobs'].items()].count('vendor-update'), 1)
release = (WORKFLOWS / 'create-release.yml').read_text(encoding='utf-8')
# #1967: Create Release's vendor_update runs util/release_vendor_update.py, which selects it.
self.assertIn('run: python3 util/release_vendor_update.py',
(WORKFLOWS / 'create-release.yml').read_text(encoding='utf-8'))
release = (ROOT / 'util' / 'release_vendor_update.py').read_text(encoding='utf-8')
self.assertIn('select(.name == "vendor-update")', release)

def test_the_vendor_report_steps_never_fail_the_job(self) -> None:
Expand Down
Loading
Loading