From 64725eb7940cdf53eb65f59f45d891e6b40958bd Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 23 Apr 2026 14:10:34 -0700 Subject: [PATCH] chore(release): route release.yml through ci-tooling reusable @v0.4.2 Replace inline release pipeline with caller of provide-io/ci-tooling/.github/workflows/python-release.yml@v0.4.2 and fix pre-existing mypy errors to unblock commit: - quality/profiling/profiler.py: remove unused type: ignore on None assignment - file/temp.py: silence import-untyped on optional yaml import - cli/testing.py: silence import-untyped on optional yaml import verify-testpypi uses importlib.metadata since provide.testkit has no __version__ attribute (matches the old smoke-test's intent but adds an assertion). --- .github/workflows/release.yml | 136 +++++++++--------- src/provide/testkit/cli/testing.py | 2 +- src/provide/testkit/file/temp.py | 2 +- .../testkit/quality/profiling/profiler.py | 2 +- 4 files changed, 67 insertions(+), 75 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f22d6c95..5c30c258 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,40 +1,28 @@ name: ๐Ÿš€ Release +# Release is driven by a user-created GitHub Release (or manual dispatch for dry runs). +# Tag-push auto-release is deliberately not wired up yet โ€” flip when ready. on: - push: - tags: - - "v*" release: types: [published] workflow_dispatch: jobs: - build: - runs-on: ubuntu-24.04 + # Delegate tests + build + (optional) GH release to the ci-tooling reusable. + # GH release creation is suppressed because `release: published` means one already exists. + release: + uses: provide-io/ci-tooling/.github/workflows/python-release.yml@v0.4.2 permissions: - contents: read - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - python-version: "3.11" - - run: uv sync --group dev - - run: uv run pytest --no-header -q - - run: uv build - - run: uvx twine check dist/* - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: dist - path: dist/* - - name: Generate Python SBOM - run: uvx cyclonedx-py environment --output-format json -o sbom-python.cdx.json - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: sbom-python - path: sbom-python.cdx.json + contents: write + with: + create-github-release: false + secrets: inherit + # TestPyPI publish runs in the caller so PyPI Trusted Publishing matches the + # OIDC job_workflow_ref against this repo's release.yml (reusable workflows + # are not matched by PyPI's publisher config). publish-testpypi: - needs: build + needs: release runs-on: ubuntu-24.04 permissions: id-token: write @@ -43,51 +31,47 @@ jobs: name: testpypi url: https://test.pypi.org/p/provide-testkit steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + - name: ๐Ÿ“ฆ Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: dist + name: release-artifacts path: dist - - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 + - name: ๐Ÿ“ค Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 with: repository-url: https://test.pypi.org/legacy/ skip-existing: true + # Install from TestPyPI and assert the installed version matches the release tag. + # Uses the helper scripts from ci-tooling/scripts/release/. verify-testpypi: needs: publish-testpypi runs-on: ubuntu-24.04 steps: - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + - name: ๐Ÿ“ฅ Checkout ci-tooling helpers + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: provide-io/ci-tooling + ref: v0.4.2 + path: .ci-tooling + - name: ๐Ÿ Setup Python + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: python-version: "3.11" enable-cache: false - - name: Extract version from tag - id: version - run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" - - name: Install from TestPyPI and verify (with retry) + - name: ๐Ÿ”– Resolve version + id: ver + env: + REF_NAME: ${{ github.ref_name }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: .ci-tooling/scripts/release/resolve-tag-version.sh >> "$GITHUB_OUTPUT" + - name: โœ… Install from TestPyPI and verify run: | - PKG="provide-testkit==${{ steps.version.outputs.version }}" - for attempt in 1 2 3; do - echo "Attempt $attempt: installing $PKG from TestPyPI..." - if pip install \ - --index-url https://test.pypi.org/simple/ \ - --extra-index-url https://pypi.org/simple/ \ - "$PKG"; then - echo "Install succeeded on attempt $attempt" - break - fi - if [ "$attempt" -lt 3 ]; then - echo "Install failed, waiting 30s for TestPyPI propagation..." - sleep 30 - else - echo "All 3 attempts failed" - exit 1 - fi - done - python -c "import provide.testkit; print('provide-testkit OK')" + .ci-tooling/scripts/release/verify-testpypi.sh provide-testkit "${{ steps.ver.outputs.version }}" "from importlib.metadata import version; import provide.testkit; v = version("provide-testkit")" + # PyPI publish via trusted publisher bound to this repo's release.yml + env `pypi`. publish-pypi: - needs: [build, verify-testpypi] - if: github.event_name == 'release' + needs: verify-testpypi runs-on: ubuntu-24.04 permissions: id-token: write @@ -96,37 +80,45 @@ jobs: name: pypi url: https://pypi.org/p/provide-testkit steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + - name: ๐Ÿ“ฆ Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: dist + name: release-artifacts path: dist - - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 + - name: ๐Ÿ“ค Publish to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1 + with: + skip-existing: true + # Generate SBOM, sign artifacts, attach everything to the GH release. sign-and-upload: - needs: build + needs: [publish-pypi] if: github.event_name == 'release' runs-on: ubuntu-24.04 permissions: contents: write id-token: write steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + - name: ๐Ÿ“ฅ Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: ๐Ÿ Setup uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: - name: dist - path: dist - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + python-version: "3.11" + - name: ๐Ÿ“ฆ Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: sbom-python - path: sbom - - uses: sigstore/gh-action-sigstore-python@04cffa1d795717b140764e8b640de88853c92acc # v3.3.0 + name: release-artifacts + path: dist + - name: ๐Ÿงพ Generate Python SBOM + run: uvx cyclonedx-py environment --output-format json -o sbom-python.cdx.json + - name: ๐Ÿ” Sigstore sign artifacts + uses: sigstore/gh-action-sigstore-python@f514d46b907ebcd5bedc05145c03b69c1edd8b46 # v3.0.0 with: inputs: dist/*.whl dist/*.tar.gz - - name: Upload artifacts and signatures to release + - name: โฌ†๏ธ Attach artifacts to release env: GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name }} run: | - gh release upload "${{ github.event.release.tag_name }}" \ - dist/*.whl dist/*.tar.gz dist/*.sigstore.json \ - sbom/sbom-python.cdx.json \ - --repo "$GITHUB_REPOSITORY" \ - --clobber + gh release upload "$TAG" dist/*.whl dist/*.tar.gz dist/*.sigstore.json sbom-python.cdx.json --repo "$GITHUB_REPOSITORY" --clobber diff --git a/src/provide/testkit/cli/testing.py b/src/provide/testkit/cli/testing.py index 2877de7d..fa847598 100644 --- a/src/provide/testkit/cli/testing.py +++ b/src/provide/testkit/cli/testing.py @@ -124,7 +124,7 @@ def temp_config_file( f.write(f"{key} = {value}\n") elif format == "yaml": try: - import yaml + import yaml # type: ignore[import-untyped] yaml.safe_dump(content, f) except ImportError as e: diff --git a/src/provide/testkit/file/temp.py b/src/provide/testkit/file/temp.py index ff80df0e..b5f56359 100644 --- a/src/provide/testkit/file/temp.py +++ b/src/provide/testkit/file/temp.py @@ -107,7 +107,7 @@ def create_yaml_file(self, data: Any, suffix: str = ".yaml", prefix: str = "test ImportError: If PyYAML is not available. """ try: - import yaml + import yaml # type: ignore[import-untyped] except ImportError as e: raise ImportError("PyYAML required for YAML file creation") from e diff --git a/src/provide/testkit/quality/profiling/profiler.py b/src/provide/testkit/quality/profiling/profiler.py index e260df44..74b87e97 100644 --- a/src/provide/testkit/quality/profiling/profiler.py +++ b/src/provide/testkit/quality/profiling/profiler.py @@ -25,7 +25,7 @@ MEMRAY_AVAILABLE = True except ImportError: MEMRAY_AVAILABLE = False - memray = None # type: ignore[assignment] + memray = None from ..base import QualityResult, QualityToolError