From 17212b8590767b46c55b6b0029f75e47404e421b Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 14 Mar 2026 14:49:55 -0400 Subject: [PATCH 1/7] Drop 3.8, fix mypy, add ty, bump test req versions --- .github/workflows/tests.yml | 171 ++++++++++------------- .readthedocs.yml | 15 ++ build_wheels.sh | 12 +- docs/source/conf.py | 16 ++- line_profiler/_logger.py | 6 +- line_profiler/autoprofile/autoprofile.py | 2 +- line_profiler/cli_utils.py | 4 +- pyproject.toml | 13 +- requirements/ipython.txt | 4 +- requirements/optional.txt | 2 +- requirements/tests.txt | 17 +-- 11 files changed, 132 insertions(+), 130 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b44389ff..8c779fe0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v6.0.2 - name: Set up Python 3.13 for linting uses: actions/setup-python@v5.6.0 with: @@ -34,12 +34,14 @@ jobs: run: |- # stop the build if there are Python syntax errors or undefined names flake8 ./line_profiler --count --select=E9,F63,F7,F82 --show-source --statistics - - name: Typecheck with mypy + - name: Typecheck run: |- python -m pip install mypy pip install -r requirements/runtime.txt - mypy --install-types --non-interactive ./line_profiler mypy ./line_profiler + python -m pip install ty + pip install -r requirements/runtime.txt + ty check ./line_profiler build_and_test_sdist: ## # Build the binary package from source and test it in the same @@ -49,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v6.0.2 - name: Set up Python 3.13 uses: actions/setup-python@v5.6.0 with: @@ -57,8 +59,8 @@ jobs: - name: Upgrade pip run: |- python -m pip install pip uv -U - python -m uv pip install -r requirements/tests.txt - python -m uv pip install -r requirements/runtime.txt + python -m pip install --prefer-binary -r requirements/tests.txt + python -m pip install --prefer-binary -r requirements/runtime.txt - name: Build sdist shell: bash run: |- @@ -69,7 +71,7 @@ jobs: - name: Install sdist run: |- ls -al wheelhouse - python -m uv pip install wheelhouse/line_profiler*.tar.gz -v + python -m pip install --prefer-binary wheelhouse/line_profiler*.tar.gz -v - name: Test minimal loose sdist env: COVERAGE_CORE: ctrace @@ -103,7 +105,7 @@ jobs: echo "MOD_DPATH = $MOD_DPATH" python -m pytest --verbose --cov=line_profiler $MOD_DPATH ../tests cd .. - - uses: actions/upload-artifact@v4.4.0 + - uses: actions/upload-artifact@v6.0.0 name: Upload sdist artifact with: name: sdist_wheels @@ -126,41 +128,35 @@ jobs: # explicitly here. os: - ubuntu-latest - # Overhead of building ARM wheels on Intel Linux nodes is - # unreasonably high (20s build time per wheel vs 3m); - # it's better to just spin another runner up to build them - # natively - - ubuntu-24.04-arm - macOS-latest - windows-latest + - ubuntu-24.04-arm cibw_skip: - '*-win32 cp3{9,10}-win_arm64 cp313-musllinux_i686' arch: - auto steps: - name: Checkout source - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v6.0.2 - name: Enable MSVC 64bit uses: ilammy/msvc-dev-cmd@v1 if: ${{ startsWith(matrix.os, 'windows-') }} && ${{ contains(matrix.cibw_skip, '*-win32') }} with: arch: ${{ contains(matrix.os, 'arm') && 'arm64' || 'x64' }} - # Note: Since we're building Linux wheels on their native - # architectures, we don't need QEMU - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 + uses: docker/setup-qemu-action@v3.7.0 if: runner.os == 'Linux' && matrix.arch != 'auto' with: platforms: all - name: Build binary wheels - uses: pypa/cibuildwheel@v3.1.2 + uses: pypa/cibuildwheel@v3.3.1 with: output-dir: wheelhouse config-file: pyproject.toml env: CIBW_SKIP: ${{ matrix.cibw_skip }} CIBW_TEST_SKIP: '*-win_arm64' - CIBW_ENVIRONMENT: PYTHONUTF8=1 + CIBW_ARCHS_LINUX: ${{ matrix.arch }} PYTHONUTF8: '1' VSCMD_ARG_TGT_ARCH: '' - name: Show built files @@ -186,7 +182,7 @@ jobs: echo '### The cwd should now have a coverage.xml' ls -altr pwd - - uses: codecov/codecov-action@v5.4.3 + - uses: codecov/codecov-action@v5.5.2 name: Codecov Upload env: HAVE_CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN != '' }} @@ -195,12 +191,12 @@ jobs: with: file: ./coverage.xml token: ${{ secrets.CODECOV_TOKEN }} - - uses: codecov/codecov-action@v5.4.3 + - uses: codecov/codecov-action@v5.5.2 name: Codecov Upload with: file: ./coverage.xml token: ${{ secrets.CODECOV_TOKEN }} - - uses: actions/upload-artifact@v4.4.0 + - uses: actions/upload-artifact@v6.0.0 name: Upload wheels artifact with: name: wheels-${{ matrix.os }}-${{ matrix.arch }} @@ -222,29 +218,25 @@ jobs: # Xcookie generates an explicit list of environments that will be used # for testing instead of using the more concise matrix notation. include: - - python-version: '3.8' + - python-version: '3.9' install-extras: tests-strict,runtime-strict os: ubuntu-latest arch: auto - - python-version: '3.8' - install-extras: tests-strict,runtime-strict - os: ubuntu-24.04-arm - arch: auto - - python-version: '3.8' + - python-version: '3.9' install-extras: tests-strict,runtime-strict os: macOS-latest arch: auto - - python-version: '3.8' + - python-version: '3.9' install-extras: tests-strict,runtime-strict os: windows-latest arch: auto - - python-version: '3.13' - install-extras: tests-strict,runtime-strict,optional-strict - os: ubuntu-latest + - python-version: '3.9' + install-extras: tests-strict,runtime-strict + os: ubuntu-24.04-arm arch: auto - python-version: '3.13' install-extras: tests-strict,runtime-strict,optional-strict - os: ubuntu-24.04-arm + os: ubuntu-latest arch: auto - python-version: '3.13' install-extras: tests-strict,runtime-strict,optional-strict @@ -256,32 +248,28 @@ jobs: arch: auto - python-version: '3.13' install-extras: tests-strict,runtime-strict,optional-strict - os: windows-11-arm + os: ubuntu-24.04-arm arch: auto - python-version: '3.13' - install-extras: tests - os: ubuntu-latest + install-extras: tests-strict,runtime-strict,optional-strict + os: windows-11-arm arch: auto - python-version: '3.13' install-extras: tests - os: ubuntu-24.04-arm + os: macOS-latest arch: auto - python-version: '3.13' install-extras: tests - os: macOS-latest + os: windows-latest arch: auto - python-version: '3.13' install-extras: tests - os: windows-latest + os: ubuntu-24.04-arm arch: auto - python-version: '3.13' install-extras: tests os: windows-11-arm arch: auto - - python-version: '3.8' - install-extras: tests,optional - os: ubuntu-latest - arch: auto - python-version: '3.9' install-extras: tests,optional os: ubuntu-latest @@ -306,89 +294,77 @@ jobs: install-extras: tests,optional os: ubuntu-latest arch: auto - - python-version: '3.8' - install-extras: tests,optional - os: ubuntu-24.04-arm - arch: auto - python-version: '3.9' install-extras: tests,optional - os: ubuntu-24.04-arm + os: macOS-latest arch: auto - python-version: '3.10' install-extras: tests,optional - os: ubuntu-24.04-arm + os: macOS-latest arch: auto - python-version: '3.11' install-extras: tests,optional - os: ubuntu-24.04-arm + os: macOS-latest arch: auto - python-version: '3.12' install-extras: tests,optional - os: ubuntu-24.04-arm + os: macOS-latest arch: auto - python-version: '3.13' install-extras: tests,optional - os: ubuntu-24.04-arm + os: macOS-latest arch: auto - python-version: '3.14' - install-extras: tests,optional - os: ubuntu-24.04-arm - arch: auto - - python-version: '3.8' install-extras: tests,optional os: macOS-latest arch: auto - python-version: '3.9' install-extras: tests,optional - os: macOS-latest + os: windows-latest arch: auto - python-version: '3.10' install-extras: tests,optional - os: macOS-latest + os: windows-latest arch: auto - python-version: '3.11' install-extras: tests,optional - os: macOS-latest + os: windows-latest arch: auto - python-version: '3.12' install-extras: tests,optional - os: macOS-latest + os: windows-latest arch: auto - python-version: '3.13' install-extras: tests,optional - os: macOS-latest + os: windows-latest arch: auto - python-version: '3.14' - install-extras: tests,optional - os: macOS-latest - arch: auto - - python-version: '3.8' install-extras: tests,optional os: windows-latest arch: auto - python-version: '3.9' install-extras: tests,optional - os: windows-latest + os: ubuntu-24.04-arm arch: auto - python-version: '3.10' install-extras: tests,optional - os: windows-latest + os: ubuntu-24.04-arm arch: auto - python-version: '3.11' install-extras: tests,optional - os: windows-latest + os: ubuntu-24.04-arm arch: auto - python-version: '3.12' install-extras: tests,optional - os: windows-latest + os: ubuntu-24.04-arm arch: auto - python-version: '3.13' install-extras: tests,optional - os: windows-latest + os: ubuntu-24.04-arm arch: auto - python-version: '3.14' install-extras: tests,optional - os: windows-latest + os: ubuntu-24.04-arm arch: auto - python-version: '3.11' install-extras: tests,optional @@ -408,16 +384,14 @@ jobs: arch: auto steps: - name: Checkout source - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v6.0.2 - name: Enable MSVC 64bit uses: ilammy/msvc-dev-cmd@v1 if: ${{ startsWith(matrix.os, 'windows-') }} with: arch: ${{ contains(matrix.os, 'arm') && 'arm64' || 'x64' }} - # Note: Since we're testing Linux wheels on their native - # architectures, we don't need QEMU - name: Set up QEMU - uses: docker/setup-qemu-action@v3.0.0 + uses: docker/setup-qemu-action@v3.7.0 if: runner.os == 'Linux' && matrix.arch != 'auto' with: platforms: all @@ -443,28 +417,35 @@ jobs: echo "Installing helpers: setuptools" python -m uv pip install setuptools>=0.8 setuptools_scm wheel build -U echo "Installing helpers: tomli and pkginfo" - python -m uv pip install tomli pkginfo + python -m uv pip install tomli pkginfo packaging export WHEEL_FPATH=$(python -c "if 1: import pathlib + from packaging import tags + from packaging.utils import parse_wheel_filename dist_dpath = pathlib.Path('wheelhouse') - candidates = list(dist_dpath.glob('line_profiler*.whl')) - candidates += list(dist_dpath.glob('line_profiler*.tar.gz')) - fpath = sorted(candidates)[-1] + wheels = sorted(dist_dpath.glob('line_profiler*.whl')) + if wheels: + sys_tags = set(tags.sys_tags()) + matching = [] + for w in wheels: + try: + _, _, _, wheel_tags = parse_wheel_filename(w.name) + except Exception: + continue + if any(t in sys_tags for t in wheel_tags): + matching.append(w) + fpath = sorted(matching or wheels)[-1] + else: + sdists = sorted(dist_dpath.glob('line_profiler*.tar.gz')) + if not sdists: + raise SystemExit('No wheel artifacts found in wheelhouse') + fpath = sdists[-1] print(str(fpath).replace(chr(92), chr(47))) ") - export MOD_VERSION=$(python -c "if 1: - from pkginfo import Wheel, SDist - import pathlib - fpath = '$WHEEL_FPATH' - cls = Wheel if fpath.endswith('.whl') else SDist - item = cls(fpath) - print(item.version) - ") echo "WHEEL_FPATH=$WHEEL_FPATH" echo "INSTALL_EXTRAS=$INSTALL_EXTRAS" echo "UV_RESOLUTION=$UV_RESOLUTION" - echo "MOD_VERSION=$MOD_VERSION" - python -m uv pip install "line_profiler[$INSTALL_EXTRAS]==$MOD_VERSION" -f wheelhouse + python -m pip install --prefer-binary "${WHEEL_FPATH}[${INSTALL_EXTRAS}]" echo "Install finished." - name: Test wheel ${{ matrix.install-extras }} shell: bash @@ -513,7 +494,7 @@ jobs: echo '### The cwd should now have a coverage.xml' ls -altr pwd - - uses: codecov/codecov-action@v5.4.3 + - uses: codecov/codecov-action@v5.5.2 name: Codecov Upload with: file: ./coverage.xml @@ -527,7 +508,7 @@ jobs: - build_binpy_wheels steps: - name: Checkout source - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v6.0.2 - uses: actions/download-artifact@v4.1.8 name: Download wheels with: @@ -584,7 +565,7 @@ jobs: ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc ls -la wheelhouse twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } - - uses: actions/upload-artifact@v4.4.0 + - uses: actions/upload-artifact@v6.0.0 name: Upload deploy artifacts with: name: deploy_artifacts @@ -603,7 +584,7 @@ jobs: - build_binpy_wheels steps: - name: Checkout source - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v6.0.2 - uses: actions/download-artifact@v4.1.8 name: Download wheels with: @@ -660,7 +641,7 @@ jobs: ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc ls -la wheelhouse twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } - - uses: actions/upload-artifact@v4.4.0 + - uses: actions/upload-artifact@v6.0.0 name: Upload deploy artifacts with: name: deploy_artifacts @@ -680,7 +661,7 @@ jobs: - live_deploy steps: - name: Checkout source - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v6.0.2 - uses: actions/download-artifact@v4.1.8 name: Download artifacts with: diff --git a/.readthedocs.yml b/.readthedocs.yml index 1c728dac..79335f21 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,15 +7,30 @@ # Required version: 2 + build: os: "ubuntu-24.04" tools: python: "3.13" + +# Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py + +# Build documentation with MkDocs +#mkdocs: +# configuration: mkdocs.yml + +# Optionally build your docs in additional formats such as PDF and ePub formats: all + python: install: - requirements: requirements/docs.txt - method: pip path: . + #extra_requirements: + # - docs + +#conda: +# environment: environment.yml diff --git a/build_wheels.sh b/build_wheels.sh index 6a6daecb..b8e7d2bb 100755 --- a/build_wheels.sh +++ b/build_wheels.sh @@ -18,5 +18,13 @@ if ! which cibuildwheel ; then exit 1 fi -# Build version-pinned wheels -cibuildwheel --config-file pyproject.toml --platform linux --archs x86_64 +LOCAL_CP_VERSION=$(python3 -c "import sys; print('cp' + ''.join(list(map(str, sys.version_info[0:2]))))") +echo "LOCAL_CP_VERSION = $LOCAL_CP_VERSION" + +# Build for only the current version of Python +export CIBW_BUILD="${LOCAL_CP_VERSION}-*" + + +#pip wheel -w wheelhouse . +# python -m build --wheel -o wheelhouse # line_profiler: +COMMENT_IF(binpy) +cibuildwheel --config-file pyproject.toml --platform linux --archs x86_64 # line_profiler: +UNCOMMENT_IF(binpy) diff --git a/docs/source/conf.py b/docs/source/conf.py index b81e1786..0604cedd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -141,7 +141,7 @@ def visit_Assign(self, node): project = 'line_profiler' -copyright = '2025, Robert Kern' +copyright = '2026, Robert Kern' author = 'Robert Kern' modname = 'line_profiler' @@ -435,7 +435,7 @@ class PatchedPythonDomain(PythonDomain): """ def resolve_xref( - self, env, fromdocname, builder, typ, target, node, contnode + self, env, fromdocname, builder, type, target, node, contnode ): """ Helps to resolves cross-references @@ -445,7 +445,7 @@ def resolve_xref( if target.startswith('xdoc.'): target = 'xdoctest.' + target[3] return_value = super(PatchedPythonDomain, self).resolve_xref( - env, fromdocname, builder, typ, target, node, contnode + env, fromdocname, builder, type, target, node, contnode ) return return_value @@ -838,10 +838,12 @@ def create_doctest_figure(app, obj, name, lines): The idea is that each doctest that produces a figure should generate that and then that figure should be part of the docs. """ - import xdoctest import sys import types + import xdoctest + import xdoctest.core + if isinstance(obj, types.ModuleType): module = obj else: @@ -1035,9 +1037,10 @@ def postprocess_hyperlinks(app, doctree, docname): "autodoc-process-docstring" event. """ # Your hyperlink postprocessing logic here - from docutils import nodes import pathlib + from docutils import nodes + for node in doctree.traverse(nodes.reference): if 'refuri' in node.attributes: refuri = node.attributes['refuri'] @@ -1054,7 +1057,7 @@ def postprocess_hyperlinks(app, doctree, docname): def fix_rst_todo_section(lines): - new_lines = [] + # new_lines = [] for line in lines: ... ... @@ -1062,6 +1065,7 @@ def fix_rst_todo_section(lines): def setup(app): import sphinx + import sphinx.application app: sphinx.application.Sphinx = app app.add_domain(PatchedPythonDomain, override=True) diff --git a/line_profiler/_logger.py b/line_profiler/_logger.py index c632e925..bb4629a7 100644 --- a/line_profiler/_logger.py +++ b/line_profiler/_logger.py @@ -6,7 +6,7 @@ import logging from abc import ABC, abstractmethod import sys -from typing import ClassVar +from typing import ClassVar, cast from logging import INFO, DEBUG, ERROR, WARNING, CRITICAL # NOQA @@ -168,7 +168,7 @@ def configure( 'path': None, 'format': '%(asctime)s : [file] %(levelname)s : %(message)s', } - streaminfo = { + streaminfo: dict[str, bool | None | str] = { '__enable__': None, # will be determined below 'format': '%(levelname)s: %(message)s', } @@ -195,7 +195,7 @@ def configure( # Add a stream handler if enabled if streaminfo['__enable__']: - streamformat = streaminfo.get('format') + streamformat = cast(str, streaminfo.get('format')) sh = logging.StreamHandler(sys.stdout) sh.setFormatter(logging.Formatter(streamformat)) self.logger.addHandler(sh) diff --git a/line_profiler/autoprofile/autoprofile.py b/line_profiler/autoprofile/autoprofile.py index 1e20fbb7..e287d35c 100644 --- a/line_profiler/autoprofile/autoprofile.py +++ b/line_profiler/autoprofile/autoprofile.py @@ -153,4 +153,4 @@ def __exit__(self, *_, **__): # then restore it via the context manager, so that the executed # code is run as `__main__` sys.modules['__main__'] = module_obj - exec(code_obj, cast(Dict[str, Any], namespace), namespace) + exec(code_obj, cast(Dict[str, Any], namespace), namespace) # type: ignore[redundant-cast] diff --git a/line_profiler/cli_utils.py b/line_profiler/cli_utils.py index bc9b6fa1..470a06d3 100644 --- a/line_profiler/cli_utils.py +++ b/line_profiler/cli_utils.py @@ -12,11 +12,11 @@ import shutil import sys from os import PathLike -from typing import Protocol, Sequence, TypeVar, cast +from typing import cast from .toml_config import ConfigSource -_BOOLEAN_VALUES = { +_BOOLEAN_VALUES: dict[str, bool] = { **{k.casefold(): False for k in ('', '0', 'off', 'False', 'F', 'no', 'N')}, **{k.casefold(): True for k in ('1', 'on', 'True', 'T', 'yes', 'Y')}, } diff --git a/pyproject.toml b/pyproject.toml index 4ac613b8..ff66661d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ omit =[ ] [tool.cibuildwheel] -build = "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*" +build = "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*" # XXX: since `tests.yml` already defines `matrix.cibw_skip` for # `build_binpy_wheels`, can we deduplicate and just use that? # Or do we need these when building wheels for release, which may run on @@ -59,13 +59,13 @@ archs = ['AMD64', 'ARM64'] ignore_missing_imports = true [tool.xcookie] -tags = [ "pyutils", "binpy", "github",] +tags = [ "pyutils", "binpy", "github", "mypy", "binpy-ubuntu-arm"] mod_name = "line_profiler" repo_name = "line_profiler" rel_mod_parent_dpath = "." os = [ "all", "linux", "osx", "win",] main_python = '3.13' -min_python = '3.8' +min_python = '3.9' max_python = '3.14' author = "Robert Kern" author_email = "robert.kern@enthought.com" @@ -102,7 +102,7 @@ rules = { unused-type-ignore-comment = "ignore" } [tool.ruff] line-length = 80 -target-version = "py38" +target-version = "py39" [tool.ruff.lint] # Enable Flake8 (E, F) and isort (I) rules. @@ -119,3 +119,8 @@ indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" docstring-code-format = false + +[tool.ty.rules] +unused-ignore-comment = "ignore" +unused-type-ignore-comment = "ignore" +unresolved-import = "ignore" diff --git a/requirements/ipython.txt b/requirements/ipython.txt index c439de11..2c1b22c4 100644 --- a/requirements/ipython.txt +++ b/requirements/ipython.txt @@ -1,2 +1,2 @@ -IPython >=8.14.0 ; python_version < '4.0.0' and python_version >= '3.9.0' # Python 3.9+ -IPython >=8.12.2 ; python_version < '3.9.0' and python_version >= '3.8.0' # Python 3.8 +IPython >=8.14.0 ; python_version < '3.10' and python_version >= '3.9' +IPython >=8.28.0 ; python_version < '4.0' and python_version >= '3.10' diff --git a/requirements/optional.txt b/requirements/optional.txt index 24b6ba08..91637884 100644 --- a/requirements/optional.txt +++ b/requirements/optional.txt @@ -1,4 +1,4 @@ # Add requirements here, use the script for help # xdev availpkg rich -rich>=12.3.0 +rich>=13.9.0 -r ipython.txt diff --git a/requirements/tests.txt b/requirements/tests.txt index 74c2abc3..dae10d28 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,16 +1,5 @@ -pytest>=7.4.4 ; python_version < '4.0' and python_version >= '3.13' # Python 3.13+ -pytest>=7.4.4 ; python_version < '3.13' and python_version >= '3.12' # Python 3.12 -pytest>=7.4.4 ; python_version < '3.12' and python_version >= '3.11' # Python 3.11 -pytest>=7.4.4 ; python_version < '3.11' and python_version >= '3.10' # Python 3.10 -pytest>=7.4.4 ; python_version < '3.10' and python_version >= '3.9' # Python 3.9 -pytest>=7.4.4 ; python_version < '3.9' and python_version >= '3.8' # Python 3.8 - -pytest-cov>=3.0.0 - -coverage[toml]>=7.3.0 ; python_version < '4.0' and python_version >= '3.12' # Python 3.12 -coverage[toml]>=6.5.0 ; python_version < '3.12' and python_version >= '3.10' # Python 3.10-3.11 -coverage[toml]>=6.5.0 ; python_version < '3.10' and python_version >= '3.9' # Python 3.9 -coverage[toml]>=6.5.0 ; python_version < '3.9' and python_version >= '3.8' # Python 3.8 - +pytest>=7.4.4 +pytest-cov>=7.1.0 +coverage[toml]>=7.10.7 ubelt >= 1.3.4 xdoctest >= 1.1.3 From 17f2097b2c0f84b0f8f95a3cf3d55fed39f55cd5 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 2 Apr 2026 20:26:32 -0400 Subject: [PATCH 2/7] update xcookie --- .github/workflows/release.yml | 297 ++++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 231 ++------------------------ dev/setup_secrets.sh | 27 +++- pyproject.toml | 8 +- 4 files changed, 332 insertions(+), 231 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..67c3d191 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,297 @@ +# This workflow is autogenerated by xcookie. +# File kind: release +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions +# Based on ~/code/xcookie/xcookie/builders/github_actions.py +# See: https://github.com/Erotemic/xcookie + +name: BinPyRelease + +on: + push: + workflow_dispatch: + +jobs: + build_sdist: + ## + # Build the sdist artifact used by the release workflow. + # This workflow intentionally builds artifacts but does not run the + # full test matrix. + ## + name: Build sdist + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v6.0.2 + - name: Set up Python 3.13 + uses: actions/setup-python@v5.6.0 + with: + python-version: '3.13' + - name: Build sdist + shell: bash + run: |- + python -m pip install pip uv -U + python -m uv pip install setuptools>=0.8 wheel build twine + python -m build --sdist --outdir wheelhouse + python -m twine check ./wheelhouse/line_profiler*.tar.gz + - name: Show built files + shell: bash + run: ls -la wheelhouse + - uses: actions/upload-artifact@v6.0.0 + name: Upload sdist artifact + with: + name: sdist_wheels + path: ./wheelhouse/line_profiler*.tar.gz + build_binpy_wheels: + ## + # Build binary wheels used by the release workflow. + ## + name: ${{ matrix.os }}, arch=${{ matrix.arch }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Normally, xcookie generates explicit lists of platforms to build / test + # on, but in this case cibuildwheel does that for us, so we need to just + # set the environment variables for cibuildwheel. These are parsed out of + # the standard [tool.cibuildwheel] section in pyproject.toml and set + # explicitly here. + os: + - ubuntu-latest + - macOS-latest + - windows-latest + - ubuntu-24.04-arm + cibw_skip: + - '*-win32 cp3{10}-win_arm64 cp313-musllinux_i686' + arch: + - auto + steps: + - name: Checkout source + uses: actions/checkout@v6.0.2 + - name: Enable MSVC 64bit + uses: ilammy/msvc-dev-cmd@v1 + if: ${{ startsWith(matrix.os, 'windows-') }} + with: + arch: ${{ contains(matrix.os, 'arm') && 'arm64' || 'x64' }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3.7.0 + if: runner.os == 'Linux' && matrix.arch != 'auto' + with: + platforms: all + - name: Build binary wheels + uses: pypa/cibuildwheel@v3.3.1 + with: + output-dir: wheelhouse + config-file: pyproject.toml + env: + CIBW_SKIP: ${{ matrix.cibw_skip }} + CIBW_TEST_SKIP: '*-win_arm64' + CIBW_ARCHS_LINUX: ${{ matrix.arch }} + PYTHONUTF8: '1' + VSCMD_ARG_TGT_ARCH: '' + - name: Show built files + shell: bash + run: ls -la wheelhouse + - uses: actions/upload-artifact@v6.0.0 + name: Upload wheels artifact + with: + name: wheels-${{ matrix.os }}-${{ matrix.arch }} + path: ./wheelhouse/line_profiler*.whl + test_deploy: + name: Deploy Test + runs-on: ubuntu-latest + if: github.event_name == 'push' && ! startsWith(github.event.ref, 'refs/tags') && ! startsWith(github.event.ref, 'refs/heads/release') + needs: + - build_binpy_wheels + - build_sdist + steps: + - name: Checkout source + uses: actions/checkout@v6.0.2 + - uses: actions/download-artifact@v4.1.8 + name: Download wheels + with: + pattern: wheels-* + merge-multiple: true + path: wheelhouse + - uses: actions/download-artifact@v4.1.8 + name: Download sdist + with: + name: sdist_wheels + path: wheelhouse + - name: Show files to upload + shell: bash + run: ls -la wheelhouse + - name: Sign and Publish + env: + TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/ + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TEST_TWINE_PASSWORD }} + CI_SECRET: ${{ secrets.CI_SECRET }} + run: |- + GPG_EXECUTABLE=gpg + $GPG_EXECUTABLE --version + openssl version + $GPG_EXECUTABLE --list-keys + echo "Decrypting Keys" + openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_public_gpg_key.pgp.enc | $GPG_EXECUTABLE --import + openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/gpg_owner_trust.enc | $GPG_EXECUTABLE --import-ownertrust + openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_secret_gpg_subkeys.pgp.enc | $GPG_EXECUTABLE --import + echo "Finish Decrypt Keys" + $GPG_EXECUTABLE --list-keys || true + $GPG_EXECUTABLE --list-keys || echo "first invocation of gpg creates directories and returns 1" + $GPG_EXECUTABLE --list-keys + VERSION=$(python -c "import setup; print(setup.VERSION)") + python -m pip install pip uv -U + python -m pip install packaging twine -U + python -m pip install urllib3 requests[security] + GPG_KEYID=$(cat dev/public_gpg_key) + echo "GPG_KEYID = '$GPG_KEYID'" + GPG_SIGN_CMD="$GPG_EXECUTABLE --batch --yes --detach-sign --armor --local-user $GPG_KEYID" + WHEEL_PATHS=(wheelhouse/*.whl wheelhouse/*.tar.gz) + WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_PATHS[@]}") + echo "$WHEEL_PATHS_STR" + for WHEEL_PATH in "${WHEEL_PATHS[@]}" + do + echo "------" + echo "WHEEL_PATH = $WHEEL_PATH" + $GPG_SIGN_CMD --output $WHEEL_PATH.asc $WHEEL_PATH + $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH || echo "hack, the first run of gpg very fails" + $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH + done + ls -la wheelhouse + python -m pip install opentimestamps-client + ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc + ls -la wheelhouse + twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } + - uses: actions/upload-artifact@v6.0.0 + name: Upload deploy artifacts + with: + name: deploy_artifacts + path: |- + wheelhouse/*.whl + wheelhouse/*.zip + wheelhouse/*.tar.gz + wheelhouse/*.asc + wheelhouse/*.ots + live_deploy: + name: Deploy Live + runs-on: ubuntu-latest + if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags') || startsWith(github.event.ref, 'refs/heads/release')) + needs: + - build_binpy_wheels + - build_sdist + steps: + - name: Checkout source + uses: actions/checkout@v6.0.2 + - uses: actions/download-artifact@v4.1.8 + name: Download wheels + with: + pattern: wheels-* + merge-multiple: true + path: wheelhouse + - uses: actions/download-artifact@v4.1.8 + name: Download sdist + with: + name: sdist_wheels + path: wheelhouse + - name: Show files to upload + shell: bash + run: ls -la wheelhouse + - name: Sign and Publish + env: + TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/ + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + CI_SECRET: ${{ secrets.CI_SECRET }} + run: |- + GPG_EXECUTABLE=gpg + $GPG_EXECUTABLE --version + openssl version + $GPG_EXECUTABLE --list-keys + echo "Decrypting Keys" + openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_public_gpg_key.pgp.enc | $GPG_EXECUTABLE --import + openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/gpg_owner_trust.enc | $GPG_EXECUTABLE --import-ownertrust + openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_secret_gpg_subkeys.pgp.enc | $GPG_EXECUTABLE --import + echo "Finish Decrypt Keys" + $GPG_EXECUTABLE --list-keys || true + $GPG_EXECUTABLE --list-keys || echo "first invocation of gpg creates directories and returns 1" + $GPG_EXECUTABLE --list-keys + VERSION=$(python -c "import setup; print(setup.VERSION)") + python -m pip install pip uv -U + python -m pip install packaging twine -U + python -m pip install urllib3 requests[security] + GPG_KEYID=$(cat dev/public_gpg_key) + echo "GPG_KEYID = '$GPG_KEYID'" + GPG_SIGN_CMD="$GPG_EXECUTABLE --batch --yes --detach-sign --armor --local-user $GPG_KEYID" + WHEEL_PATHS=(wheelhouse/*.whl wheelhouse/*.tar.gz) + WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_PATHS[@]}") + echo "$WHEEL_PATHS_STR" + for WHEEL_PATH in "${WHEEL_PATHS[@]}" + do + echo "------" + echo "WHEEL_PATH = $WHEEL_PATH" + $GPG_SIGN_CMD --output $WHEEL_PATH.asc $WHEEL_PATH + $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH || echo "hack, the first run of gpg very fails" + $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH + done + ls -la wheelhouse + python -m pip install opentimestamps-client + ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc + ls -la wheelhouse + twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } + - uses: actions/upload-artifact@v6.0.0 + name: Upload deploy artifacts + with: + name: deploy_artifacts + path: |- + wheelhouse/*.whl + wheelhouse/*.zip + wheelhouse/*.tar.gz + wheelhouse/*.asc + wheelhouse/*.ots + release: + name: Create Github Release + if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags') || startsWith(github.event.ref, 'refs/heads/release')) + runs-on: ubuntu-latest + permissions: + contents: write + needs: + - live_deploy + steps: + - name: Checkout source + uses: actions/checkout@v6.0.2 + - uses: actions/download-artifact@v4.1.8 + name: Download artifacts + with: + name: deploy_artifacts + path: wheelhouse + - name: Show files to release + shell: bash + run: ls -la wheelhouse + - run: 'echo "Automatic Release Notes. TODO: improve" > ${{ github.workspace }}-CHANGELOG.txt' + - name: Tag Release Commit + if: (startsWith(github.event.ref, 'refs/heads/release')) + run: |- + export VERSION=$(python -c "import setup; print(setup.VERSION)") + git tag "v$VERSION" + git push origin "v$VERSION" + - uses: softprops/action-gh-release@v1 + name: Create Release + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + body_path: ${{ github.workspace }}-CHANGELOG.txt + tag_name: ${{ github.ref }} + name: Release ${{ github.ref }} + body: Automatic Release + generate_release_notes: true + draft: true + prerelease: false + files: |- + wheelhouse/*.whl + wheelhouse/*.asc + wheelhouse/*.ots + wheelhouse/*.zip + wheelhouse/*.tar.gz + + diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8c779fe0..9120f751 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,7 +1,7 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# This workflow is autogenerated by xcookie. +# File kind: tests # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -# Based on ~/code/xcookie/xcookie/rc/tests.yml.in -# Now based on ~/code/xcookie/xcookie/builders/github_actions.py +# Based on ~/code/xcookie/xcookie/builders/github_actions.py # See: https://github.com/Erotemic/xcookie name: BinPyCI @@ -132,7 +132,7 @@ jobs: - windows-latest - ubuntu-24.04-arm cibw_skip: - - '*-win32 cp3{9,10}-win_arm64 cp313-musllinux_i686' + - '*-win32 cp3{10}-win_arm64 cp313-musllinux_i686' arch: - auto steps: @@ -203,7 +203,7 @@ jobs: path: ./wheelhouse/line_profiler*.whl test_binpy_wheels: ## - # Download the previously build binary wheels from the + # Download the previously built binary wheels from the # build_binpy_wheels step, and test them in an independent # environment. ## @@ -218,19 +218,19 @@ jobs: # Xcookie generates an explicit list of environments that will be used # for testing instead of using the more concise matrix notation. include: - - python-version: '3.9' + - python-version: '3.10' install-extras: tests-strict,runtime-strict os: ubuntu-latest arch: auto - - python-version: '3.9' + - python-version: '3.10' install-extras: tests-strict,runtime-strict os: macOS-latest arch: auto - - python-version: '3.9' + - python-version: '3.10' install-extras: tests-strict,runtime-strict os: windows-latest arch: auto - - python-version: '3.9' + - python-version: '3.10' install-extras: tests-strict,runtime-strict os: ubuntu-24.04-arm arch: auto @@ -270,10 +270,6 @@ jobs: install-extras: tests os: windows-11-arm arch: auto - - python-version: '3.9' - install-extras: tests,optional - os: ubuntu-latest - arch: auto - python-version: '3.10' install-extras: tests,optional os: ubuntu-latest @@ -294,10 +290,6 @@ jobs: install-extras: tests,optional os: ubuntu-latest arch: auto - - python-version: '3.9' - install-extras: tests,optional - os: macOS-latest - arch: auto - python-version: '3.10' install-extras: tests,optional os: macOS-latest @@ -318,10 +310,6 @@ jobs: install-extras: tests,optional os: macOS-latest arch: auto - - python-version: '3.9' - install-extras: tests,optional - os: windows-latest - arch: auto - python-version: '3.10' install-extras: tests,optional os: windows-latest @@ -342,10 +330,6 @@ jobs: install-extras: tests,optional os: windows-latest arch: auto - - python-version: '3.9' - install-extras: tests,optional - os: ubuntu-24.04-arm - arch: auto - python-version: '3.10' install-extras: tests,optional os: ubuntu-24.04-arm @@ -499,202 +483,5 @@ jobs: with: file: ./coverage.xml token: ${{ secrets.CODECOV_TOKEN }} - test_deploy: - name: Deploy Test - runs-on: ubuntu-latest - if: github.event_name == 'push' && ! startsWith(github.event.ref, 'refs/tags') && ! startsWith(github.event.ref, 'refs/heads/release') - needs: - - build_and_test_sdist - - build_binpy_wheels - steps: - - name: Checkout source - uses: actions/checkout@v6.0.2 - - uses: actions/download-artifact@v4.1.8 - name: Download wheels - with: - pattern: wheels-* - merge-multiple: true - path: wheelhouse - - uses: actions/download-artifact@v4.1.8 - name: Download sdist - with: - name: sdist_wheels - path: wheelhouse - - name: Show files to upload - shell: bash - run: ls -la wheelhouse - - name: Sign and Publish - env: - TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/ - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TEST_TWINE_PASSWORD }} - CI_SECRET: ${{ secrets.CI_SECRET }} - run: |- - GPG_EXECUTABLE=gpg - $GPG_EXECUTABLE --version - openssl version - $GPG_EXECUTABLE --list-keys - echo "Decrypting Keys" - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_public_gpg_key.pgp.enc | $GPG_EXECUTABLE --import - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/gpg_owner_trust.enc | $GPG_EXECUTABLE --import-ownertrust - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_secret_gpg_subkeys.pgp.enc | $GPG_EXECUTABLE --import - echo "Finish Decrypt Keys" - $GPG_EXECUTABLE --list-keys || true - $GPG_EXECUTABLE --list-keys || echo "first invocation of gpg creates directories and returns 1" - $GPG_EXECUTABLE --list-keys - VERSION=$(python -c "import setup; print(setup.VERSION)") - python -m pip install pip uv -U - python -m pip install packaging twine -U - python -m pip install urllib3 requests[security] - GPG_KEYID=$(cat dev/public_gpg_key) - echo "GPG_KEYID = '$GPG_KEYID'" - GPG_SIGN_CMD="$GPG_EXECUTABLE --batch --yes --detach-sign --armor --local-user $GPG_KEYID" - WHEEL_PATHS=(wheelhouse/*.whl wheelhouse/*.tar.gz) - WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_PATHS[@]}") - echo "$WHEEL_PATHS_STR" - for WHEEL_PATH in "${WHEEL_PATHS[@]}" - do - echo "------" - echo "WHEEL_PATH = $WHEEL_PATH" - $GPG_SIGN_CMD --output $WHEEL_PATH.asc $WHEEL_PATH - $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH || echo "hack, the first run of gpg very fails" - $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH - done - ls -la wheelhouse - python -m pip install opentimestamps-client - ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc - ls -la wheelhouse - twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } - - uses: actions/upload-artifact@v6.0.0 - name: Upload deploy artifacts - with: - name: deploy_artifacts - path: |- - wheelhouse/*.whl - wheelhouse/*.zip - wheelhouse/*.tar.gz - wheelhouse/*.asc - wheelhouse/*.ots - live_deploy: - name: Deploy Live - runs-on: ubuntu-latest - if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags') || startsWith(github.event.ref, 'refs/heads/release')) - needs: - - build_and_test_sdist - - build_binpy_wheels - steps: - - name: Checkout source - uses: actions/checkout@v6.0.2 - - uses: actions/download-artifact@v4.1.8 - name: Download wheels - with: - pattern: wheels-* - merge-multiple: true - path: wheelhouse - - uses: actions/download-artifact@v4.1.8 - name: Download sdist - with: - name: sdist_wheels - path: wheelhouse - - name: Show files to upload - shell: bash - run: ls -la wheelhouse - - name: Sign and Publish - env: - TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/ - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} - CI_SECRET: ${{ secrets.CI_SECRET }} - run: |- - GPG_EXECUTABLE=gpg - $GPG_EXECUTABLE --version - openssl version - $GPG_EXECUTABLE --list-keys - echo "Decrypting Keys" - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_public_gpg_key.pgp.enc | $GPG_EXECUTABLE --import - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/gpg_owner_trust.enc | $GPG_EXECUTABLE --import-ownertrust - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_secret_gpg_subkeys.pgp.enc | $GPG_EXECUTABLE --import - echo "Finish Decrypt Keys" - $GPG_EXECUTABLE --list-keys || true - $GPG_EXECUTABLE --list-keys || echo "first invocation of gpg creates directories and returns 1" - $GPG_EXECUTABLE --list-keys - VERSION=$(python -c "import setup; print(setup.VERSION)") - python -m pip install pip uv -U - python -m pip install packaging twine -U - python -m pip install urllib3 requests[security] - GPG_KEYID=$(cat dev/public_gpg_key) - echo "GPG_KEYID = '$GPG_KEYID'" - GPG_SIGN_CMD="$GPG_EXECUTABLE --batch --yes --detach-sign --armor --local-user $GPG_KEYID" - WHEEL_PATHS=(wheelhouse/*.whl wheelhouse/*.tar.gz) - WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_PATHS[@]}") - echo "$WHEEL_PATHS_STR" - for WHEEL_PATH in "${WHEEL_PATHS[@]}" - do - echo "------" - echo "WHEEL_PATH = $WHEEL_PATH" - $GPG_SIGN_CMD --output $WHEEL_PATH.asc $WHEEL_PATH - $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH || echo "hack, the first run of gpg very fails" - $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH - done - ls -la wheelhouse - python -m pip install opentimestamps-client - ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc - ls -la wheelhouse - twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } - - uses: actions/upload-artifact@v6.0.0 - name: Upload deploy artifacts - with: - name: deploy_artifacts - path: |- - wheelhouse/*.whl - wheelhouse/*.zip - wheelhouse/*.tar.gz - wheelhouse/*.asc - wheelhouse/*.ots - release: - name: Create Github Release - if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags') || startsWith(github.event.ref, 'refs/heads/release')) - runs-on: ubuntu-latest - permissions: - contents: write - needs: - - live_deploy - steps: - - name: Checkout source - uses: actions/checkout@v6.0.2 - - uses: actions/download-artifact@v4.1.8 - name: Download artifacts - with: - name: deploy_artifacts - path: wheelhouse - - name: Show files to release - shell: bash - run: ls -la wheelhouse - - run: 'echo "Automatic Release Notes. TODO: improve" > ${{ github.workspace }}-CHANGELOG.txt' - - name: Tag Release Commit - if: (startsWith(github.event.ref, 'refs/heads/release')) - run: |- - export VERSION=$(python -c "import setup; print(setup.VERSION)") - git tag "v$VERSION" - git push origin "v$VERSION" - - uses: softprops/action-gh-release@v1 - name: Create Release - id: create_release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - body_path: ${{ github.workspace }}-CHANGELOG.txt - tag_name: ${{ github.ref }} - name: Release ${{ github.ref }} - body: Automatic Release - generate_release_notes: true - draft: true - prerelease: false - files: |- - wheelhouse/*.whl - wheelhouse/*.asc - wheelhouse/*.ots - wheelhouse/*.zip - wheelhouse/*.tar.gz diff --git a/dev/setup_secrets.sh b/dev/setup_secrets.sh index 0a8efc9e..81f12011 100644 --- a/dev/setup_secrets.sh +++ b/dev/setup_secrets.sh @@ -162,6 +162,22 @@ setup_package_environs_github_pyutils(){ #' | python -c "import sys; from textwrap import dedent; print(dedent(sys.stdin.read()).strip(chr(10)))" > dev/secrets_configuration.sh } +resolve_secret_value_from_varname_ptr(){ + local secret_varname_ptr="$1" + local secret_name="$2" + local secret_varname="${!secret_varname_ptr}" + if [[ "$secret_varname" == "" ]]; then + echo "Skipping $secret_name because $secret_varname_ptr is unset" >&2 + return 1 + fi + local secret_value="${!secret_varname}" + if [[ "$secret_value" == "" ]]; then + echo "Skipping $secret_name because $secret_varname is unset or empty" >&2 + return 1 + fi + printf '%s' "$secret_value" +} + upload_github_secrets(){ load_secrets unset GITHUB_TOKEN @@ -169,13 +185,14 @@ upload_github_secrets(){ if ! gh auth status ; then gh auth login fi + local secret_value source dev/secrets_configuration.sh - gh secret set "TWINE_USERNAME" -b"${!VARNAME_TWINE_USERNAME}" - gh secret set "TEST_TWINE_USERNAME" -b"${!VARNAME_TEST_TWINE_USERNAME}" + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TWINE_USERNAME TWINE_USERNAME) && gh secret set "TWINE_USERNAME" -b"$secret_value" + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TEST_TWINE_USERNAME TEST_TWINE_USERNAME) && gh secret set "TEST_TWINE_USERNAME" -b"$secret_value" toggle_setx_enter - gh secret set "CI_SECRET" -b"${!VARNAME_CI_SECRET}" - gh secret set "TWINE_PASSWORD" -b"${!VARNAME_TWINE_PASSWORD}" - gh secret set "TEST_TWINE_PASSWORD" -b"${!VARNAME_TEST_TWINE_PASSWORD}" + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_CI_SECRET CI_SECRET) && gh secret set "CI_SECRET" -b"$secret_value" + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TWINE_PASSWORD TWINE_PASSWORD) && gh secret set "TWINE_PASSWORD" -b"$secret_value" + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TEST_TWINE_PASSWORD TEST_TWINE_PASSWORD) && gh secret set "TEST_TWINE_PASSWORD" -b"$secret_value" toggle_setx_exit } diff --git a/pyproject.toml b/pyproject.toml index ff66661d..ca08ae6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,12 +34,12 @@ omit =[ ] [tool.cibuildwheel] -build = "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*" +build = "cp310-* cp311-* cp312-* cp313-* cp314-*" # XXX: since `tests.yml` already defines `matrix.cibw_skip` for # `build_binpy_wheels`, can we deduplicate and just use that? # Or do we need these when building wheels for release, which may run on # separate pipelines? -skip = ["*-win32", "cp3{9,10}-win_arm64", "cp313-musllinux_i686"] +skip = ["*-win32", "cp3{10}-win_arm64", "cp313-musllinux_i686"] build-frontend = "build" build-verbosity = 1 test-command = "python {project}/run_tests.py" @@ -65,7 +65,7 @@ repo_name = "line_profiler" rel_mod_parent_dpath = "." os = [ "all", "linux", "osx", "win",] main_python = '3.13' -min_python = '3.9' +min_python = '3.10' max_python = '3.14' author = "Robert Kern" author_email = "robert.kern@enthought.com" @@ -102,7 +102,7 @@ rules = { unused-type-ignore-comment = "ignore" } [tool.ruff] line-length = 80 -target-version = "py39" +target-version = "py310" [tool.ruff.lint] # Enable Flake8 (E, F) and isort (I) rules. From 714c414a92626ab3cb8fb74bdbdc2a83bd308077 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 2 Apr 2026 20:27:09 -0400 Subject: [PATCH 3/7] fix setup --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index 85eabaad..c5842e6a 100755 --- a/setup.py +++ b/setup.py @@ -334,8 +334,6 @@ def run_cythonize(force=False): 'Operating System :: OS Independent', 'Programming Language :: C', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', From 1468d8a34536cd99d97b33499caf23c3bb118d3d Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 14 Apr 2026 09:45:43 -0400 Subject: [PATCH 4/7] Switch to trusted publishing --- .github/workflows/release.yml | 179 ++++++++++++--- dev/setup_secrets.sh | 407 ++++++++++++++++++++++++++++++---- pyproject.toml | 2 + 3 files changed, 522 insertions(+), 66 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 67c3d191..0f66b17b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,7 +99,7 @@ jobs: test_deploy: name: Deploy Test runs-on: ubuntu-latest - if: github.event_name == 'push' && ! startsWith(github.event.ref, 'refs/tags') && ! startsWith(github.event.ref, 'refs/heads/release') + if: github.event_name == 'push' && github.event.ref == 'refs/heads/main' needs: - build_binpy_wheels - build_sdist @@ -120,31 +120,32 @@ jobs: - name: Show files to upload shell: bash run: ls -la wheelhouse - - name: Sign and Publish + - name: Sign distributions env: - TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/ - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TEST_TWINE_PASSWORD }} - CI_SECRET: ${{ secrets.CI_SECRET }} + GPG_SECRET_SIGNING_SUBKEY_B64: ${{ secrets.GPG_SECRET_SIGNING_SUBKEY_B64 }} + GPG_PUBLIC_KEY_B64: ${{ secrets.GPG_PUBLIC_KEY_B64 }} + GPG_OWNER_TRUST_B64: ${{ secrets.GPG_OWNER_TRUST_B64 }} run: |- GPG_EXECUTABLE=gpg $GPG_EXECUTABLE --version openssl version $GPG_EXECUTABLE --list-keys - echo "Decrypting Keys" - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_public_gpg_key.pgp.enc | $GPG_EXECUTABLE --import - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/gpg_owner_trust.enc | $GPG_EXECUTABLE --import-ownertrust - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_secret_gpg_subkeys.pgp.enc | $GPG_EXECUTABLE --import - echo "Finish Decrypt Keys" + echo "Importing GPG keys from CI secrets" + printf '%s' "$GPG_PUBLIC_KEY_B64" | base64 -d | $GPG_EXECUTABLE --import + printf '%s' "$GPG_OWNER_TRUST_B64" | base64 -d | $GPG_EXECUTABLE --import-ownertrust + printf '%s' "$GPG_SECRET_SIGNING_SUBKEY_B64" | base64 -d | $GPG_EXECUTABLE --import + echo "Finish importing GPG keys" $GPG_EXECUTABLE --list-keys || true - $GPG_EXECUTABLE --list-keys || echo "first invocation of gpg creates directories and returns 1" $GPG_EXECUTABLE --list-keys + GPG_KEYID=$(cat dev/public_gpg_key) + echo "GPG_KEYID = '$GPG_KEYID'" + IMPORTED_FPR=$($GPG_EXECUTABLE --list-keys --with-colons "$GPG_KEYID" | awk -F: '/^fpr/ { print $10; exit }') + if [[ "$IMPORTED_FPR" != "$GPG_KEYID" ]]; then echo "ERROR: imported GPG fingerprint $IMPORTED_FPR does not match pinned $GPG_KEYID"; exit 1; fi + echo "GPG fingerprint verified: $IMPORTED_FPR" VERSION=$(python -c "import setup; print(setup.VERSION)") python -m pip install pip uv -U python -m pip install packaging twine -U python -m pip install urllib3 requests[security] - GPG_KEYID=$(cat dev/public_gpg_key) - echo "GPG_KEYID = '$GPG_KEYID'" GPG_SIGN_CMD="$GPG_EXECUTABLE --batch --yes --detach-sign --armor --local-user $GPG_KEYID" WHEEL_PATHS=(wheelhouse/*.whl wheelhouse/*.tar.gz) WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_PATHS[@]}") @@ -161,7 +162,22 @@ jobs: python -m pip install opentimestamps-client ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc ls -la wheelhouse - twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } + - name: Prepare publish directory + shell: bash + run: |- + mkdir -p publish_wheelhouse + shopt -s nullglob + for FPATH in wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.zip + do + cp "$FPATH" publish_wheelhouse/ + done + ls -la publish_wheelhouse + - name: Publish test artifacts to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: publish_wheelhouse + skip-existing: true + repository-url: https://test.pypi.org/legacy/ - uses: actions/upload-artifact@v6.0.0 name: Upload deploy artifacts with: @@ -172,6 +188,10 @@ jobs: wheelhouse/*.tar.gz wheelhouse/*.asc wheelhouse/*.ots + permissions: + contents: read + id-token: write + environment: testpypi live_deploy: name: Deploy Live runs-on: ubuntu-latest @@ -196,31 +216,32 @@ jobs: - name: Show files to upload shell: bash run: ls -la wheelhouse - - name: Sign and Publish + - name: Sign distributions env: - TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/ - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} - CI_SECRET: ${{ secrets.CI_SECRET }} + GPG_SECRET_SIGNING_SUBKEY_B64: ${{ secrets.GPG_SECRET_SIGNING_SUBKEY_B64 }} + GPG_PUBLIC_KEY_B64: ${{ secrets.GPG_PUBLIC_KEY_B64 }} + GPG_OWNER_TRUST_B64: ${{ secrets.GPG_OWNER_TRUST_B64 }} run: |- GPG_EXECUTABLE=gpg $GPG_EXECUTABLE --version openssl version $GPG_EXECUTABLE --list-keys - echo "Decrypting Keys" - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_public_gpg_key.pgp.enc | $GPG_EXECUTABLE --import - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/gpg_owner_trust.enc | $GPG_EXECUTABLE --import-ownertrust - openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:CI_SECRET -d -a -in dev/ci_secret_gpg_subkeys.pgp.enc | $GPG_EXECUTABLE --import - echo "Finish Decrypt Keys" + echo "Importing GPG keys from CI secrets" + printf '%s' "$GPG_PUBLIC_KEY_B64" | base64 -d | $GPG_EXECUTABLE --import + printf '%s' "$GPG_OWNER_TRUST_B64" | base64 -d | $GPG_EXECUTABLE --import-ownertrust + printf '%s' "$GPG_SECRET_SIGNING_SUBKEY_B64" | base64 -d | $GPG_EXECUTABLE --import + echo "Finish importing GPG keys" $GPG_EXECUTABLE --list-keys || true - $GPG_EXECUTABLE --list-keys || echo "first invocation of gpg creates directories and returns 1" $GPG_EXECUTABLE --list-keys + GPG_KEYID=$(cat dev/public_gpg_key) + echo "GPG_KEYID = '$GPG_KEYID'" + IMPORTED_FPR=$($GPG_EXECUTABLE --list-keys --with-colons "$GPG_KEYID" | awk -F: '/^fpr/ { print $10; exit }') + if [[ "$IMPORTED_FPR" != "$GPG_KEYID" ]]; then echo "ERROR: imported GPG fingerprint $IMPORTED_FPR does not match pinned $GPG_KEYID"; exit 1; fi + echo "GPG fingerprint verified: $IMPORTED_FPR" VERSION=$(python -c "import setup; print(setup.VERSION)") python -m pip install pip uv -U python -m pip install packaging twine -U python -m pip install urllib3 requests[security] - GPG_KEYID=$(cat dev/public_gpg_key) - echo "GPG_KEYID = '$GPG_KEYID'" GPG_SIGN_CMD="$GPG_EXECUTABLE --batch --yes --detach-sign --armor --local-user $GPG_KEYID" WHEEL_PATHS=(wheelhouse/*.whl wheelhouse/*.tar.gz) WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_PATHS[@]}") @@ -237,7 +258,21 @@ jobs: python -m pip install opentimestamps-client ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc ls -la wheelhouse - twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } + - name: Prepare publish directory + shell: bash + run: |- + mkdir -p publish_wheelhouse + shopt -s nullglob + for FPATH in wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.zip + do + cp "$FPATH" publish_wheelhouse/ + done + ls -la publish_wheelhouse + - name: Publish live artifacts to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: publish_wheelhouse + skip-existing: true - uses: actions/upload-artifact@v6.0.0 name: Upload deploy artifacts with: @@ -248,6 +283,10 @@ jobs: wheelhouse/*.tar.gz wheelhouse/*.asc wheelhouse/*.ots + permissions: + contents: read + id-token: write + environment: pypi release: name: Create Github Release if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags') || startsWith(github.event.ref, 'refs/heads/release')) @@ -295,3 +334,85 @@ jobs: wheelhouse/*.tar.gz +### +# Trusted publishing setup checklist +# +# This release workflow file: +# .github/workflows/release.yml +# Workflow page: +# github.com/pyutils/line_profiler/actions/workflows/release.yml +# Workflow source: +# github.com/pyutils/line_profiler/blob/main/.github/workflows/release.yml +# GitHub environments: +# github.com/pyutils/line_profiler/settings/environments +# +# Official references: +# https://docs.pypi.org/trusted-publishers/ +# https://docs.pypi.org/trusted-publishers/using-a-publisher/ +# https://docs.pypi.org/trusted-publishers/security-model/ +# https://docs.github.com/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect +# https://docs.github.com/actions/deployment/targeting-different-environments/using-environments-for-deployment +# +# If trusted publishing is not configured yet: +# +# 1. In GitHub, create or review these protected environments: +# - testpypi +# - pypi +# URL: +# github.com/pyutils/line_profiler/settings/environments +# +# Some xcookie setups will expect a setup like: +# +# - testpypi: +# * environment name: testpypi +# * use for non-release pushes that publish to TestPyPI +# * usually no manual approval is needed +# * optionally restrict deployment branches if you only want +# TestPyPI publishes from selected branches +# +# - pypi: +# * environment name: pypi +# * use for real releases only +# * require manual approval / required reviewers +# * prevent self-review if your org supports it +# * restrict deployments to release branches / version tags +# +# - do not put TWINE_* secrets in these environments when using +# trusted publishing +# +# - if enable_gpg=true and ci_gpg_secret_transport=encrypted_repo: +# store CI_SECRET as an environment secret (not repo-wide) +# - if enable_gpg=true and ci_gpg_secret_transport=direct_ci: +# store GPG_SECRET_SIGNING_SUBKEY_B64, GPG_PUBLIC_KEY_B64, and +# GPG_OWNER_TRUST_B64 as environment secrets; no CI_SECRET needed +# +# 2. In PyPI, add a trusted publisher for this project: +# owner: pyutils +# repository: line_profiler +# workflow filename: release.yml +# environment: pypi +# Project publishing page: +# https://pypi.org/manage/project/line-profiler/settings/publishing/ +# Account publishing page: +# https://pypi.org/manage/account/publishing/ +# +# 3. In TestPyPI, add a trusted publisher for this project: +# owner: pyutils +# repository: line_profiler +# workflow filename: release.yml +# environment: testpypi +# Project publishing page: +# https://test.pypi.org/manage/project/line-profiler/settings/publishing/ +# Account publishing page: +# https://test.pypi.org/manage/account/publishing/ +# +# Notes: +# - Keep the workflow filename stable after registration. +# - The PyPI/TestPyPI project pages may not exist until the project +# exists there; use the account publishing pages for pending publishers. +# - Trusted publishing removes TWINE_* secrets. +# - When enable_gpg=true and ci_gpg_secret_transport="encrypted_repo": +# CI_SECRET is still required (environment-scoped to pypi/testpypi). +# - When enable_gpg=true and ci_gpg_secret_transport="direct_ci": +# GPG_SECRET_SIGNING_SUBKEY_B64, GPG_PUBLIC_KEY_B64, and GPG_OWNER_TRUST_B64 +# are required (environment-scoped to pypi/testpypi). No CI_SECRET. \ No newline at end of file diff --git a/dev/setup_secrets.sh b/dev/setup_secrets.sh index 81f12011..ee607ab4 100644 --- a/dev/setup_secrets.sh +++ b/dev/setup_secrets.sh @@ -139,6 +139,8 @@ setup_package_environs_github_erotemic(){ export VARNAME_TWINE_PASSWORD="EROTEMIC_PYPI_MASTER_TOKEN" export VARNAME_TEST_TWINE_PASSWORD="EROTEMIC_TEST_PYPI_MASTER_TOKEN" export VARNAME_TWINE_USERNAME="EROTEMIC_PYPI_MASTER_TOKEN_USERNAME" + export GITHUB_ENVIRONMENT_PYPI="pypi" + export GITHUB_ENVIRONMENT_TESTPYPI="testpypi" export VARNAME_TEST_TWINE_USERNAME="EROTEMIC_TEST_PYPI_MASTER_TOKEN_USERNAME" export GPG_IDENTIFIER="=Erotemic-CI " ' | python -c "import sys; from textwrap import dedent; print(dedent(sys.stdin.read()).strip(chr(10)))" > dev/secrets_configuration.sh @@ -151,6 +153,8 @@ setup_package_environs_github_pyutils(){ export VARNAME_TWINE_PASSWORD="PYUTILS_PYPI_MASTER_TOKEN" export VARNAME_TEST_TWINE_PASSWORD="PYUTILS_TEST_PYPI_MASTER_TOKEN" export VARNAME_TWINE_USERNAME="PYUTILS_PYPI_MASTER_TOKEN_USERNAME" + export GITHUB_ENVIRONMENT_PYPI="pypi" + export GITHUB_ENVIRONMENT_TESTPYPI="testpypi" export VARNAME_TEST_TWINE_USERNAME="PYUTILS_TEST_PYPI_MASTER_TOKEN_USERNAME" export GPG_IDENTIFIER="=PyUtils-CI " ' | python -c "import sys; from textwrap import dedent; print(dedent(sys.stdin.read()).strip(chr(10)))" > dev/secrets_configuration.sh @@ -178,7 +182,63 @@ resolve_secret_value_from_varname_ptr(){ printf '%s' "$secret_value" } +upload_one_github_secret(){ + local secret_name="$1" + local secret_value="$2" + local environment_name="${3:-}" + if [[ "$environment_name" == "" ]]; then + gh secret set "$secret_name" -b"$secret_value" + else + gh secret set "$secret_name" --env "$environment_name" -b"$secret_value" + fi +} + +github_repo_full_name(){ + local remote_url + remote_url="$(git remote get-url origin)" + if [[ "$remote_url" == git@github.com:* ]]; then + printf '%s' "${remote_url#git@github.com:}" | sed 's/\.git$//' + elif [[ "$remote_url" == https://github.com/* ]]; then + printf '%s' "${remote_url#https://github.com/}" | sed 's/\.git$//' + else + echo "Unable to determine GitHub repo from origin: $remote_url" >&2 + return 1 + fi +} + +ensure_github_environment(){ + local environment_name="$1" + local repo_full_name + repo_full_name="$(github_repo_full_name)" || return 1 + gh api --method PUT \ + -H "Accept: application/vnd.github+json" \ + "/repos/${repo_full_name}/environments/${environment_name}" >/dev/null +} + +setup_github_release_environments(){ + source dev/secrets_configuration.sh + local repo_full_name + local pypi_env + local testpypi_env + repo_full_name="$(github_repo_full_name)" || return 1 + pypi_env="${GITHUB_ENVIRONMENT_PYPI:-pypi}" + testpypi_env="${GITHUB_ENVIRONMENT_TESTPYPI:-testpypi}" + + ensure_github_environment "$testpypi_env" + ensure_github_environment "$pypi_env" + + echo "Ensured GitHub environments exist:" + echo " - $testpypi_env" + echo " - $pypi_env" + echo "Review environment protection rules manually as needed:" + echo " https://github.com/${repo_full_name}/settings/environments" + echo "Suggested policy:" + echo " - ${testpypi_env}: usually no approval required" + echo " - ${pypi_env}: require approval / reviewers and restrict to release refs" +} + upload_github_secrets(){ + local mode="${1:-legacy}" load_secrets unset GITHUB_TOKEN #printf "%s" "$GITHUB_TOKEN" | gh auth login --hostname Github.com --with-token @@ -186,14 +246,58 @@ upload_github_secrets(){ gh auth login fi local secret_value + local pypi_env + local testpypi_env source dev/secrets_configuration.sh - secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TWINE_USERNAME TWINE_USERNAME) && gh secret set "TWINE_USERNAME" -b"$secret_value" - secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TEST_TWINE_USERNAME TEST_TWINE_USERNAME) && gh secret set "TEST_TWINE_USERNAME" -b"$secret_value" - toggle_setx_enter - secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_CI_SECRET CI_SECRET) && gh secret set "CI_SECRET" -b"$secret_value" - secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TWINE_PASSWORD TWINE_PASSWORD) && gh secret set "TWINE_PASSWORD" -b"$secret_value" - secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TEST_TWINE_PASSWORD TEST_TWINE_PASSWORD) && gh secret set "TEST_TWINE_PASSWORD" -b"$secret_value" - toggle_setx_exit + + if [[ "$mode" == "trusted_publishing" ]]; then + pypi_env="${GITHUB_ENVIRONMENT_PYPI:-pypi}" + testpypi_env="${GITHUB_ENVIRONMENT_TESTPYPI:-testpypi}" + setup_github_release_environments + toggle_setx_enter + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_CI_SECRET CI_SECRET) || true + if [[ "$secret_value" != "" ]]; then + upload_one_github_secret "CI_SECRET" "$secret_value" "$pypi_env" + upload_one_github_secret "CI_SECRET" "$secret_value" "$testpypi_env" + fi + toggle_setx_exit + elif [[ "$mode" == "direct_gpg" ]]; then + # direct_ci GPG transport + non-trusted publishing. + # GPG material is already uploaded by upload_github_gpg_secrets. + # Upload Twine credentials environment-scoped (live password to pypi + # env, test password to testpypi env). CI_SECRET is not uploaded. + pypi_env="${GITHUB_ENVIRONMENT_PYPI:-pypi}" + testpypi_env="${GITHUB_ENVIRONMENT_TESTPYPI:-testpypi}" + setup_github_release_environments + toggle_setx_enter + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TWINE_USERNAME TWINE_USERNAME) || true + if [[ "$secret_value" != "" ]]; then + upload_one_github_secret "TWINE_USERNAME" "$secret_value" "$pypi_env" + upload_one_github_secret "TWINE_USERNAME" "$secret_value" "$testpypi_env" + fi + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TEST_TWINE_USERNAME TEST_TWINE_USERNAME) || true + if [[ "$secret_value" != "" ]]; then + upload_one_github_secret "TEST_TWINE_USERNAME" "$secret_value" "$testpypi_env" + fi + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TWINE_PASSWORD TWINE_PASSWORD) || true + if [[ "$secret_value" != "" ]]; then + upload_one_github_secret "TWINE_PASSWORD" "$secret_value" "$pypi_env" + fi + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TEST_TWINE_PASSWORD TEST_TWINE_PASSWORD) || true + if [[ "$secret_value" != "" ]]; then + upload_one_github_secret "TEST_TWINE_PASSWORD" "$secret_value" "$testpypi_env" + fi + toggle_setx_exit + else + # Legacy mode: all secrets repo-level, CI_SECRET included. + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TWINE_USERNAME TWINE_USERNAME) && upload_one_github_secret "TWINE_USERNAME" "$secret_value" + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TEST_TWINE_USERNAME TEST_TWINE_USERNAME) && upload_one_github_secret "TEST_TWINE_USERNAME" "$secret_value" + toggle_setx_enter + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_CI_SECRET CI_SECRET) && upload_one_github_secret "CI_SECRET" "$secret_value" + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TWINE_PASSWORD TWINE_PASSWORD) && upload_one_github_secret "TWINE_PASSWORD" "$secret_value" + secret_value=$(resolve_secret_value_from_varname_ptr VARNAME_TEST_TWINE_PASSWORD TEST_TWINE_PASSWORD) && upload_one_github_secret "TEST_TWINE_PASSWORD" "$secret_value" + toggle_setx_exit + fi } @@ -241,15 +345,15 @@ upload_gitlab_group_secrets(){ fi TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX) - curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups" > "$TMP_DIR/all_group_info" + curl --fail --show-error --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups" > "$TMP_DIR/all_group_info" GROUP_ID=$(< "$TMP_DIR/all_group_info" jq ". | map(select(.path==\"$GROUP_NAME\")) | .[0].id") echo "GROUP_ID = $GROUP_ID" - curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID" > "$TMP_DIR/group_info" + curl --fail --show-error --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID" > "$TMP_DIR/group_info" < "$TMP_DIR/group_info" jq # Get group-level secret variables - curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID/variables" > "$TMP_DIR/group_vars" + curl --fail --show-error --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID/variables" > "$TMP_DIR/group_vars" < "$TMP_DIR/group_vars" jq '.[] | .key' if [[ "$?" != "0" ]]; then @@ -277,20 +381,26 @@ upload_gitlab_group_secrets(){ echo "Remove variable does not exist, posting" toggle_setx_enter - curl --request POST --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID/variables" \ - --form "key=${SECRET_VARNAME}" \ - --form "value=${LOCAL_VALUE}" \ - --form "protected=true" \ - --form "masked=true" \ - --form "environment_scope=*" \ - --form "variable_type=env_var" + curl --fail --silent --show-error \ + --request POST --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID/variables" \ + --form "key=${SECRET_VARNAME}" \ + --form "value=${LOCAL_VALUE}" \ + --form "protected=true" \ + --form "masked=true" \ + --form "environment_scope=*" \ + --form "variable_type=env_var" toggle_setx_exit elif [[ "$REMOTE_VALUE" != "$LOCAL_VALUE" ]]; then echo "Remove variable does not agree, putting" # Update variable value toggle_setx_enter - curl --request PUT --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID/variables/$SECRET_VARNAME" \ - --form "value=${LOCAL_VALUE}" + curl --fail --silent --show-error \ + --request PUT --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID/variables/$SECRET_VARNAME" \ + --form "value=${LOCAL_VALUE}" \ + --form "protected=true" \ + --form "masked=true" \ + --form "environment_scope=*" \ + --form "variable_type=env_var" toggle_setx_exit else echo "Remote value agrees with local" @@ -322,13 +432,13 @@ upload_gitlab_repo_secrets(){ TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX) toggle_setx_enter - curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups" > "$TMP_DIR/all_group_info" + curl --fail --show-error --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups" > "$TMP_DIR/all_group_info" toggle_setx_exit GROUP_ID=$(< "$TMP_DIR/all_group_info" jq ". | map(select(.path==\"$GROUP_NAME\")) | .[0].id") echo "GROUP_ID = $GROUP_ID" toggle_setx_enter - curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID" > "$TMP_DIR/group_info" + curl --fail --show-error --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID" > "$TMP_DIR/group_info" toggle_setx_exit GROUP_ID=$(< "$TMP_DIR/all_group_info" jq ". | map(select(.path==\"$GROUP_NAME\")) | .[0].id") < "$TMP_DIR/group_info" jq @@ -338,16 +448,25 @@ upload_gitlab_repo_secrets(){ # Get group-level secret variables toggle_setx_enter - curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/projects/$PROJECT_ID/variables" > "$TMP_DIR/project_vars" + curl --fail --show-error --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/projects/$PROJECT_ID/variables" > "$TMP_DIR/project_vars" toggle_setx_exit < "$TMP_DIR/project_vars" jq '.[] | .key' if [[ "$?" != "0" ]]; then echo "Failed to access project level variables. Probably a permission issue" fi + local mode="${1:-legacy}" + LIVE_MODE=1 source dev/secrets_configuration.sh - SECRET_VARNAME_ARR=(VARNAME_CI_SECRET VARNAME_TWINE_PASSWORD VARNAME_TEST_TWINE_PASSWORD VARNAME_TWINE_USERNAME VARNAME_TEST_TWINE_USERNAME VARNAME_PUSH_TOKEN) + if [[ "$mode" == "direct_gpg" ]]; then + # In direct_ci transport mode the GPG key material is uploaded as + # project-level secrets by upload_gitlab_gpg_secrets; CI_SECRET is not + # needed. Only Twine and push-token secrets are uploaded here. + SECRET_VARNAME_ARR=(VARNAME_TWINE_PASSWORD VARNAME_TEST_TWINE_PASSWORD VARNAME_TWINE_USERNAME VARNAME_TEST_TWINE_USERNAME VARNAME_PUSH_TOKEN) + else + SECRET_VARNAME_ARR=(VARNAME_CI_SECRET VARNAME_TWINE_PASSWORD VARNAME_TEST_TWINE_PASSWORD VARNAME_TWINE_USERNAME VARNAME_TEST_TWINE_USERNAME VARNAME_PUSH_TOKEN) + fi for SECRET_VARNAME_PTR in "${SECRET_VARNAME_ARR[@]}"; do SECRET_VARNAME=${!SECRET_VARNAME_PTR} echo "" @@ -366,13 +485,16 @@ upload_gitlab_repo_secrets(){ # New variable echo "Remove variable does not exist, posting" if [[ "$LIVE_MODE" == "1" ]]; then - curl --request POST --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/projects/$PROJECT_ID/variables" \ - --form "key=${SECRET_VARNAME}" \ - --form "value=${LOCAL_VALUE}" \ - --form "protected=true" \ - --form "masked=true" \ - --form "environment_scope=*" \ - --form "variable_type=env_var" + curl --fail --silent --show-error \ + --request POST \ + --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" \ + "$HOST/api/v4/projects/$PROJECT_ID/variables" \ + --form "key=${SECRET_VARNAME}" \ + --form "value=${LOCAL_VALUE}" \ + --form "protected=true" \ + --form "masked=true" \ + --form "environment_scope=*" \ + --form "variable_type=env_var" else echo "dry run, not posting" fi @@ -380,8 +502,15 @@ upload_gitlab_repo_secrets(){ echo "Remove variable does not agree, putting" # Update variable value if [[ "$LIVE_MODE" == "1" ]]; then - curl --request PUT --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/projects/$PROJECT_ID/variables/$SECRET_VARNAME" \ - --form "value=${LOCAL_VALUE}" + curl --fail --silent --show-error \ + --request PUT \ + --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" \ + "$HOST/api/v4/projects/$PROJECT_ID/variables/$SECRET_VARNAME" \ + --form "value=${LOCAL_VALUE}" \ + --form "protected=true" \ + --form "masked=true" \ + --form "environment_scope=*" \ + --form "variable_type=env_var" else echo "dry run, not putting" fi @@ -410,7 +539,10 @@ export_encrypted_code_signing_keys(){ # HOW TO ENCRYPT YOUR SECRET GPG KEY # You need to have a known public gpg key for this to make any sense - MAIN_GPG_KEYID=$(gpg --list-keys --keyid-format LONG "$GPG_IDENTIFIER" | head -n 2 | tail -n 1 | awk '{print $1}') + # Full primary-key fingerprint (40 hex chars) — more collision-resistant + # than the 16-char LONG key ID. Uses machine-parseable colon format so + # the extraction is stable across gpg output layout changes. + MAIN_GPG_FPR=$(gpg --list-keys --with-colons "$GPG_IDENTIFIER" | awk -F: '/^fpr/ { print $10; exit }') GPG_SIGN_SUBKEY=$(gpg --list-keys --with-subkey-fingerprints "$GPG_IDENTIFIER" | grep "\[S\]" -A 1 | tail -n 1 | awk '{print $1}') # Careful, if you don't have a subkey, requesting it will export more than you want. # Export the main key instead (its better to have subkeys, but this is a lesser evil) @@ -421,7 +553,7 @@ export_encrypted_code_signing_keys(){ # anyway. GPG_SIGN_SUBKEY=$(gpg --list-keys --with-subkey-fingerprints "$GPG_IDENTIFIER" | grep "\[C\]" -A 1 | tail -n 1 | awk '{print $1}') fi - echo "MAIN_GPG_KEYID = $MAIN_GPG_KEYID" + echo "MAIN_GPG_FPR = $MAIN_GPG_FPR" echo "GPG_SIGN_SUBKEY = $GPG_SIGN_SUBKEY" # Only export the signing secret subkey @@ -435,9 +567,10 @@ export_encrypted_code_signing_keys(){ GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -e -a -in dev/ci_public_gpg_key.pgp > dev/ci_public_gpg_key.pgp.enc GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -e -a -in dev/ci_secret_gpg_subkeys.pgp > dev/ci_secret_gpg_subkeys.pgp.enc GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -e -a -in dev/gpg_owner_trust > dev/gpg_owner_trust.enc - echo "$MAIN_GPG_KEYID" > dev/public_gpg_key + # Store the full fingerprint as the public signer anchor + printf '%s\n' "$MAIN_GPG_FPR" > dev/public_gpg_key - # Test decrpyt + # Test decrypt GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/ci_public_gpg_key.pgp.enc | gpg --list-packets --verbose GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/ci_secret_gpg_subkeys.pgp.enc | gpg --list-packets --verbose GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/gpg_owner_trust.enc @@ -451,7 +584,6 @@ export_encrypted_code_signing_keys(){ rm dev/gpg_owner_trust git status git add dev/*.enc - git add dev/gpg_owner_trust git add dev/public_gpg_key } @@ -461,6 +593,207 @@ export_encrypted_code_signing_keys(){ #} +_gpg_locate_signing_subkey(){ + __doc__=" + Internal helper. Sets MAIN_GPG_FPR and GPG_SIGN_SUBKEY in the caller's + scope. Exits non-zero and prints a diagnostic if either cannot be found. + Requires GPG_IDENTIFIER to already be set. + " + MAIN_GPG_FPR=$(gpg --list-keys --with-colons "$GPG_IDENTIFIER" \ + | awk -F: '/^fpr/ { print $10; exit }') + GPG_SIGN_SUBKEY=$(gpg --list-keys --with-subkey-fingerprints "$GPG_IDENTIFIER" \ + | grep "\[S\]" -A 1 | tail -n 1 | awk '{print $1}') + if [[ "$GPG_SIGN_SUBKEY" == "" ]]; then + echo "WARNING: no [S] subkey found for $GPG_IDENTIFIER, falling back to [C] key" >&2 + GPG_SIGN_SUBKEY=$(gpg --list-keys --with-subkey-fingerprints "$GPG_IDENTIFIER" \ + | grep "\[C\]" -A 1 | tail -n 1 | awk '{print $1}') + fi + if [[ -z "$MAIN_GPG_FPR" ]]; then + echo "ERROR: could not determine primary key fingerprint for $GPG_IDENTIFIER" >&2 + return 1 + fi + if [[ -z "$GPG_SIGN_SUBKEY" ]]; then + echo "ERROR: could not find a signing subkey for $GPG_IDENTIFIER" >&2 + return 1 + fi + echo "MAIN_GPG_FPR = $MAIN_GPG_FPR" + echo "GPG_SIGN_SUBKEY = $GPG_SIGN_SUBKEY" +} + + +upload_github_gpg_secrets(){ + __doc__=" + Export GPG signing subkey material and upload it directly to GitHub + Actions as environment-scoped secrets (pypi + testpypi environments). + Also writes dev/public_gpg_key with the full primary key fingerprint + and stages it for commit. + + No .enc files are written to disk or committed to git. + This implements ci_gpg_secret_transport = 'direct_ci' for GitHub. + Call this instead of export_encrypted_code_signing_keys. + " + load_secrets + source dev/secrets_configuration.sh + + local pypi_env="${GITHUB_ENVIRONMENT_PYPI:-pypi}" + local testpypi_env="${GITHUB_ENVIRONMENT_TESTPYPI:-testpypi}" + + _gpg_locate_signing_subkey || return 1 + + local TMP_DIR + TMP_DIR=$(mktemp -d -t gpg-ci-XXXXXXXXXX) + # shellcheck disable=SC2064 + trap "rm -rf '$TMP_DIR'" RETURN + + # Export signing subkey secret material and associated public key + gpg --armor --export-options export-backup \ + --export-secret-subkeys "${GPG_SIGN_SUBKEY}!" > "$TMP_DIR/signing_subkey.pgp" + gpg --armor --export "${GPG_SIGN_SUBKEY}" > "$TMP_DIR/public_key.pgp" + gpg --export-ownertrust > "$TMP_DIR/owner_trust" + + # Single-line base64 for robust secret transport (tr -d '\n' is + # portable across GNU and macOS; avoids -w 0 / -b 0 divergence). + local GPG_SECRET_SIGNING_SUBKEY_B64 GPG_PUBLIC_KEY_B64 GPG_OWNER_TRUST_B64 + GPG_SECRET_SIGNING_SUBKEY_B64=$(base64 < "$TMP_DIR/signing_subkey.pgp" | tr -d '\n') + GPG_PUBLIC_KEY_B64=$(base64 < "$TMP_DIR/public_key.pgp" | tr -d '\n') + GPG_OWNER_TRUST_B64=$(base64 < "$TMP_DIR/owner_trust" | tr -d '\n') + + if [[ -z "$GPG_SECRET_SIGNING_SUBKEY_B64" ]]; then + echo "ERROR: signing subkey export is empty — aborting" >&2 + return 1 + fi + + # Write the public fingerprint anchor to the repo. + # This file is the only GPG artifact committed in direct_ci mode. + mkdir -p dev + printf '%s\n' "$MAIN_GPG_FPR" > dev/public_gpg_key + git add dev/public_gpg_key + git status + + unload_secrets + + # Ensure deployment environments exist before scoping secrets to them + setup_github_release_environments + + if ! gh auth status; then gh auth login; fi + + toggle_setx_enter + for env_name in "$pypi_env" "$testpypi_env"; do + upload_one_github_secret "GPG_SECRET_SIGNING_SUBKEY_B64" \ + "$GPG_SECRET_SIGNING_SUBKEY_B64" "$env_name" + upload_one_github_secret "GPG_PUBLIC_KEY_B64" \ + "$GPG_PUBLIC_KEY_B64" "$env_name" + upload_one_github_secret "GPG_OWNER_TRUST_B64" \ + "$GPG_OWNER_TRUST_B64" "$env_name" + done + toggle_setx_exit +} + + +upload_gitlab_gpg_secrets(){ + __doc__=" + Export GPG signing subkey material and upload it directly to GitLab + CI/CD project variables (protected=true, masked=true). + Also writes dev/public_gpg_key with the full primary key fingerprint + and stages it for commit. + + No .enc files are written to disk or committed to git. + This implements ci_gpg_secret_transport = 'direct_ci' for GitLab. + Call this instead of export_encrypted_code_signing_keys. + " + load_secrets + source dev/secrets_configuration.sh + + _gpg_locate_signing_subkey || return 1 + + local TMP_DIR + TMP_DIR=$(mktemp -d -t gpg-ci-XXXXXXXXXX) + # shellcheck disable=SC2064 + trap "rm -rf '$TMP_DIR'" RETURN + + gpg --armor --export-options export-backup \ + --export-secret-subkeys "${GPG_SIGN_SUBKEY}!" > "$TMP_DIR/signing_subkey.pgp" + gpg --armor --export "${GPG_SIGN_SUBKEY}" > "$TMP_DIR/public_key.pgp" + gpg --export-ownertrust > "$TMP_DIR/owner_trust" + + local GPG_SECRET_SIGNING_SUBKEY_B64 GPG_PUBLIC_KEY_B64 GPG_OWNER_TRUST_B64 + GPG_SECRET_SIGNING_SUBKEY_B64=$(base64 < "$TMP_DIR/signing_subkey.pgp" | tr -d '\n') + GPG_PUBLIC_KEY_B64=$(base64 < "$TMP_DIR/public_key.pgp" | tr -d '\n') + GPG_OWNER_TRUST_B64=$(base64 < "$TMP_DIR/owner_trust" | tr -d '\n') + + if [[ -z "$GPG_SECRET_SIGNING_SUBKEY_B64" ]]; then + echo "ERROR: signing subkey export is empty — aborting" >&2 + return 1 + fi + + # Write the public fingerprint anchor to the repo. + mkdir -p dev + printf '%s\n' "$MAIN_GPG_FPR" > dev/public_gpg_key + git add dev/public_gpg_key + git status + + # Locate the GitLab project via git remote + local REMOTE=origin + local HOST + HOST=https://$(git remote get-url $REMOTE \ + | cut -d "/" -f 1 | cut -d "@" -f 2 | cut -d ":" -f 1) + local PRIVATE_GITLAB_TOKEN + PRIVATE_GITLAB_TOKEN=$(git_token_for "$HOST") + if [[ "$PRIVATE_GITLAB_TOKEN" == "ERROR" ]]; then + echo "ERROR: failed to load GitLab authentication token" >&2 + return 1 + fi + + local PROJECT_PATH + PROJECT_PATH=$(git remote get-url $REMOTE | cut -d ":" -f 2 | sed 's/\.git$//') + local PROJECT_ID + PROJECT_ID=$(curl --fail --show-error --silent --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" \ + "$HOST/api/v4/projects?search=$(basename "$PROJECT_PATH")" \ + | jq -r ".[] | select(.path_with_namespace==\"$PROJECT_PATH\") | .id") + if [[ -z "$PROJECT_ID" ]]; then + echo "ERROR: could not determine GitLab project ID for $PROJECT_PATH" >&2 + return 1 + fi + echo "PROJECT_ID = $PROJECT_ID" + + _gitlab_upsert_protected_var(){ + local key="$1" value="$2" + local existing + existing=$(curl -s --show-error --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" \ + "$HOST/api/v4/projects/$PROJECT_ID/variables/$key" \ + | jq -r '.key // empty') + if [[ -z "$existing" ]]; then + curl --fail --silent --show-error --request POST \ + --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" \ + "$HOST/api/v4/projects/$PROJECT_ID/variables" \ + --form "key=$key" \ + --form "value=$value" \ + --form "protected=true" \ + --form "masked=true" \ + --form "environment_scope=*" \ + --form "variable_type=env_var" + else + curl --fail --silent --show-error --request PUT \ + --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" \ + "$HOST/api/v4/projects/$PROJECT_ID/variables/$key" \ + --form "value=$value" \ + --form "protected=true" \ + --form "masked=true" \ + --form "environment_scope=*" \ + --form "variable_type=env_var" + fi + } + + unload_secrets + + toggle_setx_enter + _gitlab_upsert_protected_var "GPG_SECRET_SIGNING_SUBKEY_B64" "$GPG_SECRET_SIGNING_SUBKEY_B64" + _gitlab_upsert_protected_var "GPG_PUBLIC_KEY_B64" "$GPG_PUBLIC_KEY_B64" + _gitlab_upsert_protected_var "GPG_OWNER_TRUST_B64" "$GPG_OWNER_TRUST_B64" + toggle_setx_exit +} + + _test_gnu(){ # shellcheck disable=SC2155 export GNUPGHOME=$(mktemp -d -t) diff --git a/pyproject.toml b/pyproject.toml index ca08ae6f..9779fdd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,8 @@ description = "Line-by-line profiler" url = "https://github.com/pyutils/line_profiler" license = "BSD" dev_status = "stable" +ci_pypi_trusted_publishing = true +ci_gpg_secret_transport = "direct_ci" typed = true skip_autogen = ["MANIFEST.in", "CHANGELOG.md"] test_env = """ From 406f5e031b5f2b7f110df54e3b42a0a1c327d000 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 14 Apr 2026 09:45:55 -0400 Subject: [PATCH 5/7] Remove encrypted in-repo secrets --- dev/ci_public_gpg_key.pgp.enc | 31 ------------------------------- dev/ci_secret_gpg_subkeys.pgp.enc | 24 ------------------------ dev/gpg_owner_trust.enc | 10 ---------- 3 files changed, 65 deletions(-) delete mode 100644 dev/ci_public_gpg_key.pgp.enc delete mode 100644 dev/ci_secret_gpg_subkeys.pgp.enc delete mode 100644 dev/gpg_owner_trust.enc diff --git a/dev/ci_public_gpg_key.pgp.enc b/dev/ci_public_gpg_key.pgp.enc deleted file mode 100644 index 2594960b..00000000 --- a/dev/ci_public_gpg_key.pgp.enc +++ /dev/null @@ -1,31 +0,0 @@ -U2FsdGVkX1+ebGmmQlyTKLiJ/h2mo6g4YxkpsiyuZb3gl9bkzAtQm6Vt0FTpDMq9 -q1W5jJuNPMqbBnhTnx/SURJ+vLEpjM1NcDRJcXph+USB8iLRtF1xmoiqDEP6Mlxf -Qw2BuSodlZbE6sUABrJS6NfMv/Aa/rvPb9tv9D/BcYcQOCY+eWqVbMgN+1M6DQ4w -AabbahAQ236RlaVNQJSPzp4hf/amXajTfFFjeGcbHYQ/lvOa33a7ob2a7BB9NOEL -HX4U6qrFksXiy6n4j5PAnoKmgAXdt+aUim/rSm1Rb3dKEF1H7wPX+fXjtOvN14E9 -uNmQfZSnFHPSPSm4mgYerU7FQ0dFc4k41r2PVgtv0O564a8ZCp/+x9kdHVM3iEUU -qN7njZ40b5dqPX2Yw7+XkD/IHlJcktH2yQ2rOdYikhAeH2sHItRVaNkJkc/aVZEo -j+6QzLLYnlMxZkrH9zhiKjiGGIAanEEPTSWoiN5pkWJ2tTOBppqePeK8h/3LutW+ -0TWXUgc8Aw4cJVJTjrqoLWf+C6ftj0EkS7vwV1IxU6SdpCm07IQHiiOQHk7pG/cF -yqNaQvGB7r8g2fhbOFj+3CJ1p+DV+b1w+vygP2OgErmR6ClJxPxOB3NPqJq0gWMe -iHJWd9NDV3PbQJU483g8zMwcs8SHj7GnOjQn7R5ETKMn71ebSyLMCX6bJ+vrESl4 -89RfUwcP+J/ShjT91QR6bysVRnQkSJON1PzatlSRWf9jQkzN2TXXWA7ERWFuZ3ru -Pah7vXfGLZY4379RIRclaRQGWjt2tsj4OiFTttDS9Fzy70GeRIiyFHf4o3rpVjNc -W9irGMvGWGD3N+1+9tHHjcwj6JQd1hNhkSIBXncvZR4EdR8+xPHqXe6FWaCZ6sJx -UExH3xcXVmXtz+Q3dFy6Zao69t/1my75QzEtnNxkZihu6Rr9s+28ZlljJCgCNcA1 -cqP6mhgXBPYKZiMBsUVEffJst+Zm0BRtpvZR3iq4ihCKRoEm4MeuUC1ZbVqzgPy9 -y9vbVchpscH+oUgnRdJoJjphmp/qT3ubQjz4JGnHyyyCT4Iumthl9wzI7yNWQe50 -AFQSMShbdRECqNiQbxzqO00qb5Tii5MiGLxlQrgQ3ixHo1ETbS06HdxKPdzRzfSC -3rjr9YIpopGC3wb+5TOlikzH6i1b+evUMkPRjeK52vi9ObEJVG8rbC9azjWF59kw -QFqvMWYFQCSTx5X3r1yErWivQRGLHWhtVFKukN49re754eCqMxwE6fy7NcJPh9nN -6XD67hhh52Ji6Du2T32fosk3C2KLYvzjr6p7O4SVIll/YWNpLKz0JDVJB4UMK28K -tPkUi3y+hAOlzzi+fiLvC90VNNScUTY4bkenzoK/pJdkjCpRuIbbIA4vY4Aj6O+1 -5yGsTr8LRrPyeeP3Z8WA86rE2p6xXWu56BvOZ04ez16wOdIDs+r/4YGTqu6sf74F -3CM2M4r1bJLAOLsuj0N41jaZa9WKEr3HJxEHA5jQQ+ZzWACrylH7tQP2tmJx8IgV -601frm3i+mTdbk3BN5GmDZLSVjzDOc+2jv05sRGBHhhTXWNSp8IxnTbml0nM99ED -farmCTtNCq+T8K/e5yMlI141eol2jjTOFeTpsUXyshSyTNwLrwU4Z++SYVPyUJMw -Uh/2Go1HIb85+iCd6Hbg78ukfrfsKkawheYk5krdEQKxt2UjtYZBqCDuRjMVZC4S -s2nC6nlOzeE8JHL8n8OOgUjK2DYJ84El82q+NP9xqN2x79rWqa7lHTYxu4PjLuCF -J/gTziqvjoY0Z5faLcRiNuxnIGBvVBBelDjxQCysdwExc1olOVV6HJKEN0S8Qu6F -CnilZ3xFKlwbY7qU91kiu4OOHy91TbbxeWhcr76dchLzMSUM5BsYFIqJec/cfCCr -EAJ9DnAOZmHpRC+V3aI2N8e/NYhzYKIBd6WerNouRP6GFB9MkPOK5See3PHPwjE8 diff --git a/dev/ci_secret_gpg_subkeys.pgp.enc b/dev/ci_secret_gpg_subkeys.pgp.enc deleted file mode 100644 index 32d710e3..00000000 --- a/dev/ci_secret_gpg_subkeys.pgp.enc +++ /dev/null @@ -1,24 +0,0 @@ -U2FsdGVkX19vJJuNAszO2wqjp6aw7vA5hyW+1B/99ifWdqX3nGQylaC+GF9gjUfb -Lb69GbU8czGELPIpUpxs8M2qHzw8Ze/9oaA2q0CpDlJoD+fPACuO+u11GxHlY9p6 -4lugdu+BVTE5pG0u6K0zW2X+elN7XXwdXI8A6NRkxgZnU8q4L3i/DJtx+lRDjSeS -cKsA2xu5NMYu5avG5XQAUzZ2OL2Q8rI+GdsSMaYup6AzejOswfOkwBtu+aZ4KXHG -gMrq/nK8+drSagb7AoCM1uDUm5nrdvdabNtIa/tVGMbcwEiKpQ9tCJ0BZF6Flh/P -Wa80LZNOR4qv1Fm+12eoezzRn3s1bXTqML4n1kj2W2BCCPWQvpPmnPKsfo/zMzZ/ -OjJq8MEMb5KJkUipDEnho/Q0A/rt+mXPdiVt5Y0sG57kVIp2uiiK7Nbnwro/ME/l -+YF3tdAWktjpn/5JsRjTsbgADDMH6pU+Yu0CPiKvUiNxyzwV+yn5LOG2VnuXywW/ -VXe+zlBsGPq58+jt2/jaosDNRhI0pn+lqzWD2xgwxjUR1X1J5fsxAbc4qWSIfNkc -55wvgbX3IhziGv2bF02Pueu0UrGL9X2d8vfM8C3W7uId09tBEJE7hKucRcQzizYk -5X1wi8x9wnMlix+lfSIcNjdFnHiizFYlwnpaXPsjCvepQOwvXfq6/JaNXQpaxBu3 -rU8snQGPy44tbnKCH6/mM99l9Lo8y/q3VLk245K4KzefkWo+w8F1XxGmlQlsIXyV -I4H39I7W42nBcYC0KbDEFjfSqaY/zVNYZicNU+M+N+1gQ6uYgPM9Nq66rwkj+jl5 -CHtF1WNQuX3o/elhP220GSfDVzXIJkqvWhb3X3HL/ro7w8O9ph93KnpH6IB3gKOU -6d1zjf3Srq7K57vuOSUHhe3CKO2HfuESym4wZgXbq+WMKfU+P0wz8PnWZxxZSG8W -0sX29sqwVBVRz+UwjoQDG9M+MrzsrFZ25e5bFikUPM/NGmxIe5kddEMOjCovUiXz -vuKIuWeOgAmW4vsxVLCOvqGclGwsA8Mez5ArKVVJBGi5xDGs683O3AddBuurqDT2 -BazQEx8U7uq60iRzQmNG9koC/7NSp2XBN1LtYAqvf0dO7Ce0545XK330Eel9oqkq -/Dfpx5Fkra49vnq+pif70BeyAbo7Y/i6/71t/Zp/kdhP5RuXmpavDttJBGS3sL/b -KROtz2x7Fi2mrUqIcgJalNbPndipytrguza8CTM9hfWMRD3e//JE7z5/8JO4sP4A -3EiBjo1u23NhUHdB9qiumopxhV1RGQthrEcdZiDQclCQ+Kg/vfhg78Qv+WsvDFDR -UxO90zC+rbr2MEF7ZELUxFv/FVnfBiRFMI6ysKBW3Naz4FXAk4NwSHUTXs+A2n9t -GmXRKiuCEwBrs1bQdUF/yDG7307L0lyLaUoehioAlN8QQ1Cd1CvKu/6DiX3elRpJ -Wzb5HcxmA1tbVrmQlr4sDg== diff --git a/dev/gpg_owner_trust.enc b/dev/gpg_owner_trust.enc deleted file mode 100644 index bad8f8c6..00000000 --- a/dev/gpg_owner_trust.enc +++ /dev/null @@ -1,10 +0,0 @@ -U2FsdGVkX1+RdDxqxenjGmXfGgLHEHL7QtsQnk9NrWtBrKBGE6CX+VR/MaM3hEoq -q40o56u7Z4AlyAF1Ze/EZIypeR6uCMH0UXyzKEjj9w87GeWyyi2gkl+CT/tZ1j8p -fCsJ8t3ckxHE1G0qmbCuL0KL5iDPVmCZxaRWa81NYw2kTl92H3fLm5dLkx06Qzqc -/V7p5JNUu5NFrfcGwcJhgg25cot+bdf2yO5i8pXjGKyTTesqRM6cZHUO/sEYpv+2 -4G56pIPz3AJYAWYq/yzGsc9FLsbwWEo3JPD0cOH+RlX12sX4nEuHEI5KT2gtXiVS -5KFwW+/5d/8kkXvk0TovnN2lFVDn2bpOFrvqAI1dRutU5Q0WAYbiudsvUOLwMZBV -N5B7Tcx6J7dijoEBLxhIrT54AyI8nwvXvYhjZ25C8KwBDrMkuSf3fMMMVUNgEwfw -Ry3gnKHaHDq2XZtMeZFojj66A/yl6rPxyrZLjub17Kfvpnjvpey3qfNCIQwOTFKu -kAFj+V5/a9FGEVTvSOTxxdzWO0l12DqAWw/XvC2yFJfY05L5PQFkWsmJHhkbKLa+ -CwV3G3qWYoiOe8u9Xs+0mA== From 76b87c172623b1c1a5ea3e175ec8dc64f17764b4 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 14 Apr 2026 12:54:33 -0400 Subject: [PATCH 6/7] reskip qemu, cleanup skip statements --- .github/workflows/release.yml | 7 +------ .github/workflows/tests.yml | 12 +----------- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f66b17b..a2bb6068 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: - windows-latest - ubuntu-24.04-arm cibw_skip: - - '*-win32 cp3{10}-win_arm64 cp313-musllinux_i686' + - '*-win32 cp310-win_arm64 cp313-musllinux_i686' arch: - auto steps: @@ -72,11 +72,6 @@ jobs: if: ${{ startsWith(matrix.os, 'windows-') }} with: arch: ${{ contains(matrix.os, 'arm') && 'arm64' || 'x64' }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3.7.0 - if: runner.os == 'Linux' && matrix.arch != 'auto' - with: - platforms: all - name: Build binary wheels uses: pypa/cibuildwheel@v3.3.1 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9120f751..f318f5ba 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -132,7 +132,7 @@ jobs: - windows-latest - ubuntu-24.04-arm cibw_skip: - - '*-win32 cp3{10}-win_arm64 cp313-musllinux_i686' + - '*-win32 cp310-win_arm64 cp313-musllinux_i686' arch: - auto steps: @@ -143,11 +143,6 @@ jobs: if: ${{ startsWith(matrix.os, 'windows-') }} && ${{ contains(matrix.cibw_skip, '*-win32') }} with: arch: ${{ contains(matrix.os, 'arm') && 'arm64' || 'x64' }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3.7.0 - if: runner.os == 'Linux' && matrix.arch != 'auto' - with: - platforms: all - name: Build binary wheels uses: pypa/cibuildwheel@v3.3.1 with: @@ -374,11 +369,6 @@ jobs: if: ${{ startsWith(matrix.os, 'windows-') }} with: arch: ${{ contains(matrix.os, 'arm') && 'arm64' || 'x64' }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3.7.0 - if: runner.os == 'Linux' && matrix.arch != 'auto' - with: - platforms: all - name: Setup Python uses: actions/setup-python@v5.6.0 with: diff --git a/pyproject.toml b/pyproject.toml index 9779fdd2..3abd4137 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ build = "cp310-* cp311-* cp312-* cp313-* cp314-*" # `build_binpy_wheels`, can we deduplicate and just use that? # Or do we need these when building wheels for release, which may run on # separate pipelines? -skip = ["*-win32", "cp3{10}-win_arm64", "cp313-musllinux_i686"] +skip = ["*-win32", "cp310-win_arm64", "cp313-musllinux_i686"] build-frontend = "build" build-verbosity = 1 test-command = "python {project}/run_tests.py" From 2ffc719bca61709738a5f5543647a038b9e3d525 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 14 Apr 2026 14:16:20 -0400 Subject: [PATCH 7/7] Update changelog and fix minimum version in setup.py --- CHANGELOG.rst | 1 + dev/secrets_configuration.sh | 2 ++ setup.py | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d28eb7cf..836caa15 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,7 @@ Changes * FIX: Make sure that the profiled code is run in the ``sys.modules['__main__']`` namespace to avoid issues w/e.g. pickling (#423) +* CHANGE: Drop support for Python 3.8 and Python 3.9 5.0.2 diff --git a/dev/secrets_configuration.sh b/dev/secrets_configuration.sh index 76063b57..e47e61a2 100644 --- a/dev/secrets_configuration.sh +++ b/dev/secrets_configuration.sh @@ -2,5 +2,7 @@ export VARNAME_CI_SECRET="PYUTILS_CI_SECRET" export VARNAME_TWINE_PASSWORD="PYUTILS_PYPI_MASTER_TOKEN" export VARNAME_TEST_TWINE_PASSWORD="PYUTILS_TEST_PYPI_MASTER_TOKEN" export VARNAME_TWINE_USERNAME="PYUTILS_PYPI_MASTER_TOKEN_USERNAME" +export GITHUB_ENVIRONMENT_PYPI="pypi" +export GITHUB_ENVIRONMENT_TESTPYPI="testpypi" export VARNAME_TEST_TWINE_USERNAME="PYUTILS_TEST_PYPI_MASTER_TOKEN_USERNAME" export GPG_IDENTIFIER="=PyUtils-CI " diff --git a/setup.py b/setup.py index c5842e6a..739d5d96 100755 --- a/setup.py +++ b/setup.py @@ -315,7 +315,7 @@ def run_cythonize(force=False): setupkw['license'] = 'BSD' setupkw['packages'] = list(setuptools.find_packages()) setupkw['py_modules'] = ['kernprof', 'line_profiler'] - setupkw['python_requires'] = '>=3.8' + setupkw['python_requires'] = '>=3.10' setupkw['license_files'] = ['LICENSE.txt', 'LICENSE_Python.txt'] setupkw['package_data'] = {'line_profiler': ['py.typed', '*.pyi', '*.toml']} # `include_package_data` is needed to put `rc/line_profiler.toml` in