From 2019c9662744548e5b78bbc78cafabd3968aaddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20De=20Revi=C3=A8re?= Date: Sat, 28 Mar 2026 14:38:34 +0100 Subject: [PATCH] Add CI workflows and lock `ruff` in dev dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `lint.yml` (ruff format/lint, `pyproject.toml` validation, scriv fragment check), `tests.yml` (pytest matrix, Python 3.10–3.14), and `prepare-release.yml` (`scriv collect` → branch → PR). Switch `.ci/gate` from `uvx ruff` to `uv run --locked --group dev ruff` so local and CI paths use the same pinned version. Co-authored-by: AI --- .ci/gate | 4 +- .github/workflows/lint.yml | 46 ++++++++++ .github/workflows/prepare-release.yml | 117 ++++++++++++++++++++++++++ .github/workflows/tests.yml | 33 ++++++++ pyproject.toml | 4 + uv.lock | 27 ++++++ 6 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/prepare-release.yml create mode 100644 .github/workflows/tests.yml diff --git a/.ci/gate b/.ci/gate index 9944f08..6a2d005 100755 --- a/.ci/gate +++ b/.ci/gate @@ -6,8 +6,8 @@ set -euo pipefail # - Opt in with: GATE_REAL=1 gate # - Or pass custom args: GATE_PYTEST_ARGS='-m real' gate -uvx ruff format --check . -uvx ruff check . +uv run --locked --group dev ruff format --check . +uv run --locked --group dev ruff check . py_versions=(3.10 3.11 3.12 3.13 3.14) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..a536c1c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,46 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + name: Lint and format check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Run ruff formatter check + run: uv run --locked --group dev ruff format --check . + + - name: Run ruff linter + run: uv run --locked --group dev ruff check . + + - name: Validate pyproject.toml + run: uvx validate-pyproject pyproject.toml + + - name: Validate changelog fragments + run: | + set -euo pipefail + if uv run --locked --group dev scriv print; then + exit 0 + else + status=$? + if [ "$status" -eq 2 ]; then + echo "::notice::No changelog fragments to collect" + exit 0 + fi + exit "$status" + fi diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..b9040e7 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,117 @@ +name: Prepare Release + +on: + workflow_dispatch: + inputs: + version: + description: "Version to prepare (e.g., 1.0.0)" + required: true + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + prepare: + name: Collect changelog and prepare release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Collect changelog fragments + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + if uv run --locked --group dev scriv collect --version "$VERSION"; then + echo "Changelog fragments collected" + else + status=$? + if [ "$status" -eq 2 ]; then + echo "::notice::No changelog fragments to collect" + exit 0 + fi + exit "$status" + fi + + - name: Commit, push branch, and open PR + env: + BASE_BRANCH: ${{ github.ref_name }} + GH_TOKEN: ${{ github.token }} + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + + if git diff --quiet; then + echo "::notice::No changes to commit" + exit 0 + fi + + branch="release-prep/${BASE_BRANCH}/v${VERSION}" + title="Collect changelog for v${VERSION}" + body_file="$(mktemp)" + { + printf '%s\n' "## What changed" + printf '%s\n' "* Collect changelog fragments for \`v${VERSION}\`." + printf '\n' + printf '%s\n' "## Why" + printf '%s\n' "* Prepare the release changelog in a reviewable pull request." + printf '%s\n' "* Avoid pushing an unsigned automation commit to the protected" + printf '%s\n' " base branch." + printf '\n' + printf '%s\n' "## How to test" + printf '%s\n' "* Inspect the generated \`CHANGELOG.md\` update and removed fragments." + printf '%s\n' "* Run \`uv run --locked --group dev scriv collect --version ${VERSION}\`" + printf '%s\n' " locally to reproduce." + printf '\n' + printf '%s\n' "## Risk/comp notes" + printf '%s\n' "* Scope is limited to the generated \`CHANGELOG.md\` update and" + printf '%s\n' " removal of collected fragments." + printf '%s\n' "* Review the collected release notes before merge." + printf '\n' + printf '%s\n' "## Changelog fragment" + printf '%s\n' "* No. This PR consumes existing fragments for \`v${VERSION}\`." + } >"$body_file" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git fetch origin "$branch" || true + git switch -C "$branch" + git add -u CHANGELOG.md changelog.d/ + git commit -m "$title" + git push --force-with-lease --set-upstream origin "$branch" + + existing_pr_number="$(gh pr list \ + --repo "$GITHUB_REPOSITORY" \ + --base "$BASE_BRANCH" \ + --head "$branch" \ + --state open \ + --json number \ + --jq '.[0].number // empty')" + + if [ -n "$existing_pr_number" ]; then + gh pr edit "$existing_pr_number" \ + --repo "$GITHUB_REPOSITORY" \ + --title "$title" \ + --body-file "$body_file" + exit 0 + fi + + gh pr create \ + --repo "$GITHUB_REPOSITORY" \ + --base "$BASE_BRANCH" \ + --head "$branch" \ + --title "$title" \ + --body-file "$body_file" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3b66d66 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + tests: + name: Run tests (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - name: Install dependencies + run: uv sync --locked --group dev + + - name: Run pytest + run: uv run --locked --group dev pytest -q tests/ -m "not real" diff --git a/pyproject.toml b/pyproject.toml index 0afbf7e..502f7b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ Repository = "https://github.com/sderev/shellgenius" [dependency-groups] dev = [ "pytest>=8,<9", + "ruff==0.15.8", "scriv[toml]>=1.8,<2", ] @@ -48,6 +49,9 @@ markers = [ ] testpaths = ["tests"] +[tool.ruff] +line-length = 100 + [tool.scriv] changelog = "CHANGELOG.md" format = "md" diff --git a/uv.lock b/uv.lock index 695ecfc..702c4e7 100644 --- a/uv.lock +++ b/uv.lock @@ -818,6 +818,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] +[[package]] +name = "ruff" +version = "0.15.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, + { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, + { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, +] + [[package]] name = "scriv" version = "1.8.0" @@ -854,6 +879,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "pytest" }, + { name = "ruff" }, { name = "scriv", extra = ["toml"] }, ] @@ -868,6 +894,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "pytest", specifier = ">=8,<9" }, + { name = "ruff", specifier = "==0.15.8" }, { name = "scriv", extras = ["toml"], specifier = ">=1.8,<2" }, ]