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
117 changes: 3 additions & 114 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,42 +252,7 @@ jobs:
PCAPKIT_TAG_EXISTS: ${{ steps.check_tag.outputs.exists }}
PCAPKIT_PYPI_WHEELS: ${{ steps.python-versions.outputs.pypi-wheels }}
PCAPKIT_CONDA_BUILDS: ${{ steps.python-versions.outputs.conda-builds }}
run: |
set -euo pipefail

wheels="$PCAPKIT_PYPI_WHEELS"
builds="$PCAPKIT_CONDA_BUILDS"
tiers=.github/python-versions.toml
if [ "$PCAPKIT_TAG_EXISTS" = true ]; then
# The checkout fetched every tag, but not one pushed since.
git rev-parse -q --verify "refs/tags/${PCAPKIT_TAG}^{commit}" > /dev/null \
|| git fetch -q --no-tags origin "+refs/tags/${PCAPKIT_TAG}:refs/tags/${PCAPKIT_TAG}"
from="$(git rev-parse "refs/tags/${PCAPKIT_TAG}^{commit}")"
if ! git cat-file -e "${from}:${tiers}" 2> /dev/null; then
from="$(git log --diff-filter=A --format=%H -- "$tiers" | tail -n 1)"
if [ -z "$from" ]; then
echo "::error title=No tier file in the history::${tiers} was never added to this checkout's history."
exit 1
fi
echo "::notice title=Released before the tier file::${PCAPKIT_TAG} predates ${tiers}; counting the tiers ${from} added it with."
fi
if [ "$from" != "$(git rev-parse HEAD)" ]; then
released="$(mktemp -d)"
git show "${from}:${tiers}" > "${released}/python-versions.toml"
git show "${from}:util/python_versions.py" > "${released}/python_versions.py"
# #2047: the reader imports util/_actions.py from beside it, from the
# commit that has it.
if git cat-file -e "${from}:util/_actions.py" 2> /dev/null; then
git show "${from}:util/_actions.py" > "${released}/_actions.py"
fi
wheels="$(python3 "${released}/python_versions.py" --file "${released}/python-versions.toml" pypi-wheels)"
builds="$(python3 "${released}/python_versions.py" --file "${released}/python-versions.toml" conda-builds)"
fi
fi

echo "${PCAPKIT_TAG}: counting the wheels ${wheels} and the conda builds ${builds}"
echo "pypi-wheels=${wheels}" >> "$GITHUB_OUTPUT"
echo "conda-builds=${builds}" >> "$GITHUB_OUTPUT"
run: python3 util/release_tiers.py

# PyPI evidence: a wheel per supported-tier leg (``PCAPKIT_PYPI_WHEELS``;
# #1633 -- a KTLO wheel neither completes the release nor, failing, leaves
Expand All @@ -303,44 +268,7 @@ jobs:
env:
PCAPKIT_VERSION: ${{ steps.get_version.outputs.PCAPKIT_VERSION }}
PCAPKIT_PYPI_WHEELS: ${{ steps.released.outputs.pypi-wheels }}
run: |
set -euo pipefail

# One wheel per supported leg, plus the sdist.
read -r -a wheels <<< "$PCAPKIT_PYPI_WHEELS"
expected=$(( ${#wheels[@]} + 1 ))
# `--retry-connrefused` and `|| echo 000` keep a pypi.org outage from
# failing `version_check` under `set -e`, which every publisher
# requires; "incomplete" is the safe default.
status="$(curl -s --retry 3 --retry-connrefused \
-o /tmp/pypi_release.json -w '%{http_code}' \
"https://pypi.org/pypi/pypcapkit/${PCAPKIT_VERSION}/json" || echo 000)"

count=0
if [ "$status" = "200" ]; then
# One line per `printf` argument, so the block scalar's indentation
# never reaches the source `python3 -` reads: indented `python3 -c`
# source compiles only on Python 3.14 (3.9 through 3.13 raise
# `IndentationError: unexpected indent`, measured). Not leaning on
# the setup-python above: the conda job's check runs before its own.
count="$(printf '%s\n' \
"import json, os" \
"with open('/tmp/pypi_release.json') as f:" \
" data = json.load(f)" \
"supported = set(os.environ['PCAPKIT_PYPI_WHEELS'].split())" \
"names = [u.get('filename', '') for u in data.get('urls', [])]" \
"wheels = {n.split('-')[2] for n in names if n.endswith('.whl') and n.count('-') == 4}" \
"print(len(wheels & supported) + any(n.endswith('.tar.gz') for n in names))" \
| python3 -)"
fi

complete=false
if [ "$count" -ge "$expected" ]; then
complete=true
fi

echo "http $status, found $count of $expected expected files for ${PCAPKIT_VERSION} on PyPI"
echo "PCAPKIT_PYPI_COMPLETE=$complete" >> "$GITHUB_OUTPUT"
run: python3 util/release_published.py pypi /tmp/pypi_release.json

# Anaconda evidence, as PyPI's above, per platform and Python: the
# job-level cost saver, under which ``conda``'s per-leg checks, the
Expand All @@ -351,46 +279,7 @@ jobs:
env:
PCAPKIT_VERSION: ${{ steps.get_version.outputs.PCAPKIT_VERSION }}
PCAPKIT_CONDA_BUILDS: ${{ steps.released.outputs.conda-builds }}
run: |
set -euo pipefail

# Each platform the ``conda`` legs publish, for each supported version.
export PCAPKIT_CONDA_SUBDIRS="linux-64 osx-64 osx-arm64"
read -r -a subdirs <<< "$PCAPKIT_CONDA_SUBDIRS"
read -r -a builds <<< "$PCAPKIT_CONDA_BUILDS"
expected=$(( ${#subdirs[@]} * ${#builds[@]} ))
# Same coupling risk, and the same fix, as the PyPI check above.
status="$(curl -s --retry 3 --retry-connrefused \
-o /tmp/anaconda_release.json -w '%{http_code}' \
"https://api.anaconda.org/release/jarryshaw/pypcapkit/${PCAPKIT_VERSION}" || echo 000)"

count=0
if [ "$status" = "200" ]; then
# The same `printf` form as the PyPI check above.
count="$(printf '%s\n' \
"import json, os" \
"with open('/tmp/anaconda_release.json') as f:" \
" data = json.load(f)" \
"platforms = set(os.environ['PCAPKIT_CONDA_SUBDIRS'].split())" \
"py_tags = set(os.environ['PCAPKIT_CONDA_BUILDS'].split())" \
"seen = {" \
" (d.get('attrs', {}).get('subdir'), tag)" \
" for d in data.get('distributions', [])" \
" for tag in py_tags" \
" if d.get('attrs', {}).get('build', '').startswith(tag + '_')" \
" and d.get('attrs', {}).get('subdir') in platforms" \
"}" \
"print(len(seen))" \
| python3 -)"
fi

complete=false
if [ "$count" -ge "$expected" ]; then
complete=true
fi

echo "http $status, found $count of $expected expected platform/Python combinations for ${PCAPKIT_VERSION} on Anaconda"
echo "PCAPKIT_CONDA_COMPLETE=$complete" >> "$GITHUB_OUTPUT"
run: python3 util/release_published.py anaconda /tmp/anaconda_release.json

# #1748: resolved as #1634's check in ``github``, ``pypi`` and ``conda``
# resolves it, so the gates skip exactly the runs that check would refuse.
Expand Down
9 changes: 9 additions & 0 deletions docs/source/contributing/workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,15 @@ does; each has tests under :file:`tests/project/` that run it.
- ``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>`__).
* - :file:`util/release_tiers.py`
- ``Read the released Python versions`` in ``create-release.yml``'s
``version_check``, on setup-python's ``python3`` (`#1660
<https://github.com/JarryShaw/PyPCAPKit/issues/1660>`__).
* - :file:`util/release_published.py`
- ``Check PyPI publish completeness`` and ``Check Anaconda publish
completeness`` in ``create-release.yml``'s ``version_check``, as
``release_published.py pypi`` and ``anaconda``, on setup-python's
``python3`` (`#888 <https://github.com/JarryShaw/PyPCAPKit/issues/888>`__).

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
8 changes: 5 additions & 3 deletions tests/project/test_actions_helper_2047.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@
#: The scripts that import the helper.
SCRIPTS = ('apt_packages', 'check_suite_report', 'ci_timing', 'classify_changes', 'codeql_report',
'coverage_report', 'discussion_report', 'leg_summary', 'pip_install', 'project_status',
'python_versions', 'regenerate_samples', 'release_status', 'release_tags', 'release_upload',
'release_vendor_update', 'superseded', 'update_python_versions', 'vendor_code_changed',
'vendor_test_gate', 'verdict_covers_checkout')
'python_versions', 'regenerate_samples', 'release_published', 'release_status', 'release_tags',
'release_tiers', 'release_upload', 'release_vendor_update', 'superseded', 'update_python_versions',
'vendor_code_changed', 'vendor_test_gate', 'verdict_covers_checkout')
#: 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 @@ -66,8 +66,10 @@
'project_status': (['--help'], {}),
'python_versions': (['latest'], {}),
'regenerate_samples': (['--help'], {}),
'release_published': (['--help'], {}),
'release_status': (['--help'], {}),
'release_tags': (['--help'], {}),
'release_tiers': (['--help'], {}),
'release_upload': (['--help'], {}),
'release_vendor_update': (['--help'], {}),
'superseded': (['--help'], {}),
Expand Down
25 changes: 13 additions & 12 deletions tests/project/test_conda_completeness_counts_every_leg_1922.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
"""``version_check``'s Anaconda completeness check counts every platform the ``conda`` legs publish (#1922).

* The platforms it counts are the ones the ``conda`` job's ``os`` legs map to,
read from that job's matrix and its ``case "${{ matrix.os }}"`` arms.
* The platforms it counts, :file:`util/release_published.py`'s ``SUBDIRS``
since #1967, are the ones the ``conda`` job's ``os`` legs map to, read from
that job's matrix and its ``case "${{ matrix.os }}"`` arms.
* Run as the workflow runs it, with ``curl`` stubbed, a release reads complete
only with each of those platforms for each build, so a missing ``osx-arm64``
upload reads incomplete and the ``conda`` gate runs the leg that lacks it.
Expand All @@ -17,11 +18,13 @@
import re
import shutil
import subprocess # nosec: B404
import sys
import tempfile
import unittest
from typing import TYPE_CHECKING

from tests import _workflows
from util import release_published

try:
import yaml
Expand All @@ -38,8 +41,6 @@

#: A ``case "${{ matrix.os }}" in`` arm setting ``target_platform``.
ARM = re.compile(r'^\s*([\w.-]+)\)\s*\n\s*target_platform="([^"]*)"', re.MULTILINE)
#: The check's own list of platforms, which sets both its count and its set.
SUBDIRS = re.compile(r'^\s*export PCAPKIT_CONDA_SUBDIRS="([^"]*)"\s*$', re.MULTILINE)


def jobs() -> 'dict[str, Any]':
Expand Down Expand Up @@ -70,16 +71,15 @@ class TheCheckCountsTheLegs(unittest.TestCase):
"""What the check counts is what the legs publish."""

def test_the_check_lists_each_published_platform(self) -> None:
listed = SUBDIRS.findall(check()['run'])
self.assertEqual(len(listed), 1, 'the check names its platforms in one PCAPKIT_CONDA_SUBDIRS list')
self.assertEqual(sorted(listed[0].split()), sorted(published()))
"""Its one list of platforms, which sets both its count and its set."""
self.assertEqual(check()['run'].split()[:3], ['python3', 'util/release_published.py', 'anaconda'])
self.assertEqual(sorted(release_published.SUBDIRS.split()), sorted(published()))
self.assertIn('osx-arm64', published())


@unittest.skipIf(shutil.which('bash') is None or shutil.which('python3') is None,
'bash and python3 are needed to run the workflow shell')
@unittest.skipIf(shutil.which('python3') is None, 'python3 is needed to run the count')
class TheCheckRunsAsTheWorkflowRunsIt(unittest.TestCase):
"""The step's script, with ``curl`` answering from a fake Anaconda release."""
"""The step's script, on this interpreter, with ``curl`` answering from a fake Anaconda release."""

def complete(self, distributions: 'list[tuple[str, str]]') -> 'tuple[str, str]':
"""``PCAPKIT_CONDA_COMPLETE`` and the log for a release holding these (platform, build) pairs."""
Expand All @@ -96,8 +96,9 @@ def complete(self, distributions: 'list[tuple[str, str]]') -> 'tuple[str, str]':
curl.chmod(0o755)
output = scratch / 'output'
output.write_text('', encoding='utf-8')
result = subprocess.run( # nosec: B603 B607
['bash', '-e', '-c', step['run'].replace('/tmp/anaconda_release.json', str(scratch / 'release.json'))],
words = step['run'].replace('/tmp/anaconda_release.json', str(scratch / 'release.json')).split()
result = subprocess.run( # nosec: B603
[sys.executable, str(ROOT / words[1]), *words[2:]],
env=dict(os.environ, PATH=f'{scratch}{os.pathsep}{os.environ["PATH"]}', REPLY=str(reply),
GITHUB_OUTPUT=str(output), PCAPKIT_VERSION='1.5.0', PCAPKIT_CONDA_BUILDS=' '.join(BUILDS)),
capture_output=True, text=True, check=False, timeout=60)
Expand Down
11 changes: 7 additions & 4 deletions tests/project/test_python_matrix_tiers_1633.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,10 +391,13 @@ def pypi_complete(self, files: 'list[str]') -> 'str':
curl.chmod(0o755)
output = self.tmp / 'output'
output.write_text('', encoding='utf-8')
result = bash(script.replace('/tmp/pypi_release.json', str(self.tmp / 'release.json')),
{'PATH': f'{bin_dir}{os.pathsep}{os.environ["PATH"]}', 'REPLY': str(reply),
'GITHUB_OUTPUT': str(output), 'PCAPKIT_VERSION': '1.5.0',
'PCAPKIT_PYPI_WHEELS': wheels})
# #1967: ``python3 util/release_published.py pypi ...``, on this interpreter.
command = script_command(script.replace('/tmp/pypi_release.json', str(self.tmp / 'release.json')))
assert command is not None, script
result = subprocess.run(command, env=dict(os.environ, PATH=f'{bin_dir}{os.pathsep}{os.environ["PATH"]}',
REPLY=str(reply), GITHUB_OUTPUT=str(output),
PCAPKIT_VERSION='1.5.0', PCAPKIT_PYPI_WHEELS=wheels),
capture_output=True, text=True, check=False, timeout=60)
self.assertEqual(result.returncode, 0, result.stderr)
return dict(line.split('=', 1) for line in output.read_text(encoding='utf-8').split())['PCAPKIT_PYPI_COMPLETE']

Expand Down
5 changes: 4 additions & 1 deletion tests/project/test_python_versions_1660.py
Original file line number Diff line number Diff line change
Expand Up @@ -908,8 +908,11 @@ def complete(self, distributions: 'list[tuple[str, str]]') -> 'str':
curl.chmod(0o755)
output = scratch / 'output'
output.write_text('', encoding='utf-8')
# #1967: ``python3 util/release_published.py anaconda ...``, on this interpreter.
words = step['run'].replace('/tmp/anaconda_release.json', str(scratch / 'release.json')).split()
self.assertEqual(words[:2], ['python3', 'util/release_published.py'])
result = subprocess.run(
['bash', '-e', '-c', step['run'].replace('/tmp/anaconda_release.json', str(scratch / 'release.json'))],
[sys.executable, str(ROOT / words[1]), *words[2:]],
env=dict(os.environ, PATH=f'{scratch}{os.pathsep}{os.environ["PATH"]}', REPLY=str(reply),
GITHUB_OUTPUT=str(output), PCAPKIT_VERSION='1.5.0', PCAPKIT_CONDA_BUILDS=builds),
capture_output=True, text=True, check=False, timeout=60)
Expand Down
43 changes: 30 additions & 13 deletions tests/project/test_release_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2190,6 +2190,10 @@ def test_dropping_any_gate_clause_is_caught(self) -> None:
self.assertTrue(gate_coverage_violations(mutant, self.jobs))


#: ``version_check``'s PyPI and Anaconda evidence checks, since #1967.
EVIDENCE_SCRIPT = 'util/release_published.py'


class TestEvidenceBasedGating(WorkflowMixin, unittest.TestCase):
"""#888: ``tag``, ``pypi`` and ``conda`` read their own evidence now,
instead of ``PCAPKIT_TAG_EXISTS`` -- the tag ``github`` creates, which
Expand Down Expand Up @@ -2421,15 +2425,16 @@ def test_pypi_evidence_reads_the_per_version_endpoint(self) -> None:
make the evidence check always read 0 files.

"""
block = self.jobs['version_check']
self.assertIn('pypi.org/pypi/pypcapkit/', block)
self.assertIn("data.get('urls'", block)
# #1967: the step runs util/release_published.py, which holds the check.
self.assertIn(f'run: python3 {EVIDENCE_SCRIPT} pypi ', self.jobs['version_check'])
script = (ROOT / EVIDENCE_SCRIPT).read_text(encoding='utf-8')
self.assertIn("PYPI_URL = 'https://pypi.org/pypi/pypcapkit/'", script)
self.assertIn("data.get('urls'", script)

def test_conda_evidence_checks_every_platform(self) -> None:
block = self.jobs['version_check']
self.assertIn('linux-64', block)
self.assertIn('osx-64', block)
self.assertIn('osx-arm64', block)
self.assertIn(f'run: python3 {EVIDENCE_SCRIPT} anaconda ', self.jobs['version_check'])
script = (ROOT / EVIDENCE_SCRIPT).read_text(encoding='utf-8')
self.assertIn("SUBDIRS = 'linux-64 osx-64 osx-arm64'", script)


class TestGateExpressionsEvaluateCorrectly(WorkflowMixin, unittest.TestCase):
Expand Down Expand Up @@ -3559,18 +3564,30 @@ class TestPythonSnippetsSurviveOlderInterpreters(WorkflowMixin, unittest.TestCas
itself put there -- this class is what actually runs each snippet under
an older interpreter to check that claim, rather than trusting it.

Since #1967 the PyPI and Anaconda evidence checks in ``version_check`` feed
``python3 -`` from :file:`util/release_published.py` instead, so this runs
those snippets too, as that script builds them.

"""

def snippets(self) -> 'list[str]':
"""The workflow's ``printf`` snippets, then the evidence script's."""
from util import release_published # pylint: disable=import-outside-toplevel
return printf_python_snippets(self.text) + [ # type: ignore[attr-defined]
source('/tmp/release.json') for source in (release_published.pypi_source,
release_published.anaconda_source)]

def test_the_scan_found_the_snippets(self) -> None:
"""A scan that finds nothing would pass every test below it."""
snippets = printf_python_snippets(self.text) # type: ignore[attr-defined]
self.assertEqual(
len(snippets), 3,
'expected exactly 3 printf-fed python3 snippets (the PyPI and '
'Anaconda evidence checks in version_check, plus the per-leg '
'check in conda); found a different count -- if that is '
'intended, this expectation needs revisiting alongside it',
len(snippets), 1,
'expected exactly 1 printf-fed python3 snippet (the per-leg '
'check in conda; version_check\'s two run from '
'util/release_published.py since #1967); found a different count '
'-- if that is intended, this expectation needs revisiting alongside it',
)
self.assertEqual(len(self.snippets()), 3)

def test_each_snippet_runs_without_an_indentation_error(self) -> None:
interpreter = pre_314_interpreter()
Expand All @@ -3581,7 +3598,7 @@ def test_each_snippet_runs_without_an_indentation_error(self) -> None:
[interpreter, '--version'], capture_output=True, text=True,
).stdout.strip()

for index, snippet in enumerate(printf_python_snippets(self.text)): # type: ignore[attr-defined]
for index, snippet in enumerate(self.snippets()):
with self.subTest(snippet=index):
# `$target_platform`/`$py_tag` are bash interpolation the real
# step performs before Python ever sees this text; harmless
Expand Down
Loading
Loading