From 3c915b8573859fae2b7538314d87b2c47dad10fe Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Thu, 24 Sep 2026 14:11:45 -0500 Subject: [PATCH 1/3] Gate release on exact artifacts Build distributions once, qualify the exact wheel and sdist across the supported Python/OS matrix, run optimized/static/differential and real-Core checks, and publish only after those jobs pass. Record source SHA, dependency environments, Bitcoin Core version, artifact hashes, and qualification results in a release provenance bundle. Scrub PYTHONPATH and user-site imports in installed-artifact checks so the source checkout cannot satisfy the test accidentally. Security: release qualification uses pinned Bitcoin Core v32.0rc2 bytes and read-only permissions until the final publication job. Validation: 865 normal and 865 optimized tests passed; mypy, Ruff check/format, build and Twine passed; exact wheel and sdist both passed isolated installed-package verification with PYTHONPATH deliberately contaminated; workflow YAML parses successfully. Fixes #5. --- .github/workflows/publish.yml | 222 ++++++++++++++++++++++++++++-- tools/verify_installed_wheel.py | 4 + tools/verify_wheel_environment.py | 41 +++++- 3 files changed, 248 insertions(+), 19 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7edcac9f..63708e7d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,21 +6,51 @@ on: env: SOURCE_DATE_EPOCH: "1763060600" + BITCOIN_CORE_ARCHIVE: bitcoin-32.0rc2-x86_64-linux-gnu.tar.gz + BITCOIN_CORE_SHA256: 0255103718033e6aee15fa944717fc277e047b845bff1e7408af0ea732d8d0c1 + BITCOIN_CORE_URL: https://bitcoincore.org/bin/bitcoin-core-32.0/test.rc2 + +permissions: + contents: read jobs: build: runs-on: ubuntu-latest permissions: - contents: write + contents: read + outputs: + python_versions: ${{ steps.metadata.outputs.python_versions }} + version: ${{ steps.metadata.outputs.version }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - - run: >- - python -c "import os,tomllib; - version=tomllib.load(open('pyproject.toml','rb'))['project']['version']; - assert os.environ['GITHUB_REF_NAME'] == f'v{version}'" + - id: metadata + name: Check release metadata + run: | + python - <<'PY' + import json + import os + import tomllib + + with open("pyproject.toml", "rb") as file: + project = tomllib.load(file)["project"] + version = project["version"] + if os.environ["GITHUB_REF_NAME"] != f"v{version}": + raise SystemExit("release tag does not match package version") + prefix = "Programming Language :: Python :: " + versions = sorted( + classifier.removeprefix(prefix) + for classifier in project["classifiers"] + if classifier.startswith(prefix) and classifier.removeprefix(prefix).count(".") == 1 + ) + if not versions: + raise SystemExit("no supported Python versions are classified") + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"version={version}\n") + output.write(f"python_versions={json.dumps(versions, separators=(',', ':'))}\n") + PY - run: >- python -m pip install --require-hashes -r requirements/release-build-dependencies.txt @@ -33,28 +63,196 @@ jobs: tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" --owner=0 --group=0 --numeric-owner \ -C normalized-sdist -cf - "codex32-${GITHUB_REF_NAME#v}" | gzip -n > "$archive.new" mv "$archive.new" "$archive" - - name: Attach distributions to the GitHub release - run: gh release upload "$GITHUB_REF_NAME" dist/* - env: - GH_TOKEN: ${{ github.token }} + - name: Record built artifact provenance + run: | + mkdir provenance + git rev-parse HEAD > provenance/source-commit.txt + sha256sum dist/* > provenance/artifact-sha256.txt + python -m pip freeze --all > provenance/build-environment.txt - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: distributions path: dist/ + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: build-provenance + path: provenance/ - publish: + supported-matrix: + needs: build + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ${{ fromJSON(needs.build.outputs.python_versions) }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ matrix.python-version }} + - run: python -m pip install --upgrade pip + - run: python -m pip install --require-hashes -r requirements/cli-build-dependencies.txt + - name: Install legacy wallet-test oracle when present + shell: bash + run: | + if [[ -f requirements/test-wallet-dependencies.txt ]]; then + python -m pip install --no-build-isolation --require-hashes \ + -r requirements/test-wallet-dependencies.txt + fi + - run: python -m pip install --no-build-isolation -e '.[dev]' + - run: python -m pip check + - run: python -m pytest -q + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: distributions + path: dist/ + - name: Install and test the exact wheel and sdist + shell: bash + run: | + for artifact in dist/codex32-*.whl dist/codex32-*.tar.gz; do + python tools/verify_wheel_environment.py --artifact "$artifact" + done + - name: Record qualification dependencies + shell: bash + run: >- + python -m pip freeze --all > + "qualification-${RUNNER_OS}-${{ matrix.python-version }}.txt" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: qualification-${{ runner.os }}-${{ matrix.python-version }} + path: qualification-*.txt + + quality: needs: build runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.13" + - run: python -m pip install --upgrade pip + - run: python -m pip install --require-hashes -r requirements/cli-build-dependencies.txt + - name: Install legacy wallet-test oracle when present + run: | + if [[ -f requirements/test-wallet-dependencies.txt ]]; then + python -m pip install --no-build-isolation --require-hashes \ + -r requirements/test-wallet-dependencies.txt + fi + - run: python -m pip install --no-build-isolation -e '.[dev]' + - run: python -O -m pytest -q + - run: python -m mypy src/codex32 + - run: python -m ruff check . + - run: python -m ruff format --check . + - run: python tools/differential_correction.py --verify + - name: Run legacy wallet differential when present + run: | + if [[ -f tools/differential_wallet.py ]]; then + python tools/differential_wallet.py --verify + fi + + bitcoin-core: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.13" + - name: Download pinned Bitcoin Core + run: | + curl --fail --location --proto '=https' --tlsv1.2 \ + --output "$RUNNER_TEMP/$BITCOIN_CORE_ARCHIVE" \ + "$BITCOIN_CORE_URL/$BITCOIN_CORE_ARCHIVE" + echo "$BITCOIN_CORE_SHA256 $RUNNER_TEMP/$BITCOIN_CORE_ARCHIVE" | sha256sum --check + tar -xzf "$RUNNER_TEMP/$BITCOIN_CORE_ARCHIVE" -C "$RUNNER_TEMP" + - name: Run real-Core release integration + env: + PYTHONPATH: ${{ github.workspace }}/src + run: | + core="$RUNNER_TEMP/bitcoin-32.0rc2/bin" + "$core/bitcoind" --version | head -1 | tee bitcoin-core-version.txt + python tools/bitcoin_core_regtest.py \ + --bitcoind "$core/bitcoind" \ + --bitcoin-cli "$core/bitcoin-cli" + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: bitcoin-core-provenance + path: bitcoin-core-version.txt + + publish: + needs: [build, supported-matrix, quality, bitcoin-core] + runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/codex32 permissions: - contents: read + contents: write id-token: write steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: distributions path: dist/ - - name: Publish distributions to PyPI + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: build-provenance + path: provenance/ + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: bitcoin-core-provenance + path: provenance/ + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: qualification-* + path: provenance/dependencies/ + merge-multiple: true + - name: Verify qualified artifact bytes + run: sha256sum --check provenance/artifact-sha256.txt + - name: Record release qualification + env: + RELEASE_VERSION: ${{ needs.build.outputs.version }} + MATRIX_RESULT: ${{ needs.supported-matrix.result }} + QUALITY_RESULT: ${{ needs.quality.result }} + CORE_RESULT: ${{ needs.bitcoin-core.result }} + run: | + python - <<'PY' + import json + import os + from pathlib import Path + + hashes = {} + for line in Path("provenance/artifact-sha256.txt").read_text().splitlines(): + digest, path = line.split(maxsplit=1) + hashes[Path(path).name] = digest + record = { + "source_commit": Path("provenance/source-commit.txt").read_text().strip(), + "tag": os.environ["GITHUB_REF_NAME"], + "version": os.environ["RELEASE_VERSION"], + "bitcoin_core": Path("provenance/bitcoin-core-version.txt").read_text().strip(), + "artifact_sha256": hashes, + "qualification": { + "supported_matrix": os.environ["MATRIX_RESULT"], + "quality": os.environ["QUALITY_RESULT"], + "bitcoin_core": os.environ["CORE_RESULT"], + }, + } + Path("provenance/release-qualification.json").write_text( + json.dumps(record, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + PY + - name: Bundle release provenance + run: >- + tar -czf "codex32-${{ needs.build.outputs.version }}-provenance.tar.gz" + -C provenance . + - name: Attach qualified distributions and provenance + run: >- + gh release upload "$GITHUB_REF_NAME" dist/* + "codex32-${{ needs.build.outputs.version }}-provenance.tar.gz" --clobber + env: + GH_TOKEN: ${{ github.token }} + - name: Publish qualified distributions to PyPI uses: pypa/gh-action-pypi-publish@a892a5a61159132606e93a2fa6f4358831b04d26 # v1.14.2 + with: + packages-dir: dist/ diff --git a/tools/verify_installed_wheel.py b/tools/verify_installed_wheel.py index 373b532d..b456759b 100644 --- a/tools/verify_installed_wheel.py +++ b/tools/verify_installed_wheel.py @@ -4,6 +4,7 @@ import importlib.util import sys +from pathlib import Path import codex32 from codex32 import ( @@ -26,6 +27,9 @@ def main() -> None: + source_package = Path(__file__).resolve().parents[1] / "src" / "codex32" + imported_package = Path(codex32.__file__).resolve().parent + assert imported_package != source_package assert importlib.util.find_spec("bip32") is None assert importlib.util.find_spec("coincurve") is None diff --git a/tools/verify_wheel_environment.py b/tools/verify_wheel_environment.py index 689e1ee0..50cc0f85 100644 --- a/tools/verify_wheel_environment.py +++ b/tools/verify_wheel_environment.py @@ -1,4 +1,4 @@ -"""Install the built wheel without dependencies and run the installed-wheel verifier.""" +"""Install a built distribution in isolation and run the installed-package verifier.""" from __future__ import annotations @@ -13,22 +13,46 @@ def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--wheel", type=Path) + parser.add_argument("--artifact", type=Path) arguments = parser.parse_args() root = Path(__file__).resolve().parents[1] - if arguments.wheel is None: + if arguments.wheel is not None and arguments.artifact is not None: + parser.error("choose only one of --wheel or --artifact") + requested = arguments.artifact or arguments.wheel + if requested is None: wheels = tuple((root / "dist").glob("codex32-*.whl")) if len(wheels) != 1: raise RuntimeError(f"expected exactly one codex32 wheel, found {len(wheels)}") - wheel = wheels[0] + artifact = wheels[0] else: - wheel = arguments.wheel.resolve() - if not wheel.is_file(): - raise FileNotFoundError(wheel) + artifact = requested.resolve() + if not artifact.is_file(): + raise FileNotFoundError(artifact) + if artifact.suffix != ".whl" and not artifact.name.endswith(".tar.gz"): + raise ValueError("artifact must be a wheel or .tar.gz source distribution") with tempfile.TemporaryDirectory(prefix="codex32-wheel-") as temporary: environment = Path(temporary) venv.EnvBuilder(with_pip=True).create(environment) python = environment / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + clean_env = os.environ.copy() + clean_env.pop("PYTHONPATH", None) + clean_env["PYTHONNOUSERSITE"] = "1" + if artifact.name.endswith(".tar.gz"): + subprocess.run( + [ + str(python), + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--require-hashes", + "-r", + str(root / "requirements" / "cli-build-dependencies.txt"), + ], + check=True, + env=clean_env, + ) subprocess.run( [ str(python), @@ -37,14 +61,17 @@ def main() -> None: "install", "--disable-pip-version-check", "--no-deps", - str(wheel), + "--no-build-isolation", + str(artifact), ], check=True, + env=clean_env, ) subprocess.run( [str(python), str(root / "tools" / "verify_installed_wheel.py")], check=True, cwd=temporary, + env=clean_env, ) From 446df199636800abeef936f9cbbe5333062cf103 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Sat, 26 Sep 2026 19:48:54 -0500 Subject: [PATCH 2/3] release: Gate publication on qualified artifacts --- .github/workflows/publish.yml | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 63708e7d..675aa936 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,8 +1,9 @@ name: Publish release on: - release: - types: [published] + push: + tags: + - "v*" env: SOURCE_DATE_EPOCH: "1763060600" @@ -91,6 +92,7 @@ jobs: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - run: python -m pip install --upgrade pip - run: python -m pip install --require-hashes -r requirements/cli-build-dependencies.txt - name: Install legacy wallet-test oracle when present @@ -159,6 +161,12 @@ jobs: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: distributions + path: dist/ + - name: Install qualified wheel + run: python -m pip install --no-deps dist/codex32-*.whl - name: Download pinned Bitcoin Core run: | curl --fail --location --proto '=https' --tlsv1.2 \ @@ -168,7 +176,7 @@ jobs: tar -xzf "$RUNNER_TEMP/$BITCOIN_CORE_ARCHIVE" -C "$RUNNER_TEMP" - name: Run real-Core release integration env: - PYTHONPATH: ${{ github.workspace }}/src + PYTHONNOUSERSITE: "1" run: | core="$RUNNER_TEMP/bitcoin-32.0rc2/bin" "$core/bitcoind" --version | head -1 | tee bitcoin-core-version.txt @@ -246,13 +254,23 @@ jobs: run: >- tar -czf "codex32-${{ needs.build.outputs.version }}-provenance.tar.gz" -C provenance . - - name: Attach qualified distributions and provenance - run: >- - gh release upload "$GITHUB_REF_NAME" dist/* - "codex32-${{ needs.build.outputs.version }}-provenance.tar.gz" --clobber + - name: Stage draft GitHub release env: GH_TOKEN: ${{ github.token }} + run: | + artifact="codex32-${{ needs.build.outputs.version }}-provenance.tar.gz" + if draft="$(gh release view "$GITHUB_REF_NAME" --json isDraft --jq .isDraft 2>/dev/null)"; then + [ "$draft" = true ] || { echo "release is already public" >&2; exit 1; } + gh release upload "$GITHUB_REF_NAME" dist/* "$artifact" --clobber + else + gh release create "$GITHUB_REF_NAME" --draft --verify-tag \ + --title "$GITHUB_REF_NAME" --generate-notes dist/* "$artifact" + fi - name: Publish qualified distributions to PyPI uses: pypa/gh-action-pypi-publish@a892a5a61159132606e93a2fa6f4358831b04d26 # v1.14.2 with: packages-dir: dist/ + - name: Publish GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: gh release edit "$GITHUB_REF_NAME" --draft=false From cac15445f0429c8f937dc40815281bc2a1a2d919 Mon Sep 17 00:00:00 2001 From: Ben Westgate Date: Tue, 27 Oct 2026 01:17:21 -0500 Subject: [PATCH 3/3] release: Keep package qualification focused Retain the tested package qualification workflow, explicit GH_REPO, prerelease classification and stable-publication guard from the proposed reduction. Remove RC-specific release integration and duplicate source checks; #207 tracks stable Core qualification and #164 its binary pin. Keep #52 on its existing native-stack base because GitHub rejects base retargeting while it is part of a stack. This replacement retains only the three intended release-file changes and does not carry unrelated integration-base changes into the PR. Build, isolated wheel/sdist checks, Twine, focused Ruff, metadata cases, source-CI refusal, hash-tampering rejection and mocked release creation passed on Linux Python 3.14. No actual release publication was performed. Refs #5, #52, #207. --- .github/workflows/publish.yml | 217 +++++++------------------------- tools/verify_installed_wheel.py | 2 - 2 files changed, 44 insertions(+), 175 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 675aa936..b6f75252 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,9 +7,6 @@ on: env: SOURCE_DATE_EPOCH: "1763060600" - BITCOIN_CORE_ARCHIVE: bitcoin-32.0rc2-x86_64-linux-gnu.tar.gz - BITCOIN_CORE_SHA256: 0255103718033e6aee15fa944717fc277e047b845bff1e7408af0ea732d8d0c1 - BITCOIN_CORE_URL: https://bitcoincore.org/bin/bitcoin-core-32.0/test.rc2 permissions: contents: read @@ -19,14 +16,25 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + actions: read outputs: python_versions: ${{ steps.metadata.outputs.python_versions }} - version: ${{ steps.metadata.outputs.version }} + prerelease: ${{ steps.metadata.outputs.prerelease }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Require successful source CI + env: + GH_TOKEN: ${{ github.token }} + run: | + test "$(gh run list --repo "$GITHUB_REPOSITORY" --commit "$GITHUB_SHA" \ + --workflow python-package.yml --status completed --limit 1 \ + --json conclusion --jq '.[0].conclusion')" = success - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" + - run: >- + python -m pip install --require-hashes + -r requirements/release-build-dependencies.txt - id: metadata name: Check release metadata run: | @@ -34,27 +42,24 @@ jobs: import json import os import tomllib + from packaging.version import Version with open("pyproject.toml", "rb") as file: project = tomllib.load(file)["project"] - version = project["version"] - if os.environ["GITHUB_REF_NAME"] != f"v{version}": + version = Version(project["version"]) + if os.environ["GITHUB_REF_NAME"] != f"v{project['version']}": raise SystemExit("release tag does not match package version") prefix = "Programming Language :: Python :: " versions = sorted( - classifier.removeprefix(prefix) - for classifier in project["classifiers"] - if classifier.startswith(prefix) and classifier.removeprefix(prefix).count(".") == 1 + item.removeprefix(prefix) for item in project["classifiers"] + if item.startswith(prefix) and item.removeprefix(prefix).count(".") == 1 ) if not versions: raise SystemExit("no supported Python versions are classified") with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: - output.write(f"version={version}\n") - output.write(f"python_versions={json.dumps(versions, separators=(',', ':'))}\n") + output.write(f"python_versions={json.dumps(versions)}\n") + output.write(f"prerelease={str(version.is_prerelease).lower()}\n") PY - - run: >- - python -m pip install --require-hashes - -r requirements/release-build-dependencies.txt - run: python -m build --no-isolation - name: Normalize source-archive metadata run: | @@ -64,22 +69,19 @@ jobs: tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" --owner=0 --group=0 --numeric-owner \ -C normalized-sdist -cf - "codex32-${GITHUB_REF_NAME#v}" | gzip -n > "$archive.new" mv "$archive.new" "$archive" - - name: Record built artifact provenance + - name: Record artifact hashes and source run: | - mkdir provenance - git rev-parse HEAD > provenance/source-commit.txt - sha256sum dist/* > provenance/artifact-sha256.txt - python -m pip freeze --all > provenance/build-environment.txt + sha256sum dist/* > SHA256SUMS + git rev-parse HEAD > SOURCE_COMMIT - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: distributions - path: dist/ - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: build-provenance - path: provenance/ + path: | + dist/ + SHA256SUMS + SOURCE_COMMIT - supported-matrix: + qualify: needs: build strategy: fail-fast: false @@ -87,109 +89,29 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ${{ fromJSON(needs.build.outputs.python_versions) }} runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - run: python -m pip install --upgrade pip - - run: python -m pip install --require-hashes -r requirements/cli-build-dependencies.txt - - name: Install legacy wallet-test oracle when present - shell: bash - run: | - if [[ -f requirements/test-wallet-dependencies.txt ]]; then - python -m pip install --no-build-isolation --require-hashes \ - -r requirements/test-wallet-dependencies.txt - fi - - run: python -m pip install --no-build-isolation -e '.[dev]' - - run: python -m pip check - - run: python -m pytest -q - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: distributions - path: dist/ - - name: Install and test the exact wheel and sdist - shell: bash + - name: Install and smoke-test the exact wheel and sdist run: | + sha256sum --check SHA256SUMS for artifact in dist/codex32-*.whl dist/codex32-*.tar.gz; do python tools/verify_wheel_environment.py --artifact "$artifact" done - - name: Record qualification dependencies - shell: bash - run: >- - python -m pip freeze --all > - "qualification-${RUNNER_OS}-${{ matrix.python-version }}.txt" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: qualification-${{ runner.os }}-${{ matrix.python-version }} - path: qualification-*.txt - - quality: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: "3.13" - - run: python -m pip install --upgrade pip - - run: python -m pip install --require-hashes -r requirements/cli-build-dependencies.txt - - name: Install legacy wallet-test oracle when present - run: | - if [[ -f requirements/test-wallet-dependencies.txt ]]; then - python -m pip install --no-build-isolation --require-hashes \ - -r requirements/test-wallet-dependencies.txt - fi - - run: python -m pip install --no-build-isolation -e '.[dev]' - - run: python -O -m pytest -q - - run: python -m mypy src/codex32 - - run: python -m ruff check . - - run: python -m ruff format --check . - - run: python tools/differential_correction.py --verify - - name: Run legacy wallet differential when present - run: | - if [[ -f tools/differential_wallet.py ]]; then - python tools/differential_wallet.py --verify - fi - - bitcoin-core: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: "3.13" - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: distributions - path: dist/ - - name: Install qualified wheel - run: python -m pip install --no-deps dist/codex32-*.whl - - name: Download pinned Bitcoin Core - run: | - curl --fail --location --proto '=https' --tlsv1.2 \ - --output "$RUNNER_TEMP/$BITCOIN_CORE_ARCHIVE" \ - "$BITCOIN_CORE_URL/$BITCOIN_CORE_ARCHIVE" - echo "$BITCOIN_CORE_SHA256 $RUNNER_TEMP/$BITCOIN_CORE_ARCHIVE" | sha256sum --check - tar -xzf "$RUNNER_TEMP/$BITCOIN_CORE_ARCHIVE" -C "$RUNNER_TEMP" - - name: Run real-Core release integration - env: - PYTHONNOUSERSITE: "1" - run: | - core="$RUNNER_TEMP/bitcoin-32.0rc2/bin" - "$core/bitcoind" --version | head -1 | tee bitcoin-core-version.txt - python tools/bitcoin_core_regtest.py \ - --bitcoind "$core/bitcoind" \ - --bitcoin-cli "$core/bitcoin-cli" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: bitcoin-core-provenance - path: bitcoin-core-version.txt publish: - needs: [build, supported-matrix, quality, bitcoin-core] + needs: [build, qualify] + # Stable publication waits for the required Core qualification job (#207). + if: needs.build.outputs.prerelease == 'true' runs-on: ubuntu-latest environment: name: pypi @@ -197,80 +119,29 @@ jobs: permissions: contents: write id-token: write + env: + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + PRERELEASE: ${{ needs.build.outputs.prerelease }} steps: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: distributions - path: dist/ - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: build-provenance - path: provenance/ - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - name: bitcoin-core-provenance - path: provenance/ - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - pattern: qualification-* - path: provenance/dependencies/ - merge-multiple: true - name: Verify qualified artifact bytes - run: sha256sum --check provenance/artifact-sha256.txt - - name: Record release qualification - env: - RELEASE_VERSION: ${{ needs.build.outputs.version }} - MATRIX_RESULT: ${{ needs.supported-matrix.result }} - QUALITY_RESULT: ${{ needs.quality.result }} - CORE_RESULT: ${{ needs.bitcoin-core.result }} - run: | - python - <<'PY' - import json - import os - from pathlib import Path - - hashes = {} - for line in Path("provenance/artifact-sha256.txt").read_text().splitlines(): - digest, path = line.split(maxsplit=1) - hashes[Path(path).name] = digest - record = { - "source_commit": Path("provenance/source-commit.txt").read_text().strip(), - "tag": os.environ["GITHUB_REF_NAME"], - "version": os.environ["RELEASE_VERSION"], - "bitcoin_core": Path("provenance/bitcoin-core-version.txt").read_text().strip(), - "artifact_sha256": hashes, - "qualification": { - "supported_matrix": os.environ["MATRIX_RESULT"], - "quality": os.environ["QUALITY_RESULT"], - "bitcoin_core": os.environ["CORE_RESULT"], - }, - } - Path("provenance/release-qualification.json").write_text( - json.dumps(record, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - PY - - name: Bundle release provenance - run: >- - tar -czf "codex32-${{ needs.build.outputs.version }}-provenance.tar.gz" - -C provenance . + run: sha256sum --check SHA256SUMS - name: Stage draft GitHub release - env: - GH_TOKEN: ${{ github.token }} run: | - artifact="codex32-${{ needs.build.outputs.version }}-provenance.tar.gz" if draft="$(gh release view "$GITHUB_REF_NAME" --json isDraft --jq .isDraft 2>/dev/null)"; then [ "$draft" = true ] || { echo "release is already public" >&2; exit 1; } - gh release upload "$GITHUB_REF_NAME" dist/* "$artifact" --clobber + gh release edit "$GITHUB_REF_NAME" --prerelease="$PRERELEASE" + gh release upload "$GITHUB_REF_NAME" dist/* SHA256SUMS SOURCE_COMMIT --clobber else - gh release create "$GITHUB_REF_NAME" --draft --verify-tag \ - --title "$GITHUB_REF_NAME" --generate-notes dist/* "$artifact" + gh release create "$GITHUB_REF_NAME" --draft --verify-tag --generate-notes \ + --title "$GITHUB_REF_NAME" --prerelease="$PRERELEASE" dist/* SHA256SUMS SOURCE_COMMIT fi - name: Publish qualified distributions to PyPI uses: pypa/gh-action-pypi-publish@a892a5a61159132606e93a2fa6f4358831b04d26 # v1.14.2 with: packages-dir: dist/ - name: Publish GitHub release - env: - GH_TOKEN: ${{ github.token }} run: gh release edit "$GITHUB_REF_NAME" --draft=false diff --git a/tools/verify_installed_wheel.py b/tools/verify_installed_wheel.py index b456759b..0e0d22b5 100644 --- a/tools/verify_installed_wheel.py +++ b/tools/verify_installed_wheel.py @@ -49,8 +49,6 @@ def main() -> None: secret = parse_codex32(_SECRET) assert isinstance(secret, MasterSeed) assert master_xprv(secret) == _XPRV - assert "core_descriptors" not in codex32.__all__ - assert not hasattr(codex32, "core_descriptors") assert "bip32" not in sys.modules assert "coincurve" not in sys.modules