-
Notifications
You must be signed in to change notification settings - Fork 0
ci: add GitHub Actions workflow and fix packaging metadata #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [master] | ||
| pull_request: | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| # Least privilege: nothing here needs to write to the repo. Declaring this at | ||
| # the workflow level also overrides a permissive repo/org default. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| env: | ||
| # Assert uv.lock is up to date with pyproject.toml and install exactly what | ||
| # it pins, rather than silently resolving something different from what | ||
| # developers run locally. (UV_LOCKED, not UV_FROZEN: --frozen skips | ||
| # re-locking without checking, --locked fails when the lock is stale.) | ||
| UV_LOCKED: "1" | ||
|
|
||
| jobs: | ||
| lint: | ||
| name: Lint and type-check | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| with: | ||
| # Don't leave GITHUB_TOKEN in .git/config for later steps to reach. | ||
| persist-credentials: false | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v5 | ||
| with: | ||
| enable-cache: true | ||
|
|
||
| - name: Install dependencies | ||
| run: uv sync --all-extras --dev | ||
|
|
||
| - name: ruff check | ||
| run: uv run ruff check . | ||
|
|
||
| - name: ruff format --check | ||
| run: uv run ruff format --check . | ||
|
|
||
| - name: mypy | ||
| run: uv run mypy src/myinvois | ||
|
|
||
| test: | ||
| name: Test (Python ${{ matrix.python-version }}) | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| python-version: ["3.11", "3.12", "3.13"] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| # Don't leave GITHUB_TOKEN in .git/config for later steps to reach. | ||
| persist-credentials: false | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v5 | ||
| with: | ||
| enable-cache: true | ||
| python-version: ${{ matrix.python-version }} | ||
|
|
||
| - name: Install dependencies | ||
| run: uv sync --all-extras --dev | ||
|
|
||
| # Live tests need real MyInvois sandbox credentials, so they are | ||
| # deselected here. They skip on their own if MYINVOIS_CLIENT_ID is | ||
| # unset, but deselecting keeps the CI summary honest about what ran. | ||
| - name: pytest | ||
| run: uv run pytest -m "not live" | ||
|
|
||
| package: | ||
| name: Build and verify distributions | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| # Don't leave GITHUB_TOKEN in .git/config for later steps to reach. | ||
| persist-credentials: false | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v5 | ||
| with: | ||
| enable-cache: true | ||
|
|
||
| - name: Build sdist and wheel | ||
| run: uv build | ||
|
|
||
| # Guards two things that are easy to break and painful to un-publish: | ||
| # 1. The PEP 561 marker and the JSON code tables must ship, or | ||
| # `from myinvois.codes import ...` fails at runtime in a wheel | ||
| # install and type-checkers ignore the package. | ||
| # 2. The test signing key/cert must NEVER ship. They are force-tracked | ||
| # in git for the byte-parity tests (see tests/fixtures/cert/README.md) | ||
| # which makes an accidental include plausible. | ||
| - name: Verify distribution contents | ||
| run: uv run --no-project python scripts/check_dist.py | ||
|
|
||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: distributions | ||
| path: dist/ | ||
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| """Verify the built sdist and wheel before they are published. | ||
|
|
||
| Run from the repo root after `uv build`: | ||
|
|
||
| uv run --no-project python scripts/check_dist.py | ||
|
|
||
| Two classes of mistake are checked, both of which are unrecoverable once a | ||
| release is on PyPI (a version number can never be reused): | ||
|
|
||
| 1. **Missing runtime data.** `py.typed` and `codes/_data/*.json` are not | ||
| importable Python modules, so they only ship because of the explicit | ||
| `[tool.uv.build-backend] data-includes` entry. If that entry regresses, the | ||
| package still builds and imports, but `myinvois.codes` raises at runtime on | ||
| a wheel install and type-checkers silently ignore the package. | ||
|
|
||
| 2. **Leaked signing material.** `tests/fixtures/cert/` holds a dummy private | ||
| key and certificate that are deliberately force-tracked in git so the | ||
| byte-parity tests can pin against fixtures signed with them. They are | ||
| harmless (self-signed, test-only) but shipping a file named | ||
| `*_key.pem` inside a published distribution is the kind of thing that | ||
| triggers secret scanners and erodes trust, so it is a hard failure here. | ||
|
|
||
| Exits non-zero with a description of every problem found. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import re | ||
| import sys | ||
| import tarfile | ||
| import zipfile | ||
| from pathlib import Path | ||
|
|
||
| DIST = Path(__file__).resolve().parents[1] / "dist" | ||
|
|
||
| # Non-Python files that must ship, as paths relative to the package root. | ||
| REQUIRED_DATA_MEMBERS = ( | ||
| "myinvois/py.typed", | ||
| "myinvois/codes/_data/classification.json", | ||
| "myinvois/codes/_data/countries.json", | ||
| "myinvois/codes/_data/currencies.json", | ||
| "myinvois/codes/_data/msic.json", | ||
| "myinvois/codes/_data/payment_means.json", | ||
| "myinvois/codes/_data/states.json", | ||
| "myinvois/codes/_data/taxes.json", | ||
| "myinvois/codes/_data/units.json", | ||
| ) | ||
|
|
||
| # The same files live at different prefixes in each distribution: the wheel is | ||
| # already package-rooted, while the sdist keeps the repo's `src/` layout. Both | ||
| # are checked -- `pip install --no-binary` builds from the sdist, so a | ||
| # `data-includes` regression there breaks installs just as badly, and would go | ||
| # unnoticed if only the wheel were validated. | ||
| REQUIRED_MEMBER_PREFIXES = {"wheel": "", "sdist": "src/"} | ||
|
|
||
| # Anything matching these must NOT appear in either distribution. Keyed on the | ||
| # path so a source module such as `ubl/signing/_cert.py` (legitimate) is not | ||
| # confused with an actual PEM payload. | ||
| FORBIDDEN_PATTERNS = ( | ||
| re.compile(r"\.(pem|key|p12|pfx)$", re.IGNORECASE), | ||
| re.compile(r"(^|/)tests?/", re.IGNORECASE), | ||
| re.compile(r"(^|/)fixtures?/", re.IGNORECASE), | ||
| ) | ||
|
|
||
|
|
||
| def _wheel_and_sdist() -> tuple[Path, Path]: | ||
| wheels = sorted(DIST.glob("*.whl")) | ||
| sdists = sorted(DIST.glob("*.tar.gz")) | ||
| if len(wheels) != 1 or len(sdists) != 1: | ||
| sys.exit( | ||
| f"expected exactly one wheel and one sdist in {DIST}, " | ||
| f"found {len(wheels)} wheel(s) and {len(sdists)} sdist(s). " | ||
| "Remove stale builds and re-run `uv build`." | ||
| ) | ||
| return wheels[0], sdists[0] | ||
|
|
||
|
|
||
| def _wheel_members(wheel: Path) -> list[str]: | ||
| with zipfile.ZipFile(wheel) as zf: | ||
| return zf.namelist() | ||
|
|
||
|
|
||
| def _sdist_members(sdist: Path) -> list[str]: | ||
| with tarfile.open(sdist) as tf: | ||
| # Strip the leading `myinvois-<version>/` directory so the paths line | ||
| # up with how they are written in the repo. | ||
| return [name.partition("/")[2] for name in tf.getnames()] | ||
|
|
||
|
|
||
| def main() -> int: | ||
| if not DIST.is_dir(): | ||
| sys.exit(f"{DIST} does not exist -- run `uv build` first.") | ||
|
|
||
| wheel, sdist = _wheel_and_sdist() | ||
| wheel_members = _wheel_members(wheel) | ||
| sdist_members = _sdist_members(sdist) | ||
|
|
||
| problems: list[str] = [] | ||
|
|
||
| for kind, label, members in ( | ||
| ("wheel", wheel.name, wheel_members), | ||
| ("sdist", sdist.name, sdist_members), | ||
| ): | ||
| prefix = REQUIRED_MEMBER_PREFIXES[kind] | ||
| present = set(members) | ||
| for member in REQUIRED_DATA_MEMBERS: | ||
| if f"{prefix}{member}" not in present: | ||
| problems.append( | ||
| f"{label}: missing required member {prefix + member!r} -- check the " | ||
| "`[tool.uv.build-backend] data-includes` entry in pyproject.toml" | ||
| ) | ||
|
|
||
| for label, members in ((wheel.name, wheel_members), (sdist.name, sdist_members)): | ||
| for member in members: | ||
| if not member or member.endswith("/"): | ||
| continue | ||
| for pattern in FORBIDDEN_PATTERNS: | ||
| if pattern.search(member): | ||
| problems.append( | ||
| f"{label}: must not ship {member!r} (matched {pattern.pattern})" | ||
| ) | ||
| break | ||
|
|
||
| if problems: | ||
| print(f"Distribution check FAILED ({len(problems)} problem(s)):", file=sys.stderr) | ||
| for problem in problems: | ||
| print(f" - {problem}", file=sys.stderr) | ||
| return 1 | ||
|
|
||
| scanned = len(wheel_members) + len(sdist_members) | ||
| print(f"Distribution check passed: {wheel.name}, {sdist.name}") | ||
| print(f" {len(REQUIRED_DATA_MEMBERS)} required members present in wheel and sdist") | ||
| print(f" no test fixtures or key material in {scanned} entries") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) |
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
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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.