Skip to content

feat(packaging): a double-clickable macOS app, and a release job that cannot ship it unsigned #517

feat(packaging): a double-clickable macOS app, and a release job that cannot ship it unsigned

feat(packaging): a double-clickable macOS app, and a release job that cannot ship it unsigned #517

Workflow file for this run

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