Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-httptools for 
openSUSE:Factory checked in at 2026-10-27 18:19:36
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-httptools (Old)
 and      /work/SRC/openSUSE:Factory/.python-httptools.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-httptools"

Tue Oct 27 18:19:36 2026 rev:10 rq:1392651 version:0.9.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-httptools/python-httptools.changes        
2026-06-29 17:29:22.027999239 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-httptools.new.2252680/python-httptools.changes
   2026-10-27 18:20:43.781036200 +0100
@@ -1,0 +2,11 @@
+Mon Oct 26 08:18:07 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.9.0:
+  * Upgrade llhttp to 9.4.3
+  * Build CPython 3.15 wheels
+  * Add RISC-V 64-bit wheels
+  * Add Windows ARM64 support in CI
+  * Build PPC64LE wheels
+  * Modernize test and release workflows
+
+-------------------------------------------------------------------

Old:
----
  httptools-0.8.0.tar.gz

New:
----
  httptools-0.9.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-httptools.spec ++++++
--- /var/tmp/diff_new_pack.E1Z37d/_old  2026-10-27 18:20:46.065131866 +0100
+++ /var/tmp/diff_new_pack.E1Z37d/_new  2026-10-27 18:20:46.068131992 +0100
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-httptools
-Version:        0.8.0
+Version:        0.9.0
 Release:        0
 Summary:        Python framework independent HTTP protocol utils
 License:        MIT

++++++ httptools-0.8.0.tar.gz -> httptools-0.9.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httptools-0.8.0/.github/RELEASING.rst 
new/httptools-0.9.0/.github/RELEASING.rst
--- old/httptools-0.8.0/.github/RELEASING.rst   1970-01-01 01:00:00.000000000 
+0100
+++ new/httptools-0.9.0/.github/RELEASING.rst   2026-10-09 21:52:11.000000000 
+0200
@@ -0,0 +1,50 @@
+Releasing httptools
+===================
+
+The ``Release`` workflow builds the source distribution and builds and tests
+the Linux, macOS, and Windows wheel matrix.  Unless it's a dry-run (see below),
+it also merges and tags the release PR, creates a GitHub release, and uploads
+the distributions to PyPI.
+
+Dry-run a release
+-----------------
+
+Two ways of building and testing release wheels without permanent publishing:
+
+* Add the ``release-dry-run`` label to a PR targeting ``master``, ``ci``, or a
+  release branch. The label stays in effect for later commits to that PR.  It
+  also works for PRs from forks and is useful to test changes to the release
+  workflow itself or to test build targets that are not tested by regular PR
+  CI.
+
+* On GitHub, open **Actions > Release > Run workflow**, then select a
+  repository branch, including ``master``. Manual runs are always dry runs.
+  GitHub shows the **Run workflow** button once this workflow is on the
+  default branch.
+
+You can then find the ``dist`` artifact under **Artifacts** on the workflow run
+page.  The wheels are built and tested by cibuildwheel.
+
+Publish a release
+-----------------
+
+1. Update ``__version__`` in ``httptools/_version.py`` and prepare the release
+   changelog. ``.github/release_log.py`` can help gather changes since the
+   previous release tag::
+
+      $ uv run --no-project --with requests \
+          .github/release_log.py <previously-released-version-tag>
+
+2. Open a PR to ``master`` with the version change and release notes in its
+   body.  Without the dry-run label, the ``Release`` workflow builds the
+   distributions and tests the wheels.  Check the build results, then have a
+   Release Manager approve the pending ``pypi`` deployment using
+   **Review deployments** on the workflow run.
+
+3. After deployment approval, the workflow merges the PR, signs a tag on the
+   merge commit, creates the GitHub release, and uploads the distributions to
+   PyPI.  Check these outputs after it finishes, then edit the GitHub release
+   notes as needed.
+
+4. Open ``master`` for development by updating ``httptools/_version.py`` to the
+   next development version with a ``.dev0`` suffix.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httptools-0.8.0/.github/release_log.py 
new/httptools-0.9.0/.github/release_log.py
--- old/httptools-0.8.0/.github/release_log.py  1970-01-01 01:00:00.000000000 
+0100
+++ new/httptools-0.9.0/.github/release_log.py  2026-10-09 21:52:11.000000000 
+0200
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016-present the asyncpg authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of asyncpg and is released under
+# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0
+
+
+import json
+import requests
+import re
+import sys
+
+
+BASE_URL = 'https://api.github.com/repos/magicstack/httptools/compare'
+
+
+def main():
+    if len(sys.argv) < 2:
+        print('pass a sha1 hash as a first argument')
+        sys.exit(1)
+
+    from_hash = sys.argv[1]
+    to_hash = 'master'
+    if len(sys.argv) > 2:
+        to_hash = sys.argv[2]
+
+    r = requests.get(f'{BASE_URL}/{from_hash}...{to_hash}')
+    data = json.loads(r.text)
+
+    for commit in data['commits']:
+        message = commit['commit']['message']
+        first_line = message.partition('\n\n')[0]
+        if commit.get('author'):
+            username = '@{}'.format(commit['author']['login'])
+        else:
+            username = commit['commit']['author']['name']
+        sha = commit["sha"][:8]
+
+        m = re.search(r'\#(?P<num>\d+)\b', message)
+        if m:
+            issue_num = m.group('num')
+        else:
+            issue_num = None
+
+        print(f'* {first_line}')
+        print(f'  (by {username} in {sha}', end='')
+        print(')')
+        print()
+
+
+if __name__ == '__main__':
+    main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httptools-0.8.0/.github/workflows/release.yml 
new/httptools-0.9.0/.github/workflows/release.yml
--- old/httptools-0.8.0/.github/workflows/release.yml   2026-05-26 
00:02:50.000000000 +0200
+++ new/httptools-0.9.0/.github/workflows/release.yml   2026-10-09 
21:52:11.000000000 +0200
@@ -1,93 +1,175 @@
 name: Release
 
 on:
+  workflow_dispatch:
   pull_request:
+    types: [opened, reopened, synchronize, labeled, unlabeled]
     branches:
       - "master"
       - "ci"
       - "[0-9]+.[0-9]+.[0-9x]+*"
-    paths:
-      - "httptools/_version.py"
 
-jobs:
-  validate-release-request:
-    runs-on: ubuntu-latest
-    steps:
-    - name: Validate release PR
-      uses: 
edgedb/action-release/validate-pr@bae6b9134e872166b43d218dd79397c851c41c9a
-      id: checkver
-      with:
-        require_team: Release Managers
-        require_approval: no
-        github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
-        version_file: httptools/_version.py
-        version_line_pattern: |
-          __version__\s*=\s*(?:['"])([[:PEP440:]])(?:['"])
+permissions:
+  contents: read
 
-    - name: Stop if not approved
-      if: steps.checkver.outputs.approved != 'true'
-      run: |
-        echo ::error::PR is not approved yet.
-        exit 1
+# Serialize releases of the same PR; never cancel a publishing job.
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || 
github.ref }}
+  cancel-in-progress: false
 
-    - name: Store release version for later use
-      env:
-        VERSION: ${{ steps.checkver.outputs.version }}
-      run: |
-        mkdir -p dist/
-        echo "${VERSION}" > dist/VERSION
+jobs:
+  classify-release:
+    runs-on: ubuntu-24.04
+    permissions:
+      pull-requests: read
+    outputs:
+      mode: ${{ steps.mode.outputs.mode }}
+    steps:
+    - name: Select release mode
+      id: mode
+      uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3  # 
v9.0.0
+      with:
+        script: |
+          let mode = 'skip';
+
+          if (context.eventName === 'workflow_dispatch') {
+            mode = 'dry_run';
+          } else {
+            const { action, label, pull_request: pr } = context.payload;
+            if (action === 'labeled' || action === 'unlabeled') {
+              // Ignore unrelated label changes and do not release on removal.
+              if (action === 'labeled' && label.name === 'release-dry-run') {
+                mode = 'dry_run';
+              }
+            } else if (pr.labels.some(({ name }) => name === 
'release-dry-run')) {
+              mode = 'dry_run';
+            } else {
+              // Preserve the previous version-file filter for ordinary PRs.
+              const files = await github.paginate(github.rest.pulls.listFiles, 
{
+                ...context.repo,
+                pull_number: pr.number,
+                per_page: 100,
+              });
+              if (files.some(({ filename }) => filename === 
'httptools/_version.py')) {
+                mode = 'release';
+              }
+            }
+          }
 
-    - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882  
# v4.4.3
-      with:
-        name: dist-version
-        path: dist/
+          core.setOutput('mode', mode);
+          core.info(`Release mode: ${mode}`);
 
   build-sdist:
-    needs: validate-release-request
-    runs-on: ubuntu-latest
+    needs: classify-release
+    if: needs.classify-release.outputs.mode != 'skip'
+    runs-on: ubuntu-24.04
+
+    env:
+      PIP_DISABLE_PIP_VERSION_CHECK: 1
 
     steps:
-    - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871  # v4.2.1
+    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
       with:
         fetch-depth: 50
         submodules: true
+        persist-credentials: false
 
-    - name: Install the latest version of uv
-      uses: astral-sh/setup-uv@eb1897b8dc4b5d5bfe39a428a8f2304605e0983c  # 
7.0.0
+    - name: Set up Python
+      uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7  # 
v10.2.0
+      with:
+        python-version: "3.14"
+        activate-environment: true
 
     - name: Build source distribution
       run: |
         uv build --sdist
 
-    - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882  
# v4.4.3
+    - name: Store release version
+      run: |
+        python - <<'PY'
+        import re
+        from pathlib import Path
+
+        source = Path('httptools/_version.py').read_text()
+        match = re.search(
+            
r'''^__version__(?:\s*:\s*typing\.Final)?\s*=\s*['"]([^'"]+)['"]''',
+            source,
+            re.MULTILINE,
+        )
+        if match is None:
+            raise RuntimeError('Could not read the release version')
+        Path('dist/VERSION').write_text(match.group(1) + '\n')
+        PY
+
+    - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  
# v7.0.1
       with:
         name: dist-sdist
-        path: dist/*.tar.*
+        path: dist/
+
+  build-wheels-matrix:
+    needs: classify-release
+    if: needs.classify-release.outputs.mode != 'skip'
+    runs-on: ubuntu-24.04
+    outputs:
+      include: ${{ steps.set-matrix.outputs.include }}
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
+        with:
+          persist-credentials: false
+      - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7  # 
v10.2.0
+        with:
+          python-version: "3.14"
+          activate-environment: true
+      - name: Install cibuildwheel
+        run: uv pip install cibuildwheel==4.2.1
+      - name: Build cibuildwheel matrix
+        id: set-matrix
+        env:
+          WHEEL_TARGETS: |
+            [
+              {"platform": "linux", "archs": "x86_64", "os": "ubuntu-24.04"},
+              {"platform": "linux", "archs": "aarch64", "os": 
"ubuntu-24.04-arm"},
+              {"platform": "linux", "archs": "ppc64le", "os": "ubuntu-24.04"},
+              {"platform": "linux", "archs": "riscv64", "os": "ubuntu-24.04"},
+              {"platform": "macos", "archs": "x86_64", "os": "macos-26-intel"},
+              {"platform": "macos", "archs": "arm64", "os": "macos-26"},
+              {"platform": "windows", "archs": "x86,AMD64", "os": 
"windows-2025"},
+              {"platform": "windows", "archs": "ARM64", "os": "windows-11-arm"}
+            ]
+        run: |
+          python - <<'PY'
+          import json
+          import os
+          import subprocess
+
+          include = []
+          for target in json.loads(os.environ['WHEEL_TARGETS']):
+              identifiers = subprocess.check_output([
+                  'cibuildwheel', '--print-build-identifiers',
+                  '--platform', target['platform'], '--archs', target['archs'],
+              ], text=True).splitlines()
+              include.extend({'only': identifier, 'os': target['os']} for 
identifier in identifiers)
+
+          print(f'Wheel matrix entries: {len(include)}')
+          if not 0 < len(include) <= 256:
+              raise RuntimeError('Wheel matrix must contain between 1 and 256 
entries')
+          if len({entry['only'] for entry in include}) != len(include):
+              raise RuntimeError('Wheel matrix contains duplicate build 
identifiers')
+
+          with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
+              output.write(f'include={json.dumps(include, separators=(",", 
":"))}\n')
+          PY
 
   build-wheels:
-    needs: validate-release-request
+    needs: build-wheels-matrix
+    if: ${{ !cancelled() && needs.build-wheels-matrix.result == 'success' }}
     runs-on: ${{ matrix.os }}
+    name: Build ${{ matrix.only }}
+
     strategy:
+      fail-fast: false
       matrix:
-        os: [ubuntu-latest, macos-latest, windows-latest]
-        cibw_arch: ["auto64", "aarch64", "universal2"]
-        cibw_python:
-          - "cp39"
-          - "cp310"
-          - "cp311"
-          - "cp312"
-          - "cp313"
-          - "cp314"
-          - "cp314t"
-        exclude:
-          - os: ubuntu-latest
-            cibw_arch: universal2
-          - os: macos-latest
-            cibw_arch: aarch64
-          - os: windows-latest
-            cibw_arch: universal2
-          - os: windows-latest
-            cibw_arch: aarch64
+        include: ${{ fromJson(needs.build-wheels-matrix.outputs.include) }}
 
     defaults:
       run:
@@ -97,82 +179,342 @@
       PIP_DISABLE_PIP_VERSION_CHECK: 1
 
     steps:
-    - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871  # v4.2.1
+    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
       with:
         fetch-depth: 50
         submodules: true
+        persist-credentials: false
 
     - name: Set up QEMU
-      if: matrix.os == 'ubuntu-latest' && matrix.cibw_arch == 'aarch64'
-      uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf  
# v3.2.0
+      if: endsWith(matrix.only, '_ppc64le') || endsWith(matrix.only, 
'_riscv64')
+      uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1  
# v4.4.0
       with:
-        platforms: arm64
+        platforms: ppc64le,riscv64
 
-    - uses: pypa/cibuildwheel@7c619efba910c04005a835b110b057fc28fd6e93  # 
v3.2.0
+    - uses: pypa/cibuildwheel@e090b81e30c4d855ea63bf4b6e59204c09a101ae  # 
v4.2.1
+      with:
+        only: ${{ matrix.only }}
       env:
         CIBW_BUILD_VERBOSITY: 1
-        CIBW_BUILD: ${{ matrix.cibw_python }}-*
-        CIBW_ARCHS: ${{ matrix.cibw_arch }}
-        CIBW_TEST_EXTRAS: "test"
-        CIBW_TEST_COMMAND: "python {project}/tests/__init__.py"
-        CIBW_TEST_COMMAND_WINDOWS: "python {project}\\tests\\__init__.py"
-        CIBW_TEST_SKIP: "*universal2:arm64"
 
-    - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882  
# v4.4.3
+    - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  
# v7.0.1
       with:
-        name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}-${{ 
matrix.cibw_python }}
+        name: dist-wheels-${{ matrix.only }}
         path: wheelhouse/*.whl
 
-  publish:
+  merge-artifacts:
+    runs-on: ubuntu-24.04
     needs: [build-sdist, build-wheels]
-    runs-on: ubuntu-latest
+    if: >-
+      ${{ !cancelled() && needs.build-sdist.result == 'success' &&
+          needs.build-wheels.result == 'success' }}
+    steps:
+      - name: Merge Artifacts
+        uses: 
actions/upload-artifact/merge@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
+        with:
+          name: dist
+          pattern: dist-*
+          delete-merged: true
+
+  dry-run-refs:
+    needs: [classify-release, merge-artifacts]
+    if: >-
+      ${{ !cancelled() && needs.classify-release.outputs.mode == 'dry_run' &&
+          needs.merge-artifacts.result == 'success' }}
+    runs-on: ubuntu-24.04
+    steps:
+    - name: Checkout release commit
+      uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
+      with:
+        fetch-depth: 2
+        persist-credentials: false
+
+    - name: Sign release tag locally
+      uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3  # 
v9.0.0
+      with:
+        script: |
+          const fs = require('node:fs');
+          const os = require('node:os');
+          const path = require('node:path');
+          const {execFileSync} = require('node:child_process');
+          const cwd = process.env.GITHUB_WORKSPACE;
+          const pr = context.payload.pull_request;
+          // PR workflows check out GitHub's merge commit. Its second parent
+          // is the exact PR head selected for this dry run.
+          const head = (await exec.getExecOutput('git', [
+            'rev-parse', pr ? 'HEAD^2' : 'HEAD',
+          ], {
+            cwd, silent: true,
+          })).stdout.trim();
+          if (pr && head !== pr.head.sha) {
+            throw new Error(`Checked out ${head}, expected PR head 
${pr.head.sha}`);
+          }
+
+          const source = (await exec.getExecOutput('git', [
+            'show', `${head}:httptools/_version.py`,
+          ], {cwd, silent: true})).stdout;
+          const version = 
source.match(/^__version__(?:\s*:\s*typing\.Final)?\s*=\s*['"]([^'"]+)['"]/m)?.[1];
+          if (!version) throw new Error('Could not read the release version');
+          const tag = `v${version}`;
+          const message = pr
+            ? `${pr.title}\n\n${pr.body || ''}`
+            : `Dry run for ${context.ref}`;
+
+          // Live releases sign the merged commit with the release key. 
Generate
+          // a disposable key here so fork PRs can exercise signing too.
+          const gnupgHome = fs.mkdtempSync(path.join(os.tmpdir(), 
'release-dry-run-gpg-'));
+          fs.chmodSync(gnupgHome, 0o700);
+          const originalGnupgHome = process.env.GNUPGHOME;
+          try {
+            const env = {...process.env, GNUPGHOME: gnupgHome};
+            execFileSync('gpg', [
+              '--batch', '--pinentry-mode', 'loopback', '--passphrase', '',
+              '--quick-generate-key', 'Release dry run 
<[email protected]>',
+              'ed25519', 'sign', '0',
+            ], {env});
+            const keys = execFileSync('gpg', ['--list-secret-keys', 
'--with-colons'], {
+              env, encoding: 'utf8',
+            });
+            const key = keys.split('\n').find(line => 
line.startsWith('fpr:'))?.split(':')[9];
+            if (!key) throw new Error('Could not identify the disposable 
signing key');
+            await exec.exec('git', ['config', 'user.name', 'Release dry run'], 
{cwd});
+            await exec.exec('git', ['config', 'user.email',
+              '[email protected]'], {cwd});
+            await exec.exec('git', ['config', 'user.signingkey', key], {cwd});
+            process.env.GNUPGHOME = gnupgHome;
+            await exec.exec('git', ['tag', '--sign', '--file=-', tag, head], {
+              cwd, input: Buffer.from(message),
+            });
+            await exec.exec('git', ['tag', '--verify', tag], {cwd});
+            await exec.exec('git', ['show', '--no-patch', tag], {cwd});
+          } finally {
+            await exec.exec('gpgconf', ['--kill', 'gpg-agent'], {
+              env: {...process.env, GNUPGHOME: gnupgHome},
+              ignoreReturnCode: true,
+            });
+            if (originalGnupgHome === undefined) {
+              delete process.env.GNUPGHOME;
+            } else {
+              process.env.GNUPGHOME = originalGnupgHome;
+            }
+            fs.rmSync(gnupgHome, {recursive: true, force: true});
+          }
+          core.info(`Dry run: signed ${tag} at ${head} locally; no refs were 
pushed.`);
+
+  publish:
+    needs: [classify-release, merge-artifacts]
+    if: >-
+      ${{ !cancelled() && needs.classify-release.outputs.mode == 'release' &&
+          needs.merge-artifacts.result == 'success' }}
+    runs-on: ubuntu-24.04
+
+    environment:
+      name: pypi
+      url: https://pypi.org/p/httptools
+    permissions:
+      id-token: write
+      attestations: write
+      contents: write
+      deployments: write
+      pull-requests: write
 
     steps:
-    - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871  # v4.2.1
+    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
       with:
-        fetch-depth: 5
-        submodules: false
+        fetch-depth: 2
+        persist-credentials: false
 
-    - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 
 # v4.1.8
+    - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c 
 # v8.0.1
       with:
-        pattern: dist-*
-        merge-multiple: true
+        name: dist
         path: dist/
 
-    - name: Extract Release Version
+    - name: Extract release version
       id: relver
       run: |
-        set -e
-        echo ::set-output name=version::$(cat dist/VERSION)
+        echo "version=$(cat dist/VERSION)" >> "$GITHUB_OUTPUT"
         rm dist/VERSION
 
-    - name: Merge and tag the PR
-      uses: 
edgedb/action-release/merge@bae6b9134e872166b43d218dd79397c851c41c9a
-      with:
-        github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
-        ssh_key: ${{ secrets.RELEASE_BOT_SSH_KEY }}
-        gpg_key: ${{ secrets.RELEASE_BOT_GPG_KEY }}
-        gpg_key_id: "5C468778062D87BF!"
-        tag_name: v${{ steps.relver.outputs.version }}
+    - name: Merge release PR
+      id: merge
+      uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3  # 
v9.0.0
+      with:
+        github-token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
+        script: |
+          const expected = context.payload.pull_request;
+          const args = {...context.repo, pull_number: expected.number};
+          const {data: pr} = await github.rest.pulls.get(args);
+          if (pr.head.sha !== expected.head.sha) {
+            throw new Error('PR head changed since the distributions were 
built');
+          }
+
+          let sha;
+          if (pr.merged) {
+            sha = pr.merge_commit_sha;
+            core.info('Release PR is already merged.');
+          } else {
+            const {data: result} = await github.rest.pulls.merge({
+              ...args,
+              sha: expected.head.sha,
+              merge_method: 'merge',
+            });
+            if (!result.merged || !result.sha) {
+              throw new Error('Could not merge release PR: ' + result.message);
+            }
+            sha = result.sha;
+          }
+          if (!sha) throw new Error('Could not identify the merge commit');
+          core.setOutput('sha', sha);
 
-    - name: Publish Github Release
-      uses: 
elprans/gh-action-create-release@5f9abb8f0677196a76ea77e64341fa8ca31dad4f
+    - name: Sign and push release tag
+      uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3  # 
v9.0.0
       env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      with:
-        tag_name: v${{ steps.relver.outputs.version }}
-        release_name: v${{ steps.relver.outputs.version }}
-        target: ${{ github.event.pull_request.base.ref }}
-        body: ${{ github.event.pull_request.body }}
-        draft: false
+        RELEASE_VERSION: ${{ steps.relver.outputs.version }}
+        RELEASE_SHA: ${{ steps.merge.outputs.sha }}
+        RELEASE_BOT_GPG_KEY: ${{ secrets.RELEASE_BOT_GPG_KEY }}
+        RELEASE_BOT_GITHUB_TOKEN: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
+      with:
+        github-token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
+        script: |
+          const fs = require('node:fs');
+          const os = require('node:os');
+          const path = require('node:path');
+          const cwd = process.env.GITHUB_WORKSPACE;
+          const tag = 'v' + process.env.RELEASE_VERSION;
+          const sha = process.env.RELEASE_SHA;
+          const keyId = '5C468778062D87BF';
+          const pr = context.payload.pull_request;
+          if (!process.env.RELEASE_BOT_GPG_KEY) {
+            throw new Error('Release signing key is not configured');
+          }
+          await exec.exec('git', ['check-ref-format', 'refs/tags/' + tag], 
{cwd});
+
+          const gnupgHome = fs.mkdtempSync(path.join(os.tmpdir(), 
'release-gpg-'));
+          fs.chmodSync(gnupgHome, 0o700);
+          const auth = Buffer.from('x-access-token:' + 
process.env.RELEASE_BOT_GITHUB_TOKEN)
+            .toString('base64');
+          core.setSecret(auth);
+          const env = {
+            ...process.env,
+            GNUPGHOME: gnupgHome,
+            GIT_CONFIG_COUNT: '1',
+            GIT_CONFIG_KEY_0: 'http.https://github.com/.extraheader',
+            GIT_CONFIG_VALUE_0: 'AUTHORIZATION: basic ' + auth,
+          };
+          const options = {cwd, env, silent: true};
+          try {
+            await exec.exec('gpg', ['--batch', '--no-tty', '--import'], {
+              ...options, input: Buffer.from(process.env.RELEASE_BOT_GPG_KEY),
+            });
+            const keys = await exec.getExecOutput('gpg', [
+              '--batch', '--with-colons', '--list-secret-keys', keyId + '!',
+            ], options);
+            const fingerprint = keys.stdout.split('\n')
+              .filter(line => line.startsWith('fpr:'))
+              .map(line => line.split(':')[9])
+              .find(value => value.endsWith(keyId));
+            if (!fingerprint) throw new Error('Could not identify the release 
signing key');
+
+            const verify = async () => {
+              const target = (await exec.getExecOutput('git', [
+                'rev-parse', tag + '^{commit}',
+              ], options)).stdout.trim();
+              if (target !== sha) throw new Error('Existing tag points to a 
different commit');
+              const signature = await exec.getExecOutput('git', [
+                '-c', 'gpg.format=openpgp', 'verify-tag', '--raw', tag,
+              ], {...options, ignoreReturnCode: true});
+              const signer = signature.stderr.match(/^\[GNUPG:\] VALIDSIG 
([0-9A-F]+)/m)?.[1];
+              if (signature.exitCode !== 0 || signer !== fingerprint) {
+                throw new Error('Tag does not have a valid signature from the 
release key');
+              }
+            };
+            const fetchTag = () => exec.exec('git', [
+              'fetch', '--force', '--no-tags', 'origin',
+              'refs/tags/' + tag + ':refs/tags/' + tag,
+            ], options);
+            const tagExists = async () => {
+              try {
+                await github.rest.git.getRef({...context.repo, ref: 'tags/' + 
tag});
+                return true;
+              } catch (error) {
+                if (error.status !== 404) throw error;
+                return false;
+              }
+            };
+
+            if (await tagExists()) {
+              await fetchTag();
+              await verify();
+              core.info('Release tag already exists and has the expected 
commit and signature.');
+              return;
+            }
+
+            await exec.exec('git', ['fetch', '--no-tags', 'origin', sha], 
options);
+            const {data: bot} = await github.rest.users.getAuthenticated();
+            const email = bot.email || bot.id + '+' + bot.login + 
'@users.noreply.github.com';
+            await exec.exec('git', [
+              '-c', 'user.name=' + (bot.name || bot.login),
+              '-c', 'user.email=' + email,
+              '-c', 'gpg.format=openpgp',
+              'tag', '--sign', '--local-user', fingerprint + '!', '--file=-', 
tag, sha,
+            ], {...options, input: Buffer.from(pr.title + '\n\n' + (pr.body || 
''))});
+            await verify();
+            const push = await exec.getExecOutput('git', [
+              'push', 'origin', 'refs/tags/' + tag,
+            ], {...options, ignoreReturnCode: true});
+            if (push.exitCode !== 0) {
+              // Accept a concurrent push only when it created the same signed 
tag target.
+              if (!(await tagExists())) throw new Error('Could not push 
release tag');
+              await fetchTag();
+              await verify();
+            }
+            core.info('Signed and pushed ' + tag + ' at ' + sha);
+          } finally {
+            await exec.exec('gpgconf', ['--kill', 'gpg-agent'], {
+              ...options, ignoreReturnCode: true,
+            });
+            fs.rmSync(gnupgHome, {recursive: true, force: true});
+          }
 
-    - run: |
-        ls -al dist/
+    - name: Create GitHub release
+      uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3  # 
v9.0.0
+      env:
+        RELEASE_VERSION: ${{ steps.relver.outputs.version }}
+        RELEASE_SHA: ${{ steps.merge.outputs.sha }}
+        RELEASE_BODY: ${{ github.event.pull_request.body }}
+      with:
+        script: |
+          const tag = 'v' + process.env.RELEASE_VERSION;
+          const args = {...context.repo, tag};
+          const existingRelease = async () => {
+            try {
+              return (await github.rest.repos.getReleaseByTag(args)).data;
+            } catch (error) {
+              if (error.status !== 404) throw error;
+              return null;
+            }
+          };
+          if (await existingRelease()) {
+            core.info('Release ' + tag + ' already exists, skipping.');
+            return;
+          }
+          try {
+            await github.rest.repos.createRelease({
+              ...context.repo,
+              tag_name: tag,
+              name: tag,
+              target_commitish: process.env.RELEASE_SHA,
+              body: process.env.RELEASE_BODY,
+              draft: false,
+              prerelease: false,
+            });
+          } catch (error) {
+            if (error.status !== 422 || !(await existingRelease())) throw 
error;
+            core.info('Release ' + tag + ' was created concurrently, 
skipping.');
+          }
 
     - name: Upload to PyPI
-      uses: 
pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e  # v1.13.0
+      uses: 
pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33  # v1.14.2
       with:
-        user: __token__
-        password: ${{ secrets.PYPI_TOKEN }}
-        # password: ${{ secrets.TEST_PYPI_TOKEN }}
-        # repository_url: https://test.pypi.org/legacy/
+        attestations: true
+        skip-existing: true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httptools-0.8.0/.github/workflows/tests.yml 
new/httptools-0.9.0/.github/workflows/tests.yml
--- old/httptools-0.8.0/.github/workflows/tests.yml     2026-05-26 
00:02:50.000000000 +0200
+++ new/httptools-0.9.0/.github/workflows/tests.yml     2026-10-09 
21:52:11.000000000 +0200
@@ -2,68 +2,131 @@
 
 on:
   push:
-    branches:
-      - master
-      - ci
+    branches: [master, ci]
   pull_request:
-    branches:
-      - master
+    branches: [master]
+
+permissions:
+  contents: read
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
 
 jobs:
-  typecheck:
-    runs-on: ubuntu-latest
+  classify-tests:
+    runs-on: ubuntu-24.04
+    permissions:
+      pull-requests: read
+    outputs:
+      is_release: ${{ steps.version.outputs.is_release }}
+    steps:
+    - name: Check for a version PR
+      id: version
+      uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3  # 
v9.0.0
+      with:
+        script: |
+          let isRelease = false;
+          if (context.eventName === 'pull_request') {
+            const files = await github.paginate(github.rest.pulls.listFiles, {
+              ...context.repo,
+              pull_number: context.payload.pull_request.number,
+              per_page: 100,
+            });
+            isRelease = files.some(({filename}) => filename === 
'httptools/_version.py');
+          }
+          core.setOutput('is_release', isRelease ? 'true' : 'false');
 
+  typecheck:
+    runs-on: ubuntu-24.04
     steps:
-    - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871  # v4.2.1
+    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
       with:
-        fetch-depth: 50
         submodules: true
+        persist-credentials: false
 
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # 
v6.2.0
+    - name: Set up Python
+      uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7  # 
v10.2.0
       with:
-        python-version: "3.9"
+        python-version: "3.14"
+        activate-environment: true
+
+    - name: Install dependencies
+      run: uv pip install -e . --group dev
 
     - name: Type check
-      run: make typecheck
+      run: pyright
 
+    - name: Audit GitHub Actions workflows
+      uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482  
# v0.6.4
+      env:
+        GH_TOKEN: ${{ github.token }}
+      with:
+        token: ${{ github.token }}
+        online-audits: true
+        advanced-security: false
+        annotations: true
 
   build:
+    needs: classify-tests
+    if: needs.classify-tests.outputs.is_release != 'true'
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", 
"3.14t"]
-        os: [windows-latest, ubuntu-latest, macos-latest]
-
-    env:
-      PIP_DISABLE_PIP_VERSION_CHECK: 1
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", 
"3.14t", "3.15", "3.15t"]
+        os: [ubuntu-24.04, macos-26, windows-2025, windows-11-arm]
+        exclude:
+          - os: macos-26
+            python-version: "3.9"
+          - os: windows-11-arm
+            python-version: "3.9"
+          - os: windows-11-arm
+            python-version: "3.10"
+
+    defaults:
+      run:
+        shell: bash
 
     steps:
-    - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871  # v4.2.1
+    - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
       with:
-        fetch-depth: 50
         submodules: true
-
-    - name: Check if release PR.
-      uses: 
edgedb/action-release/validate-pr@bae6b9134e872166b43d218dd79397c851c41c9a
-      id: release
-      with:
-        github_token: ${{ secrets.RELEASE_BOT_GITHUB_TOKEN }}
-        missing_version_ok: yes
-        version_file: httptools/_version.py
-        version_line_pattern: |
-          __version__\s*=\s*(?:['"])([[:PEP440:]])(?:['"])
+        persist-credentials: false
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # 
v6.2.0
-      if: steps.release.outputs.version == 0
+      uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7  # 
v10.2.0
       with:
         python-version: ${{ matrix.python-version }}
+        activate-environment: true
+
+    - name: Install dependencies
+      run: uv pip install -e . --group dev
 
     - name: Test
-      if: steps.release.outputs.version == 0
-      run: |
-        python -m pip install -U pip setuptools wheel
-        python -m pip install .[test]
-        cd tests
-        python -m unittest -v
+      run: python -m unittest -v tests.suite
+
+  # A stable required check for branch protection, including version PRs.
+  regression-tests:
+    name: Regression Tests
+    needs: [classify-tests, typecheck, build]
+    if: ${{ always() }}
+    runs-on: ubuntu-24.04
+    permissions: {}
+    steps:
+    - name: Check required jobs
+      uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3  # 
v9.0.0
+      env:
+        JOBS: ${{ toJSON(needs) }}
+      with:
+        script: |
+          const jobs = JSON.parse(process.env.JOBS);
+          const expected = {
+            'classify-tests': 'success',
+            typecheck: 'success',
+            build: jobs['classify-tests'].outputs.is_release === 'true' ? 
'skipped' : 'success',
+          };
+          for (const [job, result] of Object.entries(expected)) {
+            if (jobs[job].result !== result) {
+              core.setFailed(`${job}: expected ${result}, got 
${jobs[job].result}`);
+            }
+          }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httptools-0.8.0/httptools/_version.py 
new/httptools-0.9.0/httptools/_version.py
--- old/httptools-0.8.0/httptools/_version.py   2026-05-26 00:02:50.000000000 
+0200
+++ new/httptools-0.9.0/httptools/_version.py   2026-10-09 21:52:11.000000000 
+0200
@@ -10,4 +10,4 @@
 # supported platforms, publish the packages on PyPI, merge the PR
 # to the target branch, create a Git tag pointing to the commit.
 
-__version__ = '0.8.0'
+__version__ = '0.9.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/httptools-0.8.0/pyproject.toml 
new/httptools-0.9.0/pyproject.toml
--- old/httptools-0.8.0/pyproject.toml  2026-05-26 00:02:50.000000000 +0200
+++ new/httptools-0.9.0/pyproject.toml  2026-10-09 21:52:11.000000000 +0200
@@ -40,6 +40,17 @@
     "wheel"
 ]
 
+[tool.cibuildwheel]
+build-frontend = "build"
+skip = "pp*"
+test-command = "python {project}/tests/__init__.py"
+
+[tool.cibuildwheel.macos]
+environment = {MACOSX_DEPLOYMENT_TARGET = "11"}
+
+[tool.cibuildwheel.windows]
+test-command = "python {project}\\tests\\__init__.py"
+
 [tool.pyright]
 pythonVersion = "3.8"
 typeCheckingMode = "strict"

Reply via email to