Merge pull request #19 from ecrum19/release/v3.1.0 #14
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: publish-python | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: "Existing Git tag to validate, for example v1.2.4. Manual runs never publish to PyPI." | |
| required: true | |
| type: string | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| # Pin pip instead of taking whatever released most recently. Every job used to | |
| # run `pip install --upgrade pip`, so each run resolved dependencies with a | |
| # different, unannounced resolver: CI was non-reproducible by construction, | |
| # and a pip release could break the build with no change in this repository. | |
| # | |
| # This does NOT prevent transient index failures, and should not be mistaken | |
| # for a fix for one. A case is on record: the pull_request run for PR #14 | |
| # failed with `no matching distributions available for your environment: | |
| # mdurl` while the identical commit had passed 15 minutes earlier on push, | |
| # passed on two other platforms in that same run, and passed again on re-run | |
| # with no code change. mdurl is a pure-Python py3-none-any wheel, so nothing | |
| # about the environment made it uninstallable -- that was the index, not the | |
| # resolver. The right response there is a re-run. The right response to an | |
| # unpinned toolchain is this pin. | |
| # | |
| # Bump deliberately, as its own commit, so a resolver change lands where it | |
| # can be attributed instead of appearing inside an unrelated PR. | |
| PIP_VERSION: "26.2.1" | |
| jobs: | |
| build: | |
| name: build-sdist-wheel | |
| runs-on: ubuntu-latest | |
| env: | |
| RELEASE_TAG: ${{ github.event.inputs.release_tag || github.ref_name }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.inputs.release_tag || github.ref }} | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Validate committed release metadata | |
| run: python scripts/release.py --check-tag "${RELEASE_TAG}" | |
| - name: Build package | |
| run: | | |
| python -m pip install "pip==${{ env.PIP_VERSION }}" build twine | |
| rm -rf dist build *.egg-info | |
| python -m build | |
| python -m twine check dist/* | |
| - name: Verify built artifact versions | |
| run: | | |
| python - <<'PY' | |
| from pathlib import Path | |
| import os | |
| import tomllib | |
| tag = os.environ["RELEASE_TAG"].strip() | |
| if not tag.startswith("v"): | |
| raise SystemExit(f"Release tag must start with 'v': {tag}") | |
| version = tag[1:] | |
| pyproject_version = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]["version"] | |
| if pyproject_version != version: | |
| raise SystemExit( | |
| f"pyproject.toml version {pyproject_version} does not match release tag version {version}" | |
| ) | |
| dist = sorted(Path("dist").glob("*")) | |
| if not dist: | |
| raise SystemExit("No build artifacts found in dist/") | |
| names = [path.name for path in dist] | |
| expected = { | |
| f"vcf_rdfizer-{version}.tar.gz", | |
| f"vcf_rdfizer-{version}-py3-none-any.whl", | |
| } | |
| missing = sorted(expected.difference(names)) | |
| if missing: | |
| raise SystemExit( | |
| f"Built artifacts do not match project version {version}; missing: {', '.join(missing)}" | |
| ) | |
| unexpected = sorted( | |
| name for name in names if name.startswith("vcf_rdfizer-") and f"vcf_rdfizer-{version}" not in name | |
| ) | |
| if unexpected: | |
| raise SystemExit( | |
| f"Found stale artifacts in dist/: {', '.join(unexpected)}" | |
| ) | |
| print("Verified build artifacts:") | |
| for name in names: | |
| print(f" {name}") | |
| PY | |
| - name: Smoke-test installed wheel | |
| run: | | |
| python -m venv /tmp/vcf-rdfizer-wheel-test | |
| /tmp/vcf-rdfizer-wheel-test/bin/python -m pip install --no-deps dist/*.whl | |
| ( | |
| cd /tmp | |
| /tmp/vcf-rdfizer-wheel-test/bin/vcf-rdfizer --help | |
| /tmp/vcf-rdfizer-wheel-test/bin/python - <<'PY' | |
| import importlib.resources | |
| from pathlib import Path | |
| import vcf_rdfizer | |
| rules = importlib.resources.files("vcf_rdfizer_data").joinpath("rules/default_rules.ttl") | |
| if not rules.is_file(): | |
| raise SystemExit("Packaged default rules are missing") | |
| if not vcf_rdfizer.resolve_default_rules_path(Path(vcf_rdfizer.__file__).resolve().parent).is_file(): | |
| raise SystemExit("Installed wrapper cannot resolve packaged default rules") | |
| PY | |
| ) | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: python-dist | |
| path: dist/* | |
| publish: | |
| name: publish-to-pypi | |
| runs-on: ubuntu-latest | |
| needs: build | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| environment: | |
| name: pypi | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Download artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: python-dist | |
| path: dist | |
| # PyPI reserves a version permanently once it is uploaded, so re-pushing a | |
| # release tag -- to correct a Dockerfile, a workflow, or anything else | |
| # outside the Python artifact -- used to fail this job on the duplicate. | |
| # Skipping what is already published makes a tag re-push idempotent: the | |
| # Docker and release jobs re-run, and PyPI is left exactly as it was. | |
| - name: Publish package | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| skip-existing: true |