feat(packaging): a double-clickable macOS app, and a release job that cannot ship it unsigned #517
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: CI | |
| # ON EVERY MERGE, and on demand. A push to `main` is what a merge looks like from Actions' side, | |
| # so every merged PR now re-runs lint, the suite, and the build-identity check against the tree | |
| # that actually resulted from the merge. | |
| # | |
| # That last phrase is the point, and it is why this is NOT the redundant run it was once called. | |
| # A `pull_request` run tests the PR head; a `push` run tests `main` after the merge landed. Those | |
| # differ whenever two independently-green branches conflict semantically -- each passes alone, the | |
| # merge of them does not. Only the post-merge run can see that. | |
| # | |
| # `pull_request` is here for a specific, load-bearing reason: it is what makes this job report a | |
| # status named `test` on a PR, and the `main` ruleset REQUIRES that context before a merge is | |
| # allowed (issue #236). The two are a matched pair and must be changed together -- removing this | |
| # trigger while the ruleset still requires `test` would leave every PR waiting forever on a check | |
| # that can never report, which is precisely the broken state #236 was filed to fix. That is not a | |
| # hypothetical: the ruleset previously required "Lint, Unit tests, Build" and "Integration Tests", | |
| # names no job here has ever produced. | |
| # | |
| # The context string is `test` because the job below is `test:` and declares no `name:`. Renaming | |
| # that job, or giving it a `name:`, silently changes the context and breaks the ruleset the same | |
| # way. If you rename it, update the ruleset in the same change. | |
| # | |
| # So the two triggers do different jobs: `pull_request` GATES the merge, `push` VERIFIES the result | |
| # of it. `release.yml` remains the gate that matters for money -- it re-runs `ruff` + `mypy` + `pytest` | |
| # itself before it will build an artifact, so a red `main` cannot become a wheel, a tag, or a | |
| # release asset even if both of these were removed. | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| # `github.ref` is `refs/pull/<n>/merge` for PR runs, `refs/heads/main` for the post-merge push, and | |
| # `refs/heads/<branch>` for a dispatch -- distinct keys, so a PR run never cancels the post-merge | |
| # run. Pushes to the same PR do cancel each other, and back-to-back merges cancel the older `main` | |
| # run -- correct in both cases: only the newest result matters. | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # Two legs, the two ends of `requires-python = ">=3.11"` (#283): | |
| # - "3.11" is the FLOOR, stated explicitly because nothing else states it: the suite | |
| # passing there is what keeps the declared floor a measured claim instead of a hope. | |
| # 3.10 fails collection on `typing.assert_never` (3.11+), which is the one concrete | |
| # feature binding the floor. | |
| # - "3.14" is the leg local development actually runs (`.python-version` pins 3.14.4); | |
| # it is named here by major only, since the matrix needs SOME explicit version and | |
| # "whatever .python-version says" is not addressable per-leg. | |
| # Both legs stay inside THIS job so the required status context remains `test` -- the | |
| # matrix fans out the context, it does not rename it, so every leg gates the merge. | |
| strategy: | |
| matrix: | |
| python: ["3.11", "3.14"] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| enable-cache: true | |
| - name: Set up Python | |
| run: uv python install "${{ matrix.python }}" | |
| - name: Sync dependencies | |
| run: uv sync --all-extras --dev --python "${{ matrix.python }}" | |
| - name: Lint | |
| run: uv run --python "${{ matrix.python }}" ruff check keel tests packages | |
| # A STEP in this job, deliberately not a job of its own. The `main` ruleset requires the | |
| # status context `test`, which this job produces; a separate `typecheck:` job would report | |
| # a context nothing requires, so a red mypy would not block a merge -- the same failure | |
| # shape the header above describes for a renamed job. As a step it inherits that gate. | |
| # | |
| # Paths come from `[tool.mypy]`'s `files` in pyproject.toml and are deliberately NOT | |
| # repeated here: `keel.*` came off the `ignore_errors` list in #266, and the point of this | |
| # step is that it cannot drift back silently. A package moving in or out of the checked | |
| # set is then one edit there, not one edit mirrored across two workflows. | |
| - name: Type-check | |
| run: uv run --python "${{ matrix.python }}" mypy | |
| - name: Test | |
| run: uv run --python "${{ matrix.python }}" pytest -q | |
| # A build that cannot identify itself must not reach a release. This also catches an | |
| # import-time break in the CLI, which `pytest` alone would not surface as sharply. | |
| - name: Build identity | |
| run: uv run --python "${{ matrix.python }}" keel --version |