Compare commits

..
36 Commits
Author SHA1 Message Date
Kevin StillhammerandGitHub 278981a28c Add option to skip Astral mirror downloads (#387)
## Summary
- add `download-from-astral-mirror` input defaulting to `true`
- skip mirror URL rewriting and download directly from GitHub Releases
when disabled
- document the input and add unit coverage

Fixes #384

Refs: pi-session 019f31bc-bd1c-7276-9b4e-9734fa0aa367

## Testing
- npm run build
- npm run check
- npm run test:unit
2026-07-05 10:17:44 +00:00
Kevin StillhammerandGitHub c35f46c92b chore: roll up Dependabot updates (#386)
## Summary
- roll up remaining open Dependabot npm updates for @octokit/core and
@octokit/plugin-rest-endpoint-methods
- roll up remaining GitHub Actions pin updates for CodeQL, zizmor,
release-drafter, setup-node, and create-pull-request
- regenerate bundled dist files

## Validation
- npm run all

Refs: pi-session 019f31b8-93d9-7f9b-b3e9-5aaac6bab959
2026-07-05 12:04:11 +02:00
somazandGitHub f3db229c84 feat: support uv.lock as version-file (#379)
Adds `uv.lock` as a supported `version-file` format, alongside the
existing
`pyproject.toml` and `requirements.txt`. A `uv.lock` pins the exact
resolved
ruff version, so reading it gives a reproducible install that matches
the
project's lockfile — the single source of truth — instead of the
lower-bound /
range typically declared in `pyproject.toml`.

The parser reads the `[[package]]` entry whose `name` is `ruff` and
returns its
`version`. If ruff is absent or the file cannot be parsed, the action
warns and
falls back to `latest` (same behaviour as the other formats).

Validation:
- `npm run all` — tsc typecheck clean, biome clean, dist rebuilt, jest
65/65 pass
- Added unit tests (`__tests__/version/file-parser.test.ts`) + a
`uv.lock` fixture
- Added an integration job (`test-default-version-from-uv-lock`) that
runs the
  action against the fixture and asserts ruff 0.9.5 is installed

closes #375
2026-07-05 11:56:05 +02:00
Kevin StillhammerandGitHub 270e80dfe7 Fix wildcard src input (#368)
Fixes: #355
2026-07-05 11:55:02 +02:00
Kevin StillhammerandGitHub 5fedeebdb3 Add Dependabot rollup skill (#385)
## Summary
- add the dependabot-pr-rollup skill copied from setup-uv
- adapt the workflow notes for ruff-action's npm and GitHub Actions
Dependabot updates

## Testing
- npm run all

Refs: pi-session 019f317d-08f2-7aac-a6d5-56a44288d94c
2026-07-05 11:49:16 +02:00
99fe79f472 chore: update known checksums for 0.15.20 (#383)
chore: update known checksums for 0.15.20

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-06-26 08:39:54 +02:00
32ad1b993e chore: update known checksums for 0.15.19 (#382)
chore: update known checksums for 0.15.19

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-06-24 07:16:05 +00:00
f78e087041 chore: update known checksums for 0.15.18 (#381)
chore: update known checksums for 0.15.18

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-06-19 05:45:18 +00:00
2f88679623 chore: update known checksums for 0.15.17 (#380)
chore: update known checksums for 0.15.17

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-06-12 05:57:40 +00:00
b79a7ac759 chore: update known checksums for 0.15.16 (#378)
chore: update known checksums for 0.15.16

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-06-05 06:27:01 +00:00
30c0929868 chore: update known checksums for 0.15.15 (#377)
chore: update known checksums for 0.15.15

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-05-29 09:11:56 +00:00
b4bf7655a0 chore: update known checksums for 0.15.14 (#376)
chore: update known checksums for 0.15.14

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-05-22 05:57:48 +00:00
Frederick WagnerandGitHub 7f78e88b21 Use v4.0.0 in README.md (#371)
This change should reduce confusion from `@4` not working.
2026-05-20 13:18:52 +02:00
8c228c72ff chore: update known checksums for 0.15.13 (#372)
chore: update known checksums for 0.15.13

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-05-15 06:36:00 +00:00
Zanie BlueandGitHub 248b6d6da3 Update the release process to match setup-uv (#364) 2026-04-27 10:31:05 -05:00
ecaa98070e chore: update known checksums for 0.15.12 (#365)
chore: update known checksums for 0.15.12

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-04-25 07:00:01 +00:00
a9cfed68e4 chore: update known checksums for 0.15.11 (#360)
chore: update known checksums for 0.15.11

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-04-17 07:31:01 +00:00
Kevin StillhammerandGitHub eb44a0e99b Draft commitish releases (#359) 2026-04-16 20:31:36 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2915b93f0d Bump zizmorcore/zizmor-action from 0.5.2 to 0.5.3 (#356)
Bumps
[zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action)
from 0.5.2 to 0.5.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/zizmorcore/zizmor-action/releases">zizmorcore/zizmor-action's
releases</a>.</em></p>
<blockquote>
<h2>v0.5.3</h2>
<ul>
<li><code>1.24.0</code> and <code>1.24.1</code> are now available via
the action</li>
<li><code>1.24.1</code> is now the default version of zizmor used by the
action</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/b1d7e1fb5de872772f31590499237e7cce841e8e"><code>b1d7e1f</code></a>
Sync zizmor versions (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/102">#102</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/a195b57475917ddcb70845e5ffe1c3a15dbbdedc"><code>a195b57</code></a>
Sync zizmor versions (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/100">#100</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/629d5d01fe5939a6aeae25c1bd1acd2cfa28e9b2"><code>629d5d0</code></a>
chore(deps): bump github/codeql-action in the github-actions group (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/99">#99</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/453d591467e8199b1d5c6883b6ec5c22a12aac72"><code>453d591</code></a>
chore(deps): bump the github-actions group with 2 updates (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/98">#98</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/ea2c18b942410df0b22bed3b94c361c407518d45"><code>ea2c18b</code></a>
Bump pins (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/97">#97</a>)</li>
<li>See full diff in <a
href="https://github.com/zizmorcore/zizmor-action/compare/71321a20a9ded102f6e9ce5718a2fcec2c4f70d8...b1d7e1fb5de872772f31590499237e7cce841e8e">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=zizmorcore/zizmor-action&package-manager=github_actions&previous-version=0.5.2&new-version=0.5.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-14 07:51:55 +02:00
Kevin StillhammerandGitHub 0ce1b0bf8b refactor version resolving (#353) 2026-04-12 13:44:40 +02:00
Kevin StillhammerandGitHub 9b8caf6c41 Add manifest-file input (#352) 2026-04-11 19:14:05 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
535554df96 Bump release-drafter/release-drafter from 6.2.0 to 7.2.0 (#350)
Bumps
[release-drafter/release-drafter](https://github.com/release-drafter/release-drafter)
from 6.2.0 to 7.2.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/release-drafter/release-drafter/releases">release-drafter/release-drafter's
releases</a>.</em></p>
<blockquote>
<h2>v7.2.0</h2>
<h1>What's Changed</h1>
<h2>New</h2>
<ul>
<li>feat: allow always collapsing a category (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1444">#1444</a>)
<a href="https://github.com/mhanberg"><code>@​mhanberg</code></a></li>
</ul>
<h2>Bug Fixes</h2>
<ul>
<li>fix: improve advanced substitutions in replacers (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1555">#1555</a>)
<a href="https://github.com/jetersen"><code>@​jetersen</code></a></li>
<li>fix: support repo-only _extends and prevent .github/ path doubling
(<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1577">#1577</a>)
<a href="https://github.com/jetersen"><code>@​jetersen</code></a></li>
</ul>
<h2>Maintenance</h2>
<ul>
<li>chore(deps): update dependency typescript to 6.0.2 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1587">#1587</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>chore(deps): update vitest to 4.1.4 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1585">#1585</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>ci(deps): update peter-evans/create-pull-request action to v8 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1588">#1588</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>chore(deps): update dependency vite to 8.0.5 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1579">#1579</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>chore(deps): update dependency nock to 14.0.12 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1583">#1583</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>chore(deps): update dependency <code>@​types/node</code> to 24.12.2
(<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1582">#1582</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>chore(deps): update dependency <code>@​biomejs/biome</code> to
2.4.10 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1581">#1581</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>chore: move codegen to monthly scheduled workflow (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1578">#1578</a>)
<a href="https://github.com/jetersen"><code>@​jetersen</code></a></li>
<li>chore: replace vite-tsconfig-paths plugin with native
resolve.tsconfigPaths (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1571">#1571</a>)
<a href="https://github.com/jetersen"><code>@​jetersen</code></a></li>
</ul>
<h2>Documentation</h2>
<ul>
<li>docs: fix autolabeler example tag (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1568">#1568</a>)
<a href="https://github.com/cchanche"><code>@​cchanche</code></a></li>
</ul>
<h2>Dependency Updates</h2>
<ul>
<li>build(deps): bump lodash and
<code>@​graphql-codegen/plugin-helpers</code> (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1589">#1589</a>)
@<a href="https://github.com/apps/dependabot">dependabot[bot]</a></li>
<li>fix(deps): update dependency <code>@​actions/github</code> to 9.1.0
(<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1586">#1586</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>chore(deps): update dependency yaml to 2.8.3 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1580">#1580</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>chore(deps): update node.js to v24.14.1 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1584">#1584</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
<li>chore(deps): update dependency <code>@​biomejs/biome</code> to
2.4.10 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1581">#1581</a>)
@<a href="https://github.com/apps/renovate">renovate[bot]</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/release-drafter/release-drafter/compare/v7.1.1...v7.2.0">https://github.com/release-drafter/release-drafter/compare/v7.1.1...v7.2.0</a></p>
<h2>v7.1.1</h2>
<h1>What's Changed</h1>
<h2>Bug Fixes</h2>
<ul>
<li>fix: remove disable-releaser and disable-autolabeler from
action.yaml (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1564">#1564</a>)
<a href="https://github.com/cchanche"><code>@​cchanche</code></a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/release-drafter/release-drafter/compare/v7.1.0...v7.1.1">https://github.com/release-drafter/release-drafter/compare/v7.1.0...v7.1.1</a></p>
<h2>v7.1.0</h2>
<h1>What's Changed</h1>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/5de93583980a40bd78603b6dfdcda5b4df377b32"><code>5de9358</code></a>
7.2.0</li>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/e50d61c7deb94fc176ad7d31d7b71f60307829b2"><code>e50d61c</code></a>
chore: rebuild dist</li>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/d3a61d3b778db0d18c3511a1d8a5585188fdb99f"><code>d3a61d3</code></a>
chore: fix npm audit vulnerabilities</li>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/8bfa2791ec73890e3087b933c9db62d0a294a461"><code>8bfa279</code></a>
build(deps): bump lodash and
<code>@​graphql-codegen/plugin-helpers</code> (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1589">#1589</a>)</li>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/c2a8a67ac931b548feeee49fe78975bd87720a0e"><code>c2a8a67</code></a>
chore: remove engine-strict from .npmrc to fix Dependabot
resolution</li>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/e51e4adf1695870d57ae9cf3fa8cc37064d6304d"><code>e51e4ad</code></a>
chore(deps): update dependency typescript to 6.0.2 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1587">#1587</a>)</li>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/0e7bd548468b9ce7f0b082417f6ec32bc47173ae"><code>0e7bd54</code></a>
fix(deps): update dependency <code>@​actions/github</code> to 9.1.0 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1586">#1586</a>)</li>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/9c0b0a8cf19d3415f835a04b1987cd2451aaac85"><code>9c0b0a8</code></a>
chore(deps): update dependency yaml to 2.8.3 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1580">#1580</a>)</li>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/b27f820cbc98c923f216e773d35bc7f4e8efd9ed"><code>b27f820</code></a>
chore(deps): update vitest to 4.1.4 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1585">#1585</a>)</li>
<li><a
href="https://github.com/release-drafter/release-drafter/commit/eb9053430f473e03512e92caee9608b0db01ebd7"><code>eb90534</code></a>
ci(deps): update peter-evans/create-pull-request action to v8 (<a
href="https://redirect.github.com/release-drafter/release-drafter/issues/1588">#1588</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/release-drafter/release-drafter/compare/6db134d15f3909ccc9eefd369f02bd1e9cffdf97...5de93583980a40bd78603b6dfdcda5b4df377b32">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=release-drafter/release-drafter&package-manager=github_actions&previous-version=6.2.0&new-version=7.2.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-11 11:41:10 +02:00
2186c6ecae chore: update known checksums for 0.15.10 (#351)
chore: update known checksums for 0.15.10

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-04-10 11:34:56 +00:00
Zanie BlueandGitHub 26892dbe43 Add a release workflow (#349)
Mirroring https://github.com/astral-sh/setup-uv/pull/839
2026-04-09 14:10:29 +02:00
d7f6ad639a chore: update known checksums for 0.15.9 (#348)
chore: update known checksums for 0.15.9

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-04-03 07:00:21 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5b5935861b Bump eifinger/actionlint-action from 1.10.0 to 1.10.2 (#347)
Bumps
[eifinger/actionlint-action](https://github.com/eifinger/actionlint-action)
from 1.10.0 to 1.10.2.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/eifinger/actionlint-action/commit/1fc89649be682d16ec5cf65ea16e269eb88d3982"><code>1fc8964</code></a>
build(deps): bump actions/checkout from 6.0.1 to 6.0.2 (<a
href="https://redirect.github.com/eifinger/actionlint-action/issues/34">#34</a>)</li>
<li><a
href="https://github.com/eifinger/actionlint-action/commit/77c97feb783dfe1768d10f9dda219af0936f44a7"><code>77c97fe</code></a>
build(deps): bump release-drafter/release-drafter from 6.2.0 to 7.1.1
(<a
href="https://redirect.github.com/eifinger/actionlint-action/issues/40">#40</a>)</li>
<li><a
href="https://github.com/eifinger/actionlint-action/commit/bc5a34046e0b08d672f8a517047621ce16320c2b"><code>bc5a340</code></a>
chore: bump actionlint to 1.7.12 (<a
href="https://redirect.github.com/eifinger/actionlint-action/issues/41">#41</a>)</li>
<li><a
href="https://github.com/eifinger/actionlint-action/commit/7802e0cc3ab3f81cbffb36fb0bf1a3621d994b89"><code>7802e0c</code></a>
Remove oracle-aarch64 from workflows (<a
href="https://redirect.github.com/eifinger/actionlint-action/issues/36">#36</a>)</li>
<li><a
href="https://github.com/eifinger/actionlint-action/commit/8d9ad94ef799fdd386147ebbed32e940280f3dcd"><code>8d9ad94</code></a>
set default actionlint version to 1.7.11 (<a
href="https://redirect.github.com/eifinger/actionlint-action/issues/35">#35</a>)</li>
<li><a
href="https://github.com/eifinger/actionlint-action/commit/4863b27ac4c7ab9e9c69405b36b49b7b4d97ead9"><code>4863b27</code></a>
build(deps): bump release-drafter/release-drafter from 6.1.0 to 6.2.0
(<a
href="https://redirect.github.com/eifinger/actionlint-action/issues/33">#33</a>)</li>
<li>See full diff in <a
href="https://github.com/eifinger/actionlint-action/compare/447fbfe7533062b7a9ea55f790f2396fba6d052a...1fc89649be682d16ec5cf65ea16e269eb88d3982">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=eifinger/actionlint-action&package-manager=github_actions&previous-version=1.10.0&new-version=1.10.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 07:39:51 +02:00
Kevin StillhammerandGitHub 0be154b683 Migrate to ESMBundler and node 24 (#345) 2026-03-28 15:52:59 +00:00
f611dfc122 chore: update known checksums for 0.15.8 (#344)
chore: update known checksums for 0.15.8

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-03-27 06:13:56 +00:00
d40baf4d10 chore: update known checksums for 0.15.7 (#342)
chore: update known checksums for 0.15.7

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-03-20 06:47:13 +00:00
18ddc929c7 chore: update known checksums for 0.15.6 (#337)
chore: update known checksums for 0.15.6

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-03-13 07:28:50 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
25445a5bce Bump zizmorcore/zizmor-action from 0.4.1 to 0.5.2 (#333)
Bumps
[zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action)
from 0.4.1 to 0.5.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/zizmorcore/zizmor-action/releases">zizmorcore/zizmor-action's
releases</a>.</em></p>
<blockquote>
<h2>v0.5.2</h2>
<h2>What's Changed</h2>
<ul>
<li>zizmor 1.23.1 is now the default used by this action.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/zizmorcore/zizmor-action/compare/v0.5.1...v0.5.2">https://github.com/zizmorcore/zizmor-action/compare/v0.5.1...v0.5.2</a></p>
<h2>v0.5.1</h2>
<h2>What's Changed</h2>
<ul>
<li>zizmor 1.23.0 is now the default used by this action.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/zizmorcore/zizmor-action/compare/v0.5.0...v0.5.1">https://github.com/zizmorcore/zizmor-action/compare/v0.5.0...v0.5.1</a></p>
<h2>v0.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Expose <code>output-file</code> as an output when
<code>advanced-security: true</code> by <a
href="https://github.com/unlobito"><code>@​unlobito</code></a> in <a
href="https://redirect.github.com/zizmorcore/zizmor-action/pull/87">zizmorcore/zizmor-action#87</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/unlobito"><code>@​unlobito</code></a>
made their first contribution in <a
href="https://redirect.github.com/zizmorcore/zizmor-action/pull/87">zizmorcore/zizmor-action#87</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/zizmorcore/zizmor-action/compare/v0.4.1...v0.5.0">https://github.com/zizmorcore/zizmor-action/compare/v0.4.1...v0.5.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/71321a20a9ded102f6e9ce5718a2fcec2c4f70d8"><code>71321a2</code></a>
Sync zizmor versions (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/96">#96</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/5ed31db0964a9d37608edd5b0675de2b52070662"><code>5ed31db</code></a>
Bump pins (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/95">#95</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/195d10ad90f31d8cd6ea1efd6ecc12969ddbe73f"><code>195d10a</code></a>
Sync zizmor versions (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/94">#94</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/c65bc8876171b6d82748ec98b77c0193b1226b94"><code>c65bc88</code></a>
chore(deps): bump github/codeql-action in the github-actions group (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/93">#93</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/c2c887f84674f9c15123e2905d2d307675d8bc01"><code>c2c887f</code></a>
chore(deps): bump zizmorcore/zizmor-action in the github-actions group
(<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/91">#91</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/5507ab0c02a9ac3996895e1598d6b3385ea7d525"><code>5507ab0</code></a>
Bump pins in README (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/90">#90</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d"><code>0dce257</code></a>
chore(deps): bump peter-evans/create-pull-request (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/88">#88</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/fb9497493b591ad90176d3ecac5ca4aeff8c9faf"><code>fb94974</code></a>
Expose <code>output-file</code> as an output when
<code>advanced-security: true</code> (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/87">#87</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/867562a69bb7adcc63dd1e8c003600a58b5f70e2"><code>867562a</code></a>
chore(deps): bump the github-actions group with 2 updates (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/85">#85</a>)</li>
<li><a
href="https://github.com/zizmorcore/zizmor-action/commit/7462f075f718787753331c6d98ca9ef8eb41e735"><code>7462f07</code></a>
Bump pins in README (<a
href="https://redirect.github.com/zizmorcore/zizmor-action/issues/84">#84</a>)</li>
<li>See full diff in <a
href="https://github.com/zizmorcore/zizmor-action/compare/135698455da5c3b3e55f73f4419e481ab68cdd95...71321a20a9ded102f6e9ce5718a2fcec2c4f70d8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=zizmorcore/zizmor-action&package-manager=github_actions&previous-version=0.4.1&new-version=0.5.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 08:05:39 +01:00
cb58d827d0 chore: update known checksums for 0.15.5 (#331)
chore: update known checksums for 0.15.5

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-03-06 08:28:19 +01:00
845ce6a88f chore: update known checksums for 0.15.4 (#328)
chore: update known checksums for 0.15.4

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-02-27 08:11:36 +01:00
48f37fab2d chore: update known checksums for 0.15.2 (#325)
chore: update known checksums for 0.15.2

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-02-20 06:59:59 +00:00
53288957fd chore: update known checksums for 0.15.1 (#323)
chore: update known checksums for 0.15.1

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-02-14 07:56:44 +00:00
bab84a8f49 chore: update known checksums for 0.15.0 (#320)
chore: update known checksums for 0.15.0

Co-authored-by: eifinger <1481961+eifinger@users.noreply.github.com>
2026-02-04 07:43:18 +00:00
54 changed files with 71172 additions and 78148 deletions
@@ -0,0 +1,53 @@
---
name: dependabot-pr-rollup
description: Find open Dependabot PRs for ruff-action, compare each PR head to its base branch, replay only the remaining npm and GitHub Actions changes in a fresh worktree and branch, run repository validation, and optionally commit, push, and open a PR. Use when you want to batch or manually replicate active Dependabot updates.
license: MIT
compatibility: Requires git, git worktree, gh CLI auth, npm, and a GitHub repo with an origin remote.
---
# Dependabot PR Rollup
## When to use
Use this skill when the user wants to:
- find all open Dependabot PRs in this repo
- reproduce their net effect in one local branch
- validate the result with the repo's standard checks
- optionally commit, push, and open a PR
## Workflow
1. Inspect the current checkout state, but do not reuse a dirty worktree.
2. List open Dependabot PRs with `gh pr list --state open --author app/dependabot`.
3. For each PR, collect the title, base branch, head branch, changed files, and relevant diffs.
4. Compare each PR head against `origin/<base>` instead of trusting the PR title. Dependabot PRs can already be partially merged, superseded by newer versions, or have no remaining net effect.
5. Create a new worktree and branch from `origin/<base>`.
6. Reproduce only the remaining dependency changes in the new worktree.
- Inspect `package.json` before editing npm dependencies.
- Run `npm ci --ignore-scripts` before applying npm updates so dependency commands run from a clean install.
- Use `npm install ... --ignore-scripts` for direct npm dependency changes so `package-lock.json` stays in sync.
- Preserve whether an npm package is a `dependency` or `devDependency`.
- When updating `@biomejs/biome`, also update the Biome schema URL version in `biome.json` to match the installed Biome version.
- For GitHub Actions updates, apply the net workflow-file changes from the Dependabot PR heads after confirming they still apply cleanly to the selected base.
7. Run `npm run all`.
8. If requested, commit the changed source, lockfile, workflow files, and generated artifacts, then push and open a PR.
## Repo-specific notes
- Use `gh` for GitHub operations.
- Keep the user's original checkout untouched by working in a separate worktree.
- This repo has Dependabot configured for both `npm` and `github-actions`.
- In this repo, `npm run all` is the safest validation command because it runs build, check, package, and unit tests.
- `npm run check` may rewrite files with Biome; include those intentional formatting changes.
- If dependency changes affect bundled output, include the regenerated `dist/` files from `npm run package`.
## Report back
Always report:
- open Dependabot PRs found
- which PRs required no net changes
- new branch name
- new worktree path
- files changed
- `npm run all` result
- if applicable, commit SHA and PR URL
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es2022",
"types": ["node"]
},
"include": ["check-all-tests-passed-needs.ts"]
}
+3 -3
View File
@@ -45,7 +45,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: ${{ matrix.language }}
source-root: src
@@ -57,7 +57,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -71,4 +71,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
+3 -1
View File
@@ -17,6 +17,8 @@ jobs:
pull-requests: read
steps:
- name: 🚀 Run Release Drafter
uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0
uses: release-drafter/release-drafter@c2e2804cc59f45f57076a99af580d0fedb697927 # v7.3.0
with:
commitish: ${{ github.sha }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+113
View File
@@ -0,0 +1,113 @@
name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Release version (e.g., 8.1.0)"
required: true
type: string
permissions: {}
jobs:
validate-release:
name: Validate release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Validate version and draft release
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
TAG: v${{ inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Version must match MAJOR.MINOR.PATCH (e.g., 8.1.0)"
exit 1
fi
RELEASE_JSON=$(gh release view "$TAG" --json isDraft,targetCommitish 2>&1) || {
echo "::error::No release found for $TAG"
exit 1
}
IS_DRAFT=$(echo "$RELEASE_JSON" | jq -r '.isDraft')
TARGET=$(echo "$RELEASE_JSON" | jq -r '.targetCommitish')
if [[ "$IS_DRAFT" != "true" ]]; then
echo "::error::Release $TAG already exists and is not a draft"
exit 1
fi
if [[ "$TARGET" != "$GITHUB_SHA" ]]; then
echo "::error::Draft release target ($TARGET) does not match current commit ($GITHUB_SHA)"
exit 1
fi
release-gate:
# N.B. This name should not change, it is used for downstream checks.
name: release-gate
needs:
- validate-release
runs-on: ubuntu-latest
environment:
name: release-gate
steps:
- run: echo "Release approved"
create-deployment:
name: create-deployment
needs:
- validate-release
- release-gate
runs-on: ubuntu-latest
environment:
name: release
steps:
- run: echo "Release deployment created"
release:
name: Release
needs:
- validate-release
- release-gate
- create-deployment
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish release
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
TAG: v${{ inputs.version }}
run: |
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Version must match MAJOR.MINOR.PATCH (e.g., 8.1.0)"
exit 1
fi
RELEASE_JSON=$(gh release view "$TAG" --json isDraft,targetCommitish 2>&1) || {
echo "::error::No release found for $TAG"
exit 1
}
IS_DRAFT=$(echo "$RELEASE_JSON" | jq -r '.isDraft')
TARGET=$(echo "$RELEASE_JSON" | jq -r '.targetCommitish')
if [[ "$IS_DRAFT" != "true" ]]; then
echo "::error::Release $TAG already exists and is not a draft"
exit 1
fi
if [[ "$TARGET" != "$GITHUB_SHA" ]]; then
echo "::error::Draft release target ($TARGET) does not match current commit ($GITHUB_SHA)"
exit 1
fi
echo "Publishing draft release $TAG"
gh release edit "$TAG" --draft=false
+82 -5
View File
@@ -22,19 +22,20 @@ jobs:
with:
persist-credentials: false
- name: Actionlint
uses: eifinger/actionlint-action@447fbfe7533062b7a9ea55f790f2396fba6d052a # v1.10.0
uses: eifinger/actionlint-action@1fc89649be682d16ec5cf65ea16e269eb88d3982 # v1.10.2
- name: Run zizmor
uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
node-version-file: .nvmrc
cache: npm
- run: |
npm ci --ignore-scripts
- run: |
npm run all
- name: Check all jobs are in all-tests-passed.needs
run: |
tsc check-all-tests-passed-needs.ts
tsc -p tsconfig.json
node check-all-tests-passed-needs.js
working-directory: .github/scripts
- name: Make sure no changes from linters are detected
@@ -279,6 +280,25 @@ jobs:
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-default-version-from-uv-lock:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Use default version from uv.lock
id: ruff-action
uses: ./
with:
src: __tests__/fixtures/python-project
version-file: __tests__/fixtures/uv.lock
- name: Correct version gets installed
run: |
if [ "$RUFF_VERSION" != "0.9.5" ]; then
exit 1
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-semver-range:
runs-on: ubuntu-latest
steps:
@@ -394,6 +414,34 @@ jobs:
fi
env:
CHECK_SHOULD_FAIL_OUTCOME: ${{ steps.check-should-fail.outcome }}
test-glob-src:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Use glob pattern in src
uses: ./
with:
src: __tests__/fixtures/glob-project/**/*.py
test-glob-single-star-src:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Use single-star glob pattern in src
uses: ./
with:
src: __tests__/fixtures/glob-single-star-project/foo/bar/*.py
test-multiple-src:
runs-on: ${{ matrix.os }}
strategy:
@@ -430,6 +478,31 @@ jobs:
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
test-custom-manifest-file:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Create test src
run: |
mkdir -p "${{ runner.temp }}/ruff-manifest-test"
printf 'print("hello")\n' > "${{ runner.temp }}/ruff-manifest-test/hello.py"
- name: Install from custom manifest file
id: ruff-action
uses: ./
with:
src: ${{ runner.temp }}/ruff-manifest-test
manifest-file: "https://raw.githubusercontent.com/astral-sh/ruff-action/${{ github.ref }}/__tests__/download/custom-manifest.ndjson"
- name: Correct version gets installed
run: |
if [ "$RUFF_VERSION" != "0.15.10" ]; then
echo "Wrong ruff version: $RUFF_VERSION"
exit 1
fi
env:
RUFF_VERSION: ${{ steps.ruff-action.outputs.ruff-version }}
all-tests-passed:
runs-on: ubuntu-latest
needs:
@@ -447,14 +520,18 @@ jobs:
- test-default-version-from-pyproject-optional-dependencies
- test-default-version-from-requirements
- test-default-version-from-requirements-with-hash
- test-default-version-from-uv-lock
- test-semver-range
- test-pep440-version-specifier
- test-checksum
- test-with-explicit-token
- test-args
- test-failure
- test-glob-src
- test-glob-single-star-src
- test-multiple-src
- test-parent-directory-pyproject
- test-custom-manifest-file
if: always()
steps:
- name: All tests passed
+5 -4
View File
@@ -14,17 +14,18 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
node-version-file: .nvmrc
cache: npm
- name: Update known checksums
id: update-known-checksums
run:
node dist/update-known-checksums/index.js
node dist/update-known-checksums/index.cjs
src/download/checksum/known-checksums.ts ${{ secrets.GITHUB_TOKEN }}
- run: npm ci --ignore-scripts && npm run all
- name: Create Pull Request
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "chore: update known checksums"
title:
+1
View File
@@ -0,0 +1 @@
24
+86 -28
View File
@@ -20,6 +20,7 @@ anything `ruff` can (ex, fix).
- [Install a specific version](#install-a-specific-version)
- [Install a version by supplying a semver range or pep440 specifier](#install-a-version-by-supplying-a-semver-range-or-pep440-specifier)
- [Install a version from a specified version file](#install-a-version-from-a-specified-version-file)
- [Install using a custom manifest URL](#install-using-a-custom-manifest-url)
- [Validate checksum](#validate-checksum)
- [GitHub authentication token](#github-authentication-token)
- [Outputs](#outputs)
@@ -27,44 +28,76 @@ anything `ruff` can (ex, fix).
## Usage
| Input | Description | Default |
|----------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| `version` | The version of Ruff to install. See [Install specific versions](#install-specific-versions) | `latest` |
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| `version` | The version of Ruff to install. See [Install specific versions](#install-specific-versions) | discovered from `pyproject.toml`, else `latest` |
| `version-file` | The file to read the version from. See [Install a version from a specified version file](#install-a-version-from-a-specified-version-file) | None |
| `manifest-file` | URL to a custom Ruff manifest in the `astral-sh/versions` format. | None |
| `download-from-astral-mirror` | Download Ruff from the Astral mirror instead of directly from GitHub Releases. | `true` |
| `args` | The arguments to pass to the `ruff` command. See [Configuring Ruff] | `check` |
| `src` | The directory or single files to run `ruff` on. | [github.workspace] |
| `checksum` | The sha256 checksum of the downloaded executable. | None |
| `github-token` | The GitHub token to use for authentication. | `GITHUB_TOKEN` |
| `src` | Source path(s) to run `ruff` on. Supports glob patterns. | [github.workspace] |
| `checksum` | The sha256 checksum of the downloaded artifact. | None |
| `github-token` | The GitHub token to use when downloading Ruff release artifacts from GitHub. | `GITHUB_TOKEN` |
By default, Ruff version metadata is resolved from the
[`astral-sh/versions` Ruff manifest](https://github.com/astral-sh/versions/blob/main/v1/ruff.ndjson).
### Basic
```yaml
- uses: astral-sh/ruff-action@v3
- uses: astral-sh/ruff-action@v4.0.0
```
### Specify a different source directory
```yaml
- uses: astral-sh/ruff-action@v3
- uses: astral-sh/ruff-action@v4.0.0
with:
src: "./src"
```
### Specify multiple files
Separate multiple `src` values with whitespace.
```yaml
- uses: astral-sh/ruff-action@v3
- uses: astral-sh/ruff-action@v4.0.0
with:
src: >-
path/to/file1.py
path/to/file2.py
```
### Use glob patterns
Glob patterns (`*`, `?`, `[...]`, and `**`) are expanded by the action using
[`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob),
so they work consistently across Linux, macOS, and Windows runners. This action
does not emulate Bash, PowerShell, or other shell-specific glob expansion.
Hidden files and directories are skipped by default; target them explicitly, for
example with `.venv/**/*.py`, to include them. If a pattern does not match any
files, the original pattern is passed to Ruff so Ruff can report the missing
path. Literal glob metacharacters in file or directory names must be escaped
using `@actions/glob` syntax.
```yaml
- uses: astral-sh/ruff-action@v3
with:
src: "src/**/*.py"
```
> [!NOTE]
> When using multiple patterns, only the first is used to search for
> `pyproject.toml` to determine the Ruff version. Use the `version` input
> for explicit control. Large glob expansions may hit command-line length limits,
> especially on Windows.
### Use to install ruff
This action adds ruff to the PATH, so you can use it in subsequent steps.
```yaml
- uses: astral-sh/ruff-action@v3
- uses: astral-sh/ruff-action@v4.0.0
- run: ruff check --fix
- run: ruff format
```
@@ -75,7 +108,7 @@ you can use the `args` input to overwrite the default value (`check`):
```yaml
- name: Install ruff without running check or format
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
args: "--version"
```
@@ -83,17 +116,17 @@ you can use the `args` input to overwrite the default value (`check`):
### Use `ruff format`
```yaml
- uses: astral-sh/ruff-action@v3
- uses: astral-sh/ruff-action@v4.0.0
with:
args: "format --check --diff"
```
### Install specific versions
By default this action looks for a pyproject.toml file in the root of the repository to determine
the ruff version to install. If no pyproject.toml file is found, or no ruff version is defined in
`project.dependencies`, `project.optional-dependencies`, or `dependency-groups`,
the latest version is installed.
By default this action searches upward from `src` until the workspace root to find the nearest
`pyproject.toml` and determine the Ruff version to install. If no `pyproject.toml` file is found,
or no Ruff version is defined in `project.dependencies`, `project.optional-dependencies`,
`dependency-groups`, or supported Poetry dependency tables, the latest version is installed.
> [!NOTE]
> This action does only support ruff versions v0.0.247 and above.
@@ -102,7 +135,7 @@ the latest version is installed.
```yaml
- name: Install the latest version of ruff
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
version: "latest"
```
@@ -111,7 +144,7 @@ the latest version is installed.
```yaml
- name: Install a specific version of ruff
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
version: "0.4.4"
```
@@ -124,21 +157,21 @@ to install the latest version that satisfies the range.
```yaml
- name: Install a semver range of ruff
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
version: ">=0.4.0"
```
```yaml
- name: Pinning a minor version of ruff
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
version: "0.4.x"
```
```yaml
- name: Install a pep440-specifier-satisfying version of ruff
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
version: ">=0.11.10,<0.12.0"
```
@@ -146,13 +179,34 @@ to install the latest version that satisfies the range.
#### Install a version from a specified version file
You can specify a file to read the version from.
Currently `pyproject.toml` and `requirements.txt` are supported.
Currently `pyproject.toml`, `requirements.txt` and `uv.lock` are supported. If the file cannot
be parsed or does not contain a Ruff version, the action warns and falls back to `latest`.
```yaml
- name: Install a version from a specified version file
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
version-file: "my-path/to/pyproject.toml-or-requirements.txt"
version-file: "my-path/to/pyproject.toml-requirements.txt-or-uv.lock"
```
Version resolution precedence is:
1. `version`
2. `version-file`
3. nearest discoverable `pyproject.toml` found by searching upward from `src`
4. `latest`
#### Install using a custom manifest URL
You can override the default `astral-sh/versions` manifest with `manifest-file`.
This affects both version resolution and artifact selection.
```yaml
- name: Install Ruff from a custom manifest
uses: astral-sh/ruff-action@v4.0.0
with:
version: "latest"
manifest-file: "https://example.com/ruff.ndjson"
```
### Validate checksum
@@ -163,7 +217,7 @@ are automatically verified by this action. The sha256 hashes can be found on the
```yaml
- name: Install a specific version and validate the checksum
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
version: "0.7.4"
checksum: "0de731c669b9ece77e799ac3f4a160c30849752714d9775c94cc4cfaf326860c"
@@ -171,9 +225,13 @@ are automatically verified by this action. The sha256 hashes can be found on the
### GitHub authentication token
This action uses the GitHub API to fetch the ruff release artifacts. To avoid hitting the GitHub API
rate limit too quickly, an authentication token can be provided via the `github-token` input. By
default, the `GITHUB_TOKEN` secret is used, which is automatically provided by GitHub Actions.
By default, this action resolves available Ruff versions from
[`astral-sh/versions`](https://github.com/astral-sh/versions) and downloads release artifacts from `https://releases.astral.sh`. If this fails this action falls back to downloading from the GitHub releases page of the Ruff repository.
Set `download-from-astral-mirror` to `false` to skip the Astral mirror and download directly from GitHub Releases.
You can provide a token via `github-token` to authenticate GitHub Releases downloads. By default, the
`GITHUB_TOKEN` secret is used, which is automatically provided by GitHub Actions.
If the default
[permissions for the GitHub token](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token)
@@ -181,7 +239,7 @@ are not sufficient, you can provide a custom GitHub token with the necessary per
```yaml
- name: Install the latest version of ruff with a custom GitHub token
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
github-token: ${{ secrets.CUSTOM_GITHUB_TOKEN }}
```
@@ -0,0 +1 @@
{"version":"0.15.10","artifacts":[{"platform":"x86_64-unknown-linux-gnu","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.10/ruff-x86_64-unknown-linux-gnu.tar.gz","archive_format":"tar.gz","sha256":"e3e9e5c791542f00d95edc74a506e1ac24efc0af9574de01ab338187bf1ff9f6"}]}
+537
View File
@@ -0,0 +1,537 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import * as semver from "semver";
const mockInfo = jest.fn();
const mockWarning = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
debug: jest.fn(),
info: mockInfo,
warning: mockWarning,
}));
const mockDownloadTool = jest.fn();
const mockExtractTar = jest.fn();
const mockExtractZip = jest.fn();
const mockCacheDir = jest.fn();
jest.unstable_mockModule("@actions/tool-cache", () => ({
cacheDir: mockCacheDir,
downloadTool: mockDownloadTool,
evaluateVersions: (versions: string[], range: string) =>
semver.maxSatisfying(versions, range) ?? "",
extractTar: mockExtractTar,
extractZip: mockExtractZip,
find: () => "",
findAllVersions: () => [],
isExplicitVersion: (version: string) => semver.valid(version) !== null,
}));
const mockGetLatestVersion = jest.fn();
const mockGetAllVersions = jest.fn();
const mockGetArtifact = jest.fn();
jest.unstable_mockModule("../../src/download/manifest", () => ({
getAllVersions: mockGetAllVersions,
getArtifact: mockGetArtifact,
getLatestVersion: mockGetLatestVersion,
}));
const mockValidateChecksum = jest.fn();
jest.unstable_mockModule("../../src/download/checksum/checksum", () => ({
validateChecksum: mockValidateChecksum,
}));
const mockCopyFile = jest.fn();
const mockReaddir = jest.fn();
jest.unstable_mockModule("node:fs", () => ({
default: {},
promises: {
copyFile: mockCopyFile,
readdir: mockReaddir,
},
}));
const { downloadVersion, rewriteToMirror } = await import(
"../../src/download/download-version"
);
const { resolveVersion } = await import("../../src/version/resolve");
describe("download-version", () => {
beforeEach(() => {
mockInfo.mockReset();
mockWarning.mockReset();
mockDownloadTool.mockReset();
mockExtractTar.mockReset();
mockExtractZip.mockReset();
mockCacheDir.mockReset();
mockGetLatestVersion.mockReset();
mockGetAllVersions.mockReset();
mockGetArtifact.mockReset();
mockValidateChecksum.mockReset();
mockCopyFile.mockReset();
mockReaddir.mockReset();
mockDownloadTool.mockResolvedValue("/tmp/downloaded");
mockExtractTar.mockResolvedValue("/tmp/extracted");
mockExtractZip.mockResolvedValue("/tmp/extracted");
mockCacheDir.mockResolvedValue("/tmp/cached");
mockReaddir.mockResolvedValue(["ruff"]);
});
describe("resolveVersion", () => {
it("uses the default manifest to resolve latest", async () => {
mockGetLatestVersion.mockResolvedValue("0.15.8");
const version = await resolveVersion("latest", undefined);
expect(version).toBe("0.15.8");
expect(mockGetLatestVersion).toHaveBeenCalledTimes(1);
expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined);
});
it("uses the default manifest to resolve available versions", async () => {
mockGetAllVersions.mockResolvedValue(["0.15.8", "0.15.7"]);
const version = await resolveVersion("0.15.x", undefined);
expect(version).toBe("0.15.8");
expect(mockGetAllVersions).toHaveBeenCalledTimes(1);
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined);
});
it("uses manifest-file when provided", async () => {
mockGetAllVersions.mockResolvedValue(["0.15.8", "0.15.7"]);
const version = await resolveVersion(
"0.15.x",
"https://example.com/custom.ndjson",
);
expect(version).toBe("0.15.8");
expect(mockGetAllVersions).toHaveBeenCalledWith(
"https://example.com/custom.ndjson",
);
});
});
describe("downloadVersion", () => {
it("fails when manifest lookup fails", async () => {
mockGetArtifact.mockRejectedValue(new Error("manifest unavailable"));
await expect(
downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
),
).rejects.toThrow("manifest unavailable");
expect(mockDownloadTool).not.toHaveBeenCalled();
expect(mockValidateChecksum).not.toHaveBeenCalled();
});
it("fails when no matching artifact exists in the default manifest", async () => {
mockGetArtifact.mockResolvedValue(undefined);
await expect(
downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
),
).rejects.toThrow(
"Could not find artifact for version 0.15.8, arch x86_64, platform unknown-linux-gnu in https://raw.githubusercontent.com/astral-sh/versions/main/v1/ruff.ndjson .",
);
expect(mockDownloadTool).not.toHaveBeenCalled();
expect(mockValidateChecksum).not.toHaveBeenCalled();
});
it("uses built-in checksums for default manifest downloads", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "manifest-checksum-that-should-be-ignored",
downloadUrl: "https://example.com/ruff.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
);
expect(mockValidateChecksum).toHaveBeenCalledWith(
undefined,
"/tmp/downloaded",
"x86_64",
"unknown-linux-gnu",
"0.15.8",
);
});
it("rewrites GitHub Releases URLs to the Astral mirror", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
);
expect(mockDownloadTool).toHaveBeenCalledWith(
"https://releases.astral.sh/github/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
undefined,
undefined,
);
});
it("does not rewrite non-GitHub URLs", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl: "https://example.com/ruff.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
);
expect(mockDownloadTool).toHaveBeenCalledWith(
"https://example.com/ruff.tar.gz",
undefined,
undefined,
);
});
it("falls back to GitHub Releases when the mirror download fails", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
});
mockDownloadTool
.mockRejectedValueOnce(new Error("mirror unavailable"))
.mockResolvedValueOnce("/tmp/downloaded");
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
);
expect(mockDownloadTool).toHaveBeenCalledTimes(2);
expect(mockDownloadTool).toHaveBeenNthCalledWith(
1,
"https://releases.astral.sh/github/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
undefined,
undefined,
);
expect(mockDownloadTool).toHaveBeenNthCalledWith(
2,
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
undefined,
"token",
);
expect(mockWarning).toHaveBeenCalledWith(
"Failed to download from mirror, falling back to GitHub Releases: mirror unavailable",
);
});
it("falls back to the canonical old GitHub Releases URL", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/0.4.7/ruff-x86_64-unknown-linux-gnu.tar.gz",
});
mockDownloadTool
.mockRejectedValueOnce(new Error("mirror unavailable"))
.mockResolvedValueOnce("/tmp/downloaded");
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.4.7",
undefined,
"token",
);
expect(mockDownloadTool).toHaveBeenNthCalledWith(
2,
"https://github.com/astral-sh/ruff/releases/download/v0.4.7/ruff-0.4.7-x86_64-unknown-linux-gnu.tar.gz",
undefined,
"token",
);
});
it("does not fall back when checksum validation fails", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
});
mockValidateChecksum.mockRejectedValue(new Error("bad checksum"));
await expect(
downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
),
).rejects.toThrow("bad checksum");
expect(mockDownloadTool).toHaveBeenCalledTimes(1);
expect(mockWarning).not.toHaveBeenCalled();
});
it("does not fall back when extraction fails", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
});
mockExtractTar.mockRejectedValue(new Error("extract failed"));
await expect(
downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
),
).rejects.toThrow("extract failed");
expect(mockDownloadTool).toHaveBeenCalledTimes(1);
expect(mockWarning).not.toHaveBeenCalled();
});
it("does not fall back for non-GitHub URLs", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl: "https://example.com/ruff.tar.gz",
});
mockDownloadTool.mockRejectedValue(new Error("download failed"));
await expect(
downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
),
).rejects.toThrow("download failed");
expect(mockDownloadTool).toHaveBeenCalledTimes(1);
});
it("skips the Astral mirror when downloadFromAstralMirror is false", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
undefined,
"token",
undefined,
false,
);
expect(mockDownloadTool).toHaveBeenCalledWith(
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
undefined,
"token",
);
expect(mockDownloadTool).toHaveBeenCalledTimes(1);
});
it("uses manifest-file checksum metadata when checksum input is unset", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "manifest-checksum",
downloadUrl: "https://example.com/custom-ruff.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
"",
"token",
"https://example.com/custom.ndjson",
);
expect(mockValidateChecksum).toHaveBeenCalledWith(
"manifest-checksum",
"/tmp/downloaded",
"x86_64",
"unknown-linux-gnu",
"0.15.8",
);
});
it("prefers checksum input over manifest-file checksum metadata", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "manifest-checksum",
downloadUrl: "https://example.com/custom-ruff.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
"user-checksum",
"token",
"https://example.com/custom.ndjson",
);
expect(mockValidateChecksum).toHaveBeenCalledWith(
"user-checksum",
"/tmp/downloaded",
"x86_64",
"unknown-linux-gnu",
"0.15.8",
);
});
it("preserves tar extraction behavior for newer versions", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl: "https://example.com/ruff.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.15.8",
"user-checksum",
"token",
);
expect(mockExtractTar).toHaveBeenCalledWith("/tmp/downloaded");
expect(mockCacheDir).toHaveBeenCalledWith(
"/tmp/extracted/ruff-x86_64-unknown-linux-gnu",
"ruff",
"0.15.8",
"x86_64",
);
});
it("preserves tar extraction behavior for older versions", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "tar.gz",
checksum: "abc123",
downloadUrl: "https://example.com/ruff.tar.gz",
});
await downloadVersion(
"unknown-linux-gnu",
"x86_64",
"0.4.10",
undefined,
"token",
);
expect(mockCacheDir).toHaveBeenCalledWith(
"/tmp/extracted",
"ruff",
"0.4.10",
"x86_64",
);
});
it("preserves zip extraction behavior on Windows", async () => {
mockGetArtifact.mockResolvedValue({
archiveFormat: "zip",
checksum: "abc123",
downloadUrl: "https://example.com/ruff.zip",
});
await downloadVersion(
"pc-windows-msvc",
"x86_64",
"0.15.8",
undefined,
"token",
);
expect(mockCopyFile).toHaveBeenCalledWith(
"/tmp/downloaded",
"/tmp/downloaded.zip",
);
expect(mockExtractZip).toHaveBeenCalledWith("/tmp/downloaded.zip");
expect(mockCacheDir).toHaveBeenCalledWith(
"/tmp/extracted",
"ruff",
"0.15.8",
"x86_64",
);
});
});
describe("rewriteToMirror", () => {
it("rewrites a GitHub Releases URL to the Astral mirror", () => {
expect(
rewriteToMirror(
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
),
).toBe(
"https://releases.astral.sh/github/ruff/releases/download/0.15.8/ruff-x86_64-unknown-linux-gnu.tar.gz",
);
});
it("returns undefined for non-GitHub URLs", () => {
expect(
rewriteToMirror("https://example.com/ruff.tar.gz"),
).toBeUndefined();
});
it("returns undefined for a different GitHub repo", () => {
expect(
rewriteToMirror(
"https://github.com/other/repo/releases/download/v1.0/file.tar.gz",
),
).toBeUndefined();
});
});
});
+196
View File
@@ -0,0 +1,196 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
const mockFetch = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
debug: jest.fn(),
info: jest.fn(),
}));
jest.unstable_mockModule("../../src/utils/fetch", () => ({
fetch: mockFetch,
}));
const {
clearManifestCache,
fetchManifest,
getAllVersions,
getArtifact,
getLatestVersion,
parseManifest,
} = await import("../../src/download/manifest");
const sampleManifestResponse = `{"version":"0.15.8","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f"},{"platform":"x86_64-pc-windows-msvc","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-pc-windows-msvc.zip","archive_format":"zip","sha256":"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036"}]}
{"version":"0.15.7","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.7/ruff-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"606b3c6949d971709f2526fa0d9f0fd23ccf60e09f117999b406b424af18a6a6"}]}`;
const multiVariantManifestResponse = `{"version":"0.15.8","artifacts":[{"platform":"aarch64-apple-darwin","variant":"python-managed","url":"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin-managed.tar.gz","archive_format":"tar.gz","sha256":"managed-checksum"},{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin.zip","archive_format":"zip","sha256":"default-checksum"}]}`;
const oldVersionManifestResponse = `{"version":"v0.4.7","artifacts":[{"platform":"0.4.7-aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/ruff/releases/download/v0.4.7/ruff-0.4.7-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"old-checksum"}]}`;
function createMockResponse(
ok: boolean,
status: number,
statusText: string,
data: string,
) {
return {
ok,
status,
statusText,
text: async () => data,
};
}
describe("manifest", () => {
beforeEach(() => {
clearManifestCache();
mockFetch.mockReset();
});
describe("fetchManifest", () => {
it("fetches and parses manifest data", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
const versions = await fetchManifest();
expect(versions).toHaveLength(2);
expect(versions[0]?.version).toBe("0.15.8");
expect(versions[1]?.version).toBe("0.15.7");
});
it("throws on a failed fetch", async () => {
mockFetch.mockResolvedValue(
createMockResponse(false, 500, "Internal Server Error", ""),
);
await expect(fetchManifest()).rejects.toThrow(
"Failed to fetch manifest data: 500 Internal Server Error",
);
});
it("caches results per URL", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
await fetchManifest("https://example.com/custom.ndjson");
await fetchManifest("https://example.com/custom.ndjson");
expect(mockFetch).toHaveBeenCalledTimes(1);
});
});
describe("getAllVersions", () => {
it("returns all version strings", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
const versions = await getAllVersions(
"https://example.com/custom.ndjson",
);
expect(versions).toEqual(["0.15.8", "0.15.7"]);
});
});
describe("getLatestVersion", () => {
it("returns the first version string", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
await expect(
getLatestVersion("https://example.com/custom.ndjson"),
).resolves.toBe("0.15.8");
});
});
describe("getArtifact", () => {
beforeEach(() => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
});
it("finds an artifact by version and platform", async () => {
const artifact = await getArtifact("0.15.8", "aarch64", "apple-darwin");
expect(artifact).toEqual({
archiveFormat: "tar.gz",
checksum:
"fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin.tar.gz",
});
});
it("finds a windows artifact", async () => {
const artifact = await getArtifact("0.15.8", "x86_64", "pc-windows-msvc");
expect(artifact).toEqual({
archiveFormat: "zip",
checksum:
"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-x86_64-pc-windows-msvc.zip",
});
});
it("prefers the default variant when multiple artifacts share a platform", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", multiVariantManifestResponse),
);
const artifact = await getArtifact("0.15.8", "aarch64", "apple-darwin");
expect(artifact).toEqual({
archiveFormat: "zip",
checksum: "default-checksum",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/0.15.8/ruff-aarch64-apple-darwin.zip",
});
});
it("finds an old artifact when the manifest version has a v prefix", async () => {
mockFetch.mockResolvedValue(
createMockResponse(true, 200, "OK", oldVersionManifestResponse),
);
const artifact = await getArtifact("0.4.7", "aarch64", "apple-darwin");
expect(artifact).toEqual({
archiveFormat: "tar.gz",
checksum: "old-checksum",
downloadUrl:
"https://github.com/astral-sh/ruff/releases/download/v0.4.7/ruff-0.4.7-aarch64-apple-darwin.tar.gz",
});
});
it("returns undefined for an unknown version", async () => {
const artifact = await getArtifact("0.0.1", "aarch64", "apple-darwin");
expect(artifact).toBeUndefined();
});
it("returns undefined for an unknown platform", async () => {
const artifact = await getArtifact(
"0.15.8",
"aarch64",
"unknown-linux-musl",
);
expect(artifact).toBeUndefined();
});
});
describe("parseManifest", () => {
it("throws for malformed manifest data", () => {
expect(() => parseManifest('{"version":"0.1.0"', "test-source")).toThrow(
"Failed to parse manifest data from test-source",
);
});
});
});
@@ -0,0 +1,13 @@
[project]
name = "pyython-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"ruff==0.6.2",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@@ -0,0 +1 @@
print("hello")
@@ -0,0 +1 @@
print("deeper")
@@ -0,0 +1 @@
print("check me")
@@ -0,0 +1,2 @@
# This file should be ignored when using foo/bar/*.py glob
print("ignore me")
@@ -0,0 +1,13 @@
[project]
name = "glob-single-star-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"ruff==0.6.2",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
+20
View File
@@ -0,0 +1,20 @@
version = 1
revision = 1
requires-python = ">=3.11"
[[package]]
name = "example-project"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "ruff" },
]
[package.metadata]
requires-dist = [{ name = "ruff", specifier = ">=0.9.0" }]
[[package]]
name = "ruff"
version = "0.9.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/source/r/ruff/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 }
+122
View File
@@ -0,0 +1,122 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
const debug = jest.fn();
const info = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
debug,
info,
}));
const { findPyprojectToml } = await import("../../src/utils/pyproject-finder");
const testFilePath = fileURLToPath(import.meta.url);
const testDir = path.dirname(testFilePath);
const repoRoot = path.resolve(testDir, "..", "..");
const fixturesDir = path.join(repoRoot, "__tests__", "fixtures");
describe("findPyprojectToml", () => {
beforeEach(() => {
debug.mockReset();
info.mockReset();
});
describe("when pyproject.toml exists in src directory", () => {
it("should return the exact path", () => {
const result = findPyprojectToml(fixturesDir, repoRoot);
expect(result).toContain("pyproject.toml");
expect(result).toContain("fixtures");
expect(info).toHaveBeenCalled();
});
});
describe("when pyproject.toml exists only in parent directory", () => {
it("should search upwards and find the parent's pyproject.toml", () => {
const subprojectDir = path.join(
fixturesDir,
"parent-config-project",
"subproject",
);
const result = findPyprojectToml(subprojectDir, repoRoot);
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
expect(result).toContain("parent-config-project");
expect(info).toHaveBeenCalled();
});
});
describe("boundary conditions", () => {
it("should stop searching at workspace root and return undefined when not found", () => {
const nodeModulesDir = path.join(repoRoot, "node_modules", "@actions");
const result = findPyprojectToml(nodeModulesDir, repoRoot);
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalledWith(
expect.stringContaining("Found pyproject.toml"),
);
});
it("should find pyproject.toml when it exists at workspace root", () => {
const parentConfigProjectDir = path.join(
fixturesDir,
"parent-config-project",
);
const subprojectDir = path.join(parentConfigProjectDir, "subproject");
const result = findPyprojectToml(subprojectDir, parentConfigProjectDir);
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
expect(result).toContain("parent-config-project");
});
it("should stop at workspace root even if searching from it", () => {
const result = findPyprojectToml(fixturesDir, fixturesDir);
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
expect(result).toContain("fixtures");
});
});
describe("edge cases", () => {
it("should handle relative paths", () => {
const result = findPyprojectToml("./__tests__/fixtures", ".");
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
});
it("should handle when src equals workspace root", () => {
const result = findPyprojectToml(fixturesDir, fixturesDir);
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
expect(result).toContain("fixtures");
});
it("should log debug messages for each checked path", () => {
const pythonProjectDir = path.join(fixturesDir, "python-project");
findPyprojectToml(pythonProjectDir, repoRoot);
expect(debug).toHaveBeenCalled();
expect(debug.mock.calls.length).toBeGreaterThan(0);
expect(debug.mock.calls[0][0]).toContain("Checking for");
expect(debug.mock.calls[0][0]).toContain("python-project");
});
it("should handle paths with trailing slashes", () => {
const result = findPyprojectToml(`${fixturesDir}/`, repoRoot);
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
});
});
});
+374
View File
@@ -0,0 +1,374 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, describe, expect, it, jest } from "@jest/globals";
jest.unstable_mockModule("@actions/core", () => ({
debug: jest.fn(),
info: jest.fn(),
}));
const {
expandSourceInput,
getSourceBasePath,
getVersionSourceDirectory,
splitInput,
} = await import("../../src/utils/source-input");
const { findPyprojectToml } = await import("../../src/utils/pyproject-finder");
let tempDir: string | undefined;
afterEach(async () => {
if (tempDir !== undefined) {
await fs.rm(tempDir, { force: true, recursive: true });
tempDir = undefined;
}
});
async function createTempProject(): Promise<string> {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ruff-action-source-"));
await fs.mkdir(path.join(tempDir, "src", "package", "nested"), {
recursive: true,
});
await fs.writeFile(path.join(tempDir, "src", "package", "__init__.py"), "");
await fs.writeFile(path.join(tempDir, "src", "package", "main.py"), "");
await fs.writeFile(
path.join(tempDir, "src", "package", "nested", "mod.py"),
"",
);
await fs.writeFile(path.join(tempDir, "src", "package", "notes.txt"), "");
await fs.mkdir(path.join(tempDir, "src", "pkg[legacy]"), {
recursive: true,
});
await fs.writeFile(path.join(tempDir, "src", "pkg[legacy]", "main.py"), "");
await fs.mkdir(path.join(tempDir, "src", "pkgl"), { recursive: true });
await fs.writeFile(path.join(tempDir, "src", "pkgl", "main.py"), "");
return tempDir;
}
describe("splitInput", () => {
it("splits whitespace-delimited inputs", () => {
expect(splitInput(" check --fix\n--diff ")).toEqual([
"check",
"--fix",
"--diff",
]);
});
it("returns an empty array for empty input", () => {
expect(splitInput(" \n\t ")).toEqual([]);
});
});
describe("expandSourceInput", () => {
it("leaves non-glob sources unchanged", async () => {
await expect(expandSourceInput("src file.py")).resolves.toEqual([
"src",
"file.py",
]);
});
it("expands recursive globs without relying on shell behavior", async () => {
const projectDir = await createTempProject();
const pattern = path.join(projectDir, "src", "**", "*.py");
await expect(expandSourceInput(pattern)).resolves.toEqual([
path.join(projectDir, "src", "package", "__init__.py"),
path.join(projectDir, "src", "package", "main.py"),
path.join(projectDir, "src", "package", "nested", "mod.py"),
path.join(projectDir, "src", "pkg[legacy]", "main.py"),
path.join(projectDir, "src", "pkgl", "main.py"),
]);
});
it("preserves relative source patterns as relative paths", async () => {
const projectDir = await createTempProject();
const originalCwd = process.cwd();
try {
process.chdir(projectDir);
await expect(expandSourceInput("src/**/*.py")).resolves.toEqual([
path.join("src", "package", "__init__.py"),
path.join("src", "package", "main.py"),
path.join("src", "package", "nested", "mod.py"),
path.join("src", "pkg[legacy]", "main.py"),
path.join("src", "pkgl", "main.py"),
]);
} finally {
process.chdir(originalCwd);
}
});
it("maps relative root glob matches to . instead of an empty argument", async () => {
const projectDir = await createTempProject();
const originalCwd = process.cwd();
try {
process.chdir(projectDir);
for (const pattern of ["**", "**/", "./**"]) {
const sources = await expandSourceInput(pattern);
expect(sources).toContain(".");
expect(sources).not.toContain("");
}
} finally {
process.chdir(originalCwd);
}
});
it("excludes hidden paths by default while allowing explicit hidden patterns", async () => {
const projectDir = await createTempProject();
const originalCwd = process.cwd();
await fs.mkdir(path.join(projectDir, ".venv"));
await fs.writeFile(path.join(projectDir, ".venv", "ignored.py"), "");
await fs.mkdir(path.join(projectDir, ".tox"));
await fs.writeFile(path.join(projectDir, ".tox", "ignored.py"), "");
try {
process.chdir(projectDir);
const defaultSources = await expandSourceInput("**/*.py");
expect(defaultSources).not.toContain(path.join(".venv", "ignored.py"));
expect(defaultSources).not.toContain(path.join(".tox", "ignored.py"));
await expect(expandSourceInput(".venv/**/*.py")).resolves.toEqual([
path.join(".venv", "ignored.py"),
]);
} finally {
process.chdir(originalCwd);
}
});
it("preserves unmatched globs for Ruff to report", async () => {
const projectDir = await createTempProject();
const pattern = path.join(projectDir, "src", "**", "missing-*.py");
await expect(expandSourceInput(pattern)).resolves.toEqual([pattern]);
});
it("expands multiple sources in input order", async () => {
const projectDir = await createTempProject();
const first = path.join(projectDir, "src", "package", "main.py");
const second = path.join(projectDir, "src", "package", "nested", "*.py");
await expect(expandSourceInput(`${first} ${second}`)).resolves.toEqual([
first,
path.join(projectDir, "src", "package", "nested", "mod.py"),
]);
});
it("uses @actions/glob syntax for paths that contain glob metacharacters", async () => {
const projectDir = await createTempProject();
await expect(
expandSourceInput(path.join(projectDir, "src", "pkg[legacy]")),
).resolves.toEqual([path.join(projectDir, "src", "pkgl")]);
await expect(
expandSourceInput(path.join(projectDir, "src", "pkg[legacy]", "*.py")),
).resolves.toEqual([path.join(projectDir, "src", "pkgl", "main.py")]);
});
it("supports @actions/glob escaping for literal glob metacharacters", async () => {
const projectDir = await createTempProject();
const pattern = path.join(projectDir, "src", "pkg[[]legacy]", "*.py");
await expect(expandSourceInput(pattern)).resolves.toEqual([
path.join(projectDir, "src", "pkg[legacy]", "main.py"),
]);
});
it("does not traverse symlinked directories", async () => {
const projectDir = await createTempProject();
const externalDir = path.join(projectDir, "external");
const symlinkPath = path.join(projectDir, "src", "linked");
await fs.mkdir(externalDir);
await fs.writeFile(path.join(externalDir, "external.py"), "");
await fs.symlink(externalDir, symlinkPath, "dir");
const sources = await expandSourceInput(
path.join(projectDir, "src", "**", "*.py"),
);
expect(sources).not.toContain(path.join(symlinkPath, "external.py"));
});
it("still expands missing paths that contain glob metacharacters", async () => {
const projectDir = await createTempProject();
const pattern = path.join(projectDir, "src", "pkg[l]");
await expect(expandSourceInput(pattern)).resolves.toEqual([
path.join(projectDir, "src", "pkgl"),
]);
});
});
describe("getVersionSourceDirectory", () => {
it("does not scan src globs when version input is explicit", async () => {
await expect(
getVersionSourceDirectory("../**/*.py", "0.1.0", undefined),
).resolves.toBe(".");
});
it("does not scan src globs when version-file input is explicit", async () => {
await expect(
getVersionSourceDirectory("../**/*.py", undefined, "pyproject.toml"),
).resolves.toBe(".");
});
it("uses source discovery when no version input is explicit", async () => {
await expect(
getVersionSourceDirectory("../**/*.py", undefined, undefined),
).rejects.toThrow("Invalid pattern '../**/*.py'");
});
});
describe("getSourceBasePath", () => {
it("returns the directory for a plain path", async () => {
await expect(getSourceBasePath("my-project")).resolves.toBe("my-project");
});
it("returns the directory for a dotted path", async () => {
await expect(getSourceBasePath("./src")).resolves.toBe("./src");
});
it("preserves filesystem root paths", async () => {
const root = path.parse(process.cwd()).root;
await expect(getSourceBasePath(root)).resolves.toBe(root);
});
it("strips glob pattern and returns parent directory", async () => {
await expect(getSourceBasePath("src/**/*.py")).resolves.toBe("src");
});
it("returns . for a top-level glob", async () => {
await expect(getSourceBasePath("*.py")).resolves.toBe(".");
});
it("returns . for empty string", async () => {
await expect(getSourceBasePath("")).resolves.toBe(".");
});
it("uses only the first whitespace-delimited token", async () => {
await expect(getSourceBasePath("sub1/*.py sub2/")).resolves.toBe("sub1");
});
it("handles trailing slash in prefix", async () => {
await expect(getSourceBasePath("sub/*.py")).resolves.toBe("sub");
});
it("uses the parent directory when a glob appears in a filename", async () => {
await expect(getSourceBasePath("src/file*.py")).resolves.toBe("src");
});
it("handles ? glob", async () => {
await expect(getSourceBasePath("file?.py")).resolves.toBe(".");
});
it("handles bracket glob", async () => {
await expect(getSourceBasePath("file[0-9].py")).resolves.toBe(".");
});
it("uses @actions/glob syntax for base paths with glob metacharacters", async () => {
const projectDir = await createTempProject();
const originalCwd = process.cwd();
try {
process.chdir(projectDir);
await expect(getSourceBasePath("src/pkg[legacy]")).resolves.toBe(
path.join("src", "pkgl"),
);
await expect(getSourceBasePath("src/pkg[legacy]/*.py")).resolves.toBe(
path.join("src", "pkgl", "main.py"),
);
} finally {
process.chdir(originalCwd);
}
});
it("supports @actions/glob escaping for version discovery from literal glob metacharacters", async () => {
const projectDir = await createTempProject();
const originalCwd = process.cwd();
const pyprojectPath = path.join(
projectDir,
"src",
"pkg[legacy]",
"pyproject.toml",
);
await fs.writeFile(
pyprojectPath,
"[project]\ndependencies = ['ruff==0.6.2']\n",
);
try {
process.chdir(projectDir);
const sourceBasePath = await getSourceBasePath(
"src/pkg[[]legacy]/main.py",
);
expect(findPyprojectToml(sourceBasePath, projectDir)).toBe(
await fs.realpath(pyprojectPath),
);
} finally {
process.chdir(originalCwd);
}
});
it("uses the first expanded glob match for version discovery", async () => {
const projectDir = await createTempProject();
const originalCwd = process.cwd();
const pyprojectPath = path.join(
projectDir,
"packages",
"foo",
"pyproject.toml",
);
await fs.mkdir(path.join(projectDir, "packages", "foo", "src"), {
recursive: true,
});
await fs.writeFile(
pyprojectPath,
"[project]\ndependencies = ['ruff==0.10.0']\n",
);
await fs.writeFile(
path.join(projectDir, "packages", "foo", "src", "module.py"),
"",
);
try {
process.chdir(projectDir);
const sourceBasePath = await getSourceBasePath("packages/*/src/**/*.py");
expect(sourceBasePath).toBe(
path.join("packages", "foo", "src", "module.py"),
);
const workspaceRoot = await fs.realpath(projectDir);
expect(findPyprojectToml(sourceBasePath, workspaceRoot)).toBe(
await fs.realpath(pyprojectPath),
);
} finally {
process.chdir(originalCwd);
}
});
it("uses the unmatched glob fallback when version-discovery glob has no matches", async () => {
const projectDir = await createTempProject();
const originalCwd = process.cwd();
await fs.mkdir(path.join(projectDir, "packages"));
try {
process.chdir(projectDir);
await expect(getSourceBasePath("packages/*/src/**/*.py")).resolves.toBe(
"packages",
);
} finally {
process.chdir(originalCwd);
}
});
it("does not treat brace expansion as glob syntax", async () => {
await expect(getSourceBasePath("src/{a,b}/*.py")).resolves.toBe(
"src/{a,b}",
);
});
});
+151
View File
@@ -0,0 +1,151 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
const info = jest.fn();
const warning = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
debug: jest.fn(),
info,
warning,
}));
const {
findRuffVersionInSpec,
getRuffVersionFromFile,
getRuffVersionFromUvLockContent,
} = await import("../../src/version/file-parser");
describe("file-parser", () => {
beforeEach(() => {
info.mockReset();
warning.mockReset();
});
describe("findRuffVersionInSpec", () => {
it("extracts version from 'ruff==0.9.3'", () => {
const result = findRuffVersionInSpec("ruff==0.9.3");
expect(result).toBe("0.9.3");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(warning).not.toHaveBeenCalled();
});
it("extracts version from 'ruff>=0.14'", () => {
const result = findRuffVersionInSpec("ruff>=0.14");
expect(result).toBe(">=0.14");
expect(warning).not.toHaveBeenCalled();
});
it("extracts version from 'ruff ~=1.0.0'", () => {
const result = findRuffVersionInSpec("ruff ~=1.0.0");
expect(result).toBe("~=1.0.0");
expect(warning).not.toHaveBeenCalled();
});
it("extracts version from 'ruff>=0.14,<1.0'", () => {
const result = findRuffVersionInSpec("ruff>=0.14,<1.0");
expect(result).toBe(">=0.14,<1.0");
expect(warning).not.toHaveBeenCalled();
});
it("extracts version from 'ruff>=0.14,<2.0,!=1.5.0'", () => {
const result = findRuffVersionInSpec("ruff>=0.14,<2.0,!=1.5.0");
expect(result).toBe(">=0.14,<2.0,!=1.5.0");
expect(warning).not.toHaveBeenCalled();
});
it("returns undefined for non-ruff dependencies", () => {
const result = findRuffVersionInSpec("another-dep==0.1.6");
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalled();
expect(warning).not.toHaveBeenCalled();
});
it("strips trailing backslash", () => {
const result = findRuffVersionInSpec("ruff==0.9.3 \\");
expect(result).toBe("0.9.3");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(warning).not.toHaveBeenCalled();
});
it("strips environment markers and warns", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14");
expect(info).toHaveBeenCalledWith(
"Found ruff version in requirements file: >=0.14",
);
expect(warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("handles whitespace", () => {
const result = findRuffVersionInSpec(" ruff >=0.14 ");
expect(result).toBe(">=0.14");
expect(warning).not.toHaveBeenCalled();
});
it("returns undefined for empty strings", () => {
const result = findRuffVersionInSpec("");
expect(result).toBeUndefined();
expect(info).not.toHaveBeenCalled();
expect(warning).not.toHaveBeenCalled();
});
});
describe("getRuffVersionFromFile", () => {
it("reads the version from requirements.txt", () => {
const result = getRuffVersionFromFile(
"__tests__/fixtures/requirements.txt",
);
expect(result).toBe("0.9.0");
});
it("reads the version from requirements files with hashes", () => {
const result = getRuffVersionFromFile(
"__tests__/fixtures/requirements-with-hash.txt",
);
expect(result).toBe("0.9.0");
});
it("reads the version from pyproject.toml dependencies", () => {
const result = getRuffVersionFromFile(
"__tests__/fixtures/pyproject.toml",
);
expect(result).toBe("0.9.3");
});
it("reads the version from Poetry dependencies", () => {
const result = getRuffVersionFromFile(
"__tests__/fixtures/pyproject-dependency-poetry-project/pyproject.toml",
);
expect(result).toBe("~0.8.2");
});
it("reads the version from uv.lock", () => {
const result = getRuffVersionFromFile("__tests__/fixtures/uv.lock");
expect(result).toBe("0.9.5");
});
});
describe("getRuffVersionFromUvLockContent", () => {
it("extracts the ruff version from uv.lock content", () => {
const result = getRuffVersionFromUvLockContent(
'[[package]]\nname = "other"\nversion = "1.0.0"\n\n[[package]]\nname = "ruff"\nversion = "0.9.5"\n',
);
expect(result).toBe("0.9.5");
});
it("returns undefined when ruff is not in uv.lock", () => {
const result = getRuffVersionFromUvLockContent(
'[[package]]\nname = "other"\nversion = "1.0.0"\n',
);
expect(result).toBeUndefined();
});
});
});
@@ -0,0 +1,164 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
afterEach,
beforeEach,
describe,
expect,
it,
jest,
} from "@jest/globals";
const debug = jest.fn();
const info = jest.fn();
const warning = jest.fn();
jest.unstable_mockModule("@actions/core", () => ({
debug,
info,
warning,
}));
const { resolveVersionRequest } = await import(
"../../src/version/version-request-resolver"
);
const tempDirs: string[] = [];
function createTempProject(files: Record<string, string> = {}): string {
const dir = fs.mkdtempSync(
path.join(os.tmpdir(), "ruff-action-version-test-"),
);
tempDirs.push(dir);
for (const [relativePath, content] of Object.entries(files)) {
const filePath = path.join(dir, relativePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content);
}
return dir;
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { force: true, recursive: true });
}
});
describe("resolveVersionRequest", () => {
beforeEach(() => {
debug.mockReset();
info.mockReset();
warning.mockReset();
});
it("prefers explicit input over workspace discovery", () => {
const workspaceRoot = createTempProject({
"pyproject.toml": `[project]\ndependencies = ["ruff==0.5.14"]\n`,
});
const request = resolveVersionRequest({
sourceDirectory: workspaceRoot,
version: "==0.6.0",
workspaceRoot,
});
expect(request).toEqual({
source: "input",
specifier: "0.6.0",
});
});
it("uses requirements.txt when it is passed via version-file", () => {
const workspaceRoot = createTempProject({
"requirements.txt": "ruff==0.6.17\nruff-api==0.1.0\n",
});
const request = resolveVersionRequest({
sourceDirectory: workspaceRoot,
versionFile: path.join(workspaceRoot, "requirements.txt"),
workspaceRoot,
});
expect(request).toEqual({
format: "requirements",
source: "version-file",
sourcePath: path.join(workspaceRoot, "requirements.txt"),
specifier: "0.6.17",
});
});
it("warns and falls back to latest when version-file does not resolve a version", () => {
const workspaceRoot = createTempProject({
"requirements.txt": "ruff-api==0.1.0\n",
});
const request = resolveVersionRequest({
sourceDirectory: workspaceRoot,
versionFile: path.join(workspaceRoot, "requirements.txt"),
workspaceRoot,
});
expect(request).toEqual({
source: "default",
specifier: "latest",
});
expect(warning).toHaveBeenCalledWith(
`Could not parse version from ${path.join(workspaceRoot, "requirements.txt")}. Using latest version.`,
);
});
it("discovers pyproject.toml by searching upward from src", () => {
const workspaceRoot = createTempProject({
"pyproject.toml": `[project]\ndependencies = ["ruff==0.10.0"]\n`,
"subproject/nested/example.py": 'print("hello")\n',
});
const sourceDirectory = path.join(workspaceRoot, "subproject", "nested");
const request = resolveVersionRequest({
sourceDirectory,
workspaceRoot,
});
expect(request).toEqual({
format: "pyproject.toml",
source: "pyproject.toml",
sourcePath: path.join(workspaceRoot, "pyproject.toml"),
specifier: "0.10.0",
});
});
it("falls back to latest when no workspace version source is found", () => {
const workspaceRoot = createTempProject({
"subproject/example.py": 'print("hello")\n',
});
const request = resolveVersionRequest({
sourceDirectory: path.join(workspaceRoot, "subproject"),
workspaceRoot,
});
expect(request).toEqual({
source: "default",
specifier: "latest",
});
expect(info).toHaveBeenCalledWith(
"Could not find pyproject.toml. Using latest version.",
);
});
it("throws when both version and version-file are specified", () => {
const workspaceRoot = createTempProject();
expect(() =>
resolveVersionRequest({
sourceDirectory: workspaceRoot,
version: "0.6.0",
versionFile: path.join(workspaceRoot, "requirements.txt"),
workspaceRoot,
}),
).toThrow("It is not allowed to specify both version and version-file");
});
});
+13 -7
View File
@@ -7,31 +7,37 @@ inputs:
required: false
default: "check"
src:
description: "Source to run Ruff. Defaults to the current directory."
description: "Source path(s) or @actions/glob pattern(s) to run Ruff on. Defaults to the current workspace."
required: false
default: ${{ github.workspace }}
version:
description: "The version of Ruff to use, e.g., `0.6.0` Defaults to the version in pyproject.toml or 'latest'."
description: "The version of Ruff to use, e.g., `0.6.0`. Defaults to the first discoverable version in pyproject.toml searched upward from `src`, or `latest`."
required: false
default: ""
version-file:
description: "Path to a pyproject.toml or requirements.txt file to read the version from."
description: "Path to a pyproject.toml, requirements.txt or uv.lock file to read the version from. If parsing fails, the action warns and falls back to `latest`."
required: false
checksum:
description: "The checksum of the ruff version to install"
required: false
manifest-file:
description: "URL to a custom manifest file in the astral-sh/versions format."
required: false
download-from-astral-mirror:
description: "Download Ruff from the Astral mirror instead of directly from GitHub Releases."
required: false
default: "true"
github-token:
description:
"Used to increase the rate limit when retrieving versions and downloading
ruff."
"Used for authenticated downloads of Ruff release artifacts from GitHub."
required: false
default: ${{ github.token }}
outputs:
ruff-version:
description: "The installed ruff version. Useful when using latest."
runs:
using: "node20"
main: "dist/ruff-action/index.js"
using: "node24"
main: "dist/ruff-action/index.cjs"
branding:
icon: "code"
color: "black"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.2/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
"assist": {
"actions": {
"source": {
Generated Vendored
+32183
View File
File diff suppressed because one or more lines are too long
Generated Vendored
-39397
View File
File diff suppressed because one or more lines are too long
+30058
View File
File diff suppressed because one or more lines are too long
-37129
View File
File diff suppressed because one or more lines are too long
-12
View File
@@ -1,12 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
moduleFileExtensions: ["ts", "js"],
preset: "ts-jest",
roots: ["<rootDir>/src"],
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
transform: {
"^.+\\.ts$": "ts-jest",
},
};
+12
View File
@@ -0,0 +1,12 @@
import { createDefaultEsmPreset } from "ts-jest";
const preset = createDefaultEsmPreset();
/** @type {import('jest').Config} */
export default {
...preset,
collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
moduleFileExtensions: ["ts", "js", "mjs"],
testEnvironment: "node",
testMatch: ["<rootDir>/__tests__/**/*.test.ts"],
};
+4885 -798
View File
File diff suppressed because it is too large Load Diff
+23 -19
View File
@@ -2,16 +2,18 @@
"name": "ruff-action",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "A GitHub Action to run Ruff, an extremely fast Python linter and code formatter.",
"main": "dist/ruff-action/index.js",
"main": "dist/ruff-action/index.cjs",
"scripts": {
"build": "tsc",
"build": "tsc --noEmit",
"check": "biome check --write",
"package": "ncc build -o dist/ruff-action src/ruff-action.ts && ncc build -o dist/update-known-checksums src/update-known-checksums.ts",
"package": "node scripts/build-dist.mjs",
"act": "act pull_request -W .github/workflows/test.yml --container-architecture linux/amd64 -s GITHUB_TOKEN=\"$(gh auth token)\"",
"update-known-checksums": "RUNNER_TEMP=known_checksums node dist/update-known-checksums/index.js src/download/checksum/known-checksums.ts \"$(gh auth token)\"",
"test": "jest",
"all": "npm ci --ignore-scripts && npm run build && npm run check && npm run test && npm run package"
"update-known-checksums": "RUNNER_TEMP=known_checksums node dist/update-known-checksums/index.cjs src/download/checksum/known-checksums.ts \"$(gh auth token)\"",
"test:unit": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
"test": "npm run build && npm run test:unit",
"all": "npm run build && npm run check && npm run package && npm run test:unit"
},
"repository": {
"type": "git",
@@ -26,25 +28,27 @@
"author": "@eifinger",
"license": "Apache-2.0",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/tool-cache": "^2.0.2",
"@octokit/core": "^7.0.3",
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/glob": "^0.7.0",
"@actions/tool-cache": "^4.0.0",
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^13.1.1",
"@octokit/plugin-rest-endpoint-methods": "^16.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
"@renovatebot/pep440": "^4.2.1",
"smol-toml": "^1.6.0"
"smol-toml": "^1.6.0",
"undici": "^6.24.1"
},
"devDependencies": {
"@biomejs/biome": "2.3.2",
"@biomejs/biome": "^2.4.7",
"@types/jest": "^29.5.14",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.10.9",
"@types/node": "^25.5.0",
"@types/semver": "^7.7.1",
"@vercel/ncc": "^0.38.3",
"jest": "^29.7.0",
"js-yaml": "^4.1.0",
"ts-jest": "^29.2.5",
"typescript": "^5.9.2"
"esbuild": "^0.27.4",
"jest": "^30.3.0",
"js-yaml": "^4.1.1",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3"
}
}
+41
View File
@@ -0,0 +1,41 @@
import { mkdir, rm } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, "..");
const builds = [
{
entryPoint: path.join(repoRoot, "src", "ruff-action.ts"),
outfile: path.join(repoRoot, "dist", "ruff-action", "index.cjs"),
staleOutfile: path.join(repoRoot, "dist", "ruff-action", "index.js"),
},
{
entryPoint: path.join(repoRoot, "src", "update-known-checksums.ts"),
outfile: path.join(repoRoot, "dist", "update-known-checksums", "index.cjs"),
staleOutfile: path.join(
repoRoot,
"dist",
"update-known-checksums",
"index.js",
),
},
];
await Promise.all(
builds.map(async ({ entryPoint, outfile, staleOutfile }) => {
await rm(staleOutfile, { force: true });
await mkdir(path.dirname(outfile), { recursive: true });
await build({
bundle: true,
entryPoints: [entryPoint],
format: "cjs",
outfile,
platform: "node",
target: "node24",
});
}),
);
+714
View File
@@ -1,5 +1,719 @@
// AUTOGENERATED_DO_NOT_EDIT
export const KNOWN_CHECKSUMS: { [key: string]: string } = {
"aarch64-apple-darwin-0.15.20":
"cb41c48690c113dc08470e64103ce65ca15249c6ce3495d3ac792b329a83e8c1",
"aarch64-pc-windows-msvc-0.15.20":
"6472417dfa49885bab36b92286cf933497bade1a30c816e6aa92e3adc684131b",
"aarch64-unknown-linux-gnu-0.15.20":
"f915de3ab6d31a49f4c57b1f97129f359f9348c162ea03acfa07011ba79e1197",
"aarch64-unknown-linux-musl-0.15.20":
"2e06ebae862dce74eb7b084081621f2d4183e18e60cb074e7906f862869fa1b6",
"arm-unknown-linux-musleabihf-0.15.20":
"3fab24c45421c992c1a32da52473319969ad66069fa7b8ef96632e9f6edb48cc",
"armv7-unknown-linux-gnueabihf-0.15.20":
"032288e4fc032c0c6e7d790bdc3272beac8e35870fc3153b94804e906b73b489",
"armv7-unknown-linux-musleabihf-0.15.20":
"40a085563d348fb7ce3536ac5d422a3c8efd901218138397e02515f5498d993e",
"i686-pc-windows-msvc-0.15.20":
"9f575c9b1d7c7304535ff6e271e15adb71e89c57381fbc14be91c741e70cabe7",
"i686-unknown-linux-gnu-0.15.20":
"918f5e2f8d5cc8881388ae2f8b1faaebc6a6d93f325cc203e245782374ae74da",
"i686-unknown-linux-musl-0.15.20":
"c06588ab67ed9b89bf8e9a7e8cfde5ed39afb2cc0050b89e4f05e8705be39ccd",
"powerpc64le-unknown-linux-gnu-0.15.20":
"6005f0dbcf9526737e998b0e9643b45705a2a14196bcdf57522a1eb4c1e92606",
"riscv64gc-unknown-linux-gnu-0.15.20":
"f050ed3b244aed95998865608e6c5c07ddc871a8cba7b4bd167212ba35d7b3a2",
"s390x-unknown-linux-gnu-0.15.20":
"e10200d1436f39bba9b041d8f4ed86762ce799344f0af3cf625bd398713f4d5b",
"x86_64-apple-darwin-0.15.20":
"36b91219b3aae00464e2a4fa361766abd47d0402ac88fbe6da5de44285738386",
"x86_64-pc-windows-msvc-0.15.20":
"835fdea5b8d4e13d2cc49439006aa1fbe9b67a89371f03871fc7f70a0cf10516",
"x86_64-unknown-linux-gnu-0.15.20":
"df8e74862d4cd4fdac11faf3048789896ff9898a0cacb98497df20d0a1cc7bb4",
"x86_64-unknown-linux-musl-0.15.20":
"0715560b2402024b529a4ffd6ea7d3814a090eb255a6216259fb192823be3ad8",
"aarch64-apple-darwin-0.15.19":
"c7253342f2ffbe2a0eaadaacd3424c3a24331b0307e57cc54031bc7ccd35d83a",
"aarch64-pc-windows-msvc-0.15.19":
"66e3014fe7ae4424a0e7c48b524ab03e8e39a51012ba5acf656c3771fed24c0a",
"aarch64-unknown-linux-gnu-0.15.19":
"b00f8c17424c0abea8f87ca52880b864d10ae8e5c5d09ee61730a80ee3874a97",
"aarch64-unknown-linux-musl-0.15.19":
"d6945815c5b2b32d01483e1d59ac7d2e744df0a20272680b2290c3e5e4ce5020",
"arm-unknown-linux-musleabihf-0.15.19":
"a533fbfe66f4ee6318d4c49b315c83b2dae9e474e1a66cb89148a12574654bd1",
"armv7-unknown-linux-gnueabihf-0.15.19":
"3acea3a3808e724412d9abf898bd9228c6417bd9641168394a5db98c06669302",
"armv7-unknown-linux-musleabihf-0.15.19":
"1fd42881a29964d884d822b4b3aacf5661aba43c95ce6b333defdab468ba57c5",
"i686-pc-windows-msvc-0.15.19":
"6f89edb30cae3ba71176bdeb2a94d2ec6f2ddf3a647da61c45adf9b28efabd67",
"i686-unknown-linux-gnu-0.15.19":
"82b0a2793f9bc1205f6ac2ecf8b2cfdd4fab450b7b63688d030c1276d477b0df",
"i686-unknown-linux-musl-0.15.19":
"75876128126ceccd1374059b3aea2415c48aa735c8a0e3c16e2c48bff589f16b",
"powerpc64le-unknown-linux-gnu-0.15.19":
"ce4548a179737c1e633cadd879124d762ebe6a850b6f6619467db2f822f42df1",
"riscv64gc-unknown-linux-gnu-0.15.19":
"0032c5458e4515b1f3aea15cef4870d43a49bee55345994ee598e0e7ebed166d",
"s390x-unknown-linux-gnu-0.15.19":
"86a9df0baa93b3e000283d0e019f52b7e684ae9a27cd07f3a985d159b8f1772a",
"x86_64-apple-darwin-0.15.19":
"aaf50afd2a76be2674b8dd24b305e42a33a99269c2c43e334f6a940b29ca291c",
"x86_64-pc-windows-msvc-0.15.19":
"e996f09c5aa5b00675318f0cdb59628633a202219d9c929736c5dcaaad2a6c4d",
"x86_64-unknown-linux-gnu-0.15.19":
"0bfabff71f317e0e3b8c363043b888f045eff092073d5d9bd0ac7f8ede465711",
"x86_64-unknown-linux-musl-0.15.19":
"682158b99b14ce60b62e7b4585100a11ebd5716355efe4048760690eda21fb99",
"aarch64-apple-darwin-0.15.18":
"f0cc66c2df37f2a11c99a85b59b8fde21b37f61df062404a0afaed387b56dad0",
"aarch64-pc-windows-msvc-0.15.18":
"344835d508df8bd5fd30a2941a7cee95b9b4f0f6342886b5d52b89571614c0d2",
"aarch64-unknown-linux-gnu-0.15.18":
"c9636879455e940fc92edb95dd6e475d683da833987d1f0c147f39e521ae98a2",
"aarch64-unknown-linux-musl-0.15.18":
"941eb7a7b8bc11f195a08393bb45def1656003031cff89af3805ca89cfd066b4",
"arm-unknown-linux-musleabihf-0.15.18":
"e70e827e863e0c5efea70661c61fafc068ec630ecac529ac8921d6c83ba5bae2",
"armv7-unknown-linux-gnueabihf-0.15.18":
"7cbdcb38059ad6c4cad1ea61e7411eb550510c8688a045aa4aee7aff3c743dc3",
"armv7-unknown-linux-musleabihf-0.15.18":
"24e9f8ae7ca03639871299a9a55439593564ed6d6572dbc3649aa0d3bf35da19",
"i686-pc-windows-msvc-0.15.18":
"d33ccad071478e3faa106506a53c0a62780abbb6d8c3c1ed29a36d4ec6a13168",
"i686-unknown-linux-gnu-0.15.18":
"8ee90592d7b22bbb81c7ebbd08adcc7ba66154046e8539d793bfcb9b4e681dd1",
"i686-unknown-linux-musl-0.15.18":
"9854c1fd59e876d229075f81dee88fb29f88bdc1aed9bca5e899810dd640db33",
"powerpc64le-unknown-linux-gnu-0.15.18":
"6efa78ce85071f72f37f77fcdc51b39fd75efd795baaf1fe1d85a3d7cb1a2c1a",
"riscv64gc-unknown-linux-gnu-0.15.18":
"d9b15d6fcd2931b0d88c8e56fd49f67ce7d1d362ad4b2623678849da5638f423",
"s390x-unknown-linux-gnu-0.15.18":
"5699eba448816315ce7fb9489512f7823d5dc872d997641dbbc984c07a8a4799",
"x86_64-apple-darwin-0.15.18":
"229d2c192718fde76794185ab9339515b75a55cc1337e4ca9d60309e2c506900",
"x86_64-pc-windows-msvc-0.15.18":
"2c38db5fef5984c93db7bdc395c20c5c3470fafdd8deb72150db4d2da7870d07",
"x86_64-unknown-linux-gnu-0.15.18":
"1ffb1d5e04e60347be17c514cbb19303d6d8c75f629060ed481a150337dbf060",
"x86_64-unknown-linux-musl-0.15.18":
"9ca54aec52cfdbe1e708cc975e41e3e736a2c1ebe76668575efe09e0701465c8",
"aarch64-apple-darwin-0.15.17":
"81f372886fb7a0056949356c615fa689a091cf79b0b54ed914c810cdbc6d85e9",
"aarch64-pc-windows-msvc-0.15.17":
"827124e08fe4633e76a3186fb9e881485faf2057d8c12dd98924474d8b7d4493",
"aarch64-unknown-linux-gnu-0.15.17":
"71593a6ca85cfede1743b9163aa4531a273f0eed6ae6c99d26ffe2af51bb5a3d",
"aarch64-unknown-linux-musl-0.15.17":
"211d610dd81ef472d3b1404b28eaa19a62cb41ab9374584847307b97821a4ab8",
"arm-unknown-linux-musleabihf-0.15.17":
"0ad5f2c15c85f334e846396c261dc18c850ed8288c8ca58fbf736783fb38ff48",
"armv7-unknown-linux-gnueabihf-0.15.17":
"6a6abb8e81ee6e9fc1d389e39986698dcd2c97c98f983e6d0b1466791f9ebd64",
"armv7-unknown-linux-musleabihf-0.15.17":
"97e60f2f92df99b4a80a12fe97316e2f403336515e7719d61d9fae6cca9c84fa",
"i686-pc-windows-msvc-0.15.17":
"50b5ba7faa811d38993b3cde5d038480fa76c5af6b4183bfc28c9ad863c47a6b",
"i686-unknown-linux-gnu-0.15.17":
"105dbbb8d898cb7ffb5f5464af3126d4be7dd1a1e58d802d482bb7a3b9712393",
"i686-unknown-linux-musl-0.15.17":
"8803e601e51bb4e0fb6747272339394014d8447f231effb414e7937b9ea12888",
"powerpc64le-unknown-linux-gnu-0.15.17":
"202fc52df8cc69d8ffdadc9bc4f68af987f9991bf153d676db3e116aa7f82e89",
"riscv64gc-unknown-linux-gnu-0.15.17":
"cdd72afde5bbfc119fee54f215696db870aeb4893ae3c134c8dc83af3a69d9c0",
"s390x-unknown-linux-gnu-0.15.17":
"f73297ab1ed5b1bb733a04ad4bc87a30b0a8c16843d7b0981b8b9e1a3495ea3f",
"x86_64-apple-darwin-0.15.17":
"5522e517bd67ee8c2cb0d6a8298388d8edc8621a0bc5dc773f65e06c953693f6",
"x86_64-pc-windows-msvc-0.15.17":
"08b6cb2bd20f3d19e3cf6e768d8d8105c8337de1c52e90c7dc1deea483d68c86",
"x86_64-unknown-linux-gnu-0.15.17":
"7bca4641db4b0256d20f3a8c38057725e0982965639f5f9a0fb2f9aece4b7c4f",
"x86_64-unknown-linux-musl-0.15.17":
"cbf85fc152b7fc25cb27f0233b325e9c72cb26965152050164e7608b72515189",
"aarch64-apple-darwin-0.15.16":
"9deb6c4c38c9324bfd98ea4efb40946aae99a987e7f9d1308a1291f14f6b46e2",
"aarch64-pc-windows-msvc-0.15.16":
"d2654d546fa03618e4ce39a568e78a0ccda71319928832266009b286f57354f8",
"aarch64-unknown-linux-gnu-0.15.16":
"a39b83af7288da13c013a789af1a4f875562ba09fc8011a2825a4883cb3a97a9",
"aarch64-unknown-linux-musl-0.15.16":
"3b39fc2608e12c554d42f6950a5ea131db8d8a0736c8949b3581c361f9f011d4",
"arm-unknown-linux-musleabihf-0.15.16":
"30d04d1f068a443fe6a919a9f0a77a3e59deab515b6b60c07a678d94332d444b",
"armv7-unknown-linux-gnueabihf-0.15.16":
"1f32fb51a347b70faa7d248ed524954736a79d3fd00887f79ee8c22004c8cca5",
"armv7-unknown-linux-musleabihf-0.15.16":
"5d14928f26c14bb40c7ec0b9db1f7e05e6e6b21ffd499dafcd77d0575fa796d2",
"i686-pc-windows-msvc-0.15.16":
"a68887cad08f7cf47e3753b1c84add837a64d9070102f43ec29d2a9fc8ed144a",
"i686-unknown-linux-gnu-0.15.16":
"3afb75f98244d7aec2532b5140da8ec9f2f9422ae27d7ba42bd6f8b20d76547e",
"i686-unknown-linux-musl-0.15.16":
"6fa9f045b0893456476836333410af37e52c32ecd511046d177802aeed42b2aa",
"powerpc64le-unknown-linux-gnu-0.15.16":
"adeee21392ed843e1e30e64a1b238d844c00385ac983fbc231b786cfe5d3bee7",
"riscv64gc-unknown-linux-gnu-0.15.16":
"e8e04a9d0664bd78e5164be9376091dbeefc455cdb684814a949dd7ebf5daa88",
"s390x-unknown-linux-gnu-0.15.16":
"2bd5e6a045f19a593b269d362c07b4bcc68b57b73d2ada1eecf3618d0cde0237",
"x86_64-apple-darwin-0.15.16":
"a8a7a60182f66b5995bd16e1831cf013e89558edf52135a1b6646f155f491f98",
"x86_64-pc-windows-msvc-0.15.16":
"b6c843fa84afb37af9d665a418324782ae18ea3dd1de775abfd7cf17f9431f85",
"x86_64-unknown-linux-gnu-0.15.16":
"b949a853b208b0f818a150fb06490487db585a15becaeda28483dd662939030a",
"x86_64-unknown-linux-musl-0.15.16":
"f52d90f8a6b1b3ad7d74301c3c796652e851d8f05b6ba26d139f05f4838cf412",
"aarch64-apple-darwin-0.15.15":
"2f7063e09d3c2af019b56f4fc6548199851e9387f359d696111af312adfe7619",
"aarch64-pc-windows-msvc-0.15.15":
"801067abc7c499bda61ff7308a271f7b5258c1a9f90abb081d9ac099cee20720",
"aarch64-unknown-linux-gnu-0.15.15":
"d334ae13383977fdaf754e6ea3d85c7b3ccbd510c713463eca73d0cd7151fe12",
"aarch64-unknown-linux-musl-0.15.15":
"46ea2750c0f21dab01f853d55a521a5f388f8573dab698735effdc6a94c9b293",
"arm-unknown-linux-musleabihf-0.15.15":
"daaf7d2b228c6b60ec30a3fefe37552d1b2ad50fc47d416dfbe04e80c78e4017",
"armv7-unknown-linux-gnueabihf-0.15.15":
"752ed1eef8c0b3550c21e9682a4eb873dfe1e08e0c23ad59a1ff57212e2665b2",
"armv7-unknown-linux-musleabihf-0.15.15":
"dda544c4890d46ec5d0b4a553087036ba9c50df6f566c54b38f8612185cd49c9",
"i686-pc-windows-msvc-0.15.15":
"57abdd72d19a6dc81e1583b40fcc5c88584cbee4406e257c8002e4ab4f825429",
"i686-unknown-linux-gnu-0.15.15":
"0c9ea8fa37a517cd833c8cdc26646ae2321d0f1f5f4359b00c3ae14b39c09f5b",
"i686-unknown-linux-musl-0.15.15":
"37a77abef48e92d43755c268ae04cc395538f630a9f295de9d2afd401965551d",
"powerpc64le-unknown-linux-gnu-0.15.15":
"ea7ba10a14ec3ef32275359cb8200775b6875f949352df3438f0c8de5b4ab514",
"riscv64gc-unknown-linux-gnu-0.15.15":
"486c47b6441134b0f8e31cd4b5e12cc8d4501e5c19ea1d68523c60b7300d4344",
"s390x-unknown-linux-gnu-0.15.15":
"d1834ddb58e5e73c6aa4c080bc3e1da4f4789150bb2d1d74824ff1bb52d5ddf9",
"x86_64-apple-darwin-0.15.15":
"e43d7c03701c7a436e18da455baf29e4be3379fb70887c4dc65091dfa73c1747",
"x86_64-pc-windows-msvc-0.15.15":
"c336db29a9e9234310212bc0d0bec1f722ec44f8a62c77b3f96a300282c6ff6d",
"x86_64-unknown-linux-gnu-0.15.15":
"2c50f95a0f553731c87f8dc413465aa06059a0dc21dfe16786682db95ffeb9dc",
"x86_64-unknown-linux-musl-0.15.15":
"c09b24fdec07d8ea73f7cbb32c38eb4f36f52298ac445dccf763171c898d8fc0",
"aarch64-apple-darwin-0.15.14":
"953243859467ed3f4f2bc6c11e241cd42a11ed42605b33d971dc2b174772da7d",
"aarch64-pc-windows-msvc-0.15.14":
"2f7a3fc221ca307c441306eb8f66740bfa129835a921862d124474080311933e",
"aarch64-unknown-linux-gnu-0.15.14":
"9740a687b333e5e9b3764a9d09001268bd841c93140a0766ddceef53fd1ccd87",
"aarch64-unknown-linux-musl-0.15.14":
"1693dfc1f402a81b32ff4fee5f8b494857a1a857a2b8d63cad88d9d39c201297",
"arm-unknown-linux-musleabihf-0.15.14":
"3107f1e38cd07aad9567b1eba6fcce3d7f7af2c011dc665c9f733c650e92f1c7",
"armv7-unknown-linux-gnueabihf-0.15.14":
"f198a9b46f860d1eba5fdd869fe12611f1828a564bd55b00a70cd8ade791717b",
"armv7-unknown-linux-musleabihf-0.15.14":
"2cb7aa2aa1b3f0762e69a76d0e636f39d9a87f554c24a705c2b9c05b0bb824e0",
"i686-pc-windows-msvc-0.15.14":
"c746d60bccd1a38e78893f683bb8b3043e1c6cd4ebe7fc4d7c11ead639808634",
"i686-unknown-linux-gnu-0.15.14":
"34f99c35d9efbb4491f252ba565edc5051a879371829ee05b9f8b96432dff466",
"i686-unknown-linux-musl-0.15.14":
"bb8b59953ccfb08a60c07666e07c1b830c4907ac8d0036bdef416ee614dd5fc7",
"powerpc64le-unknown-linux-gnu-0.15.14":
"a25f2979686c99d47c1c4fb4a7676e015ac492682fdff7b05c078e253bd22c70",
"riscv64gc-unknown-linux-gnu-0.15.14":
"757f852cfd037bae48186bbb78bf683197a21599d3c0f063c2f7d3be6f3a5d80",
"s390x-unknown-linux-gnu-0.15.14":
"d8dcf7282d04eb44bb0717892605c46d9a26d35c641819cdef77b3a6b9b6213a",
"x86_64-apple-darwin-0.15.14":
"537f569780fc71f63238f1f43dc04285b24fddbc6eea7219a9a18f408bdd869d",
"x86_64-pc-windows-msvc-0.15.14":
"c589a77b0e112c374e9a5e0c42a7fb18d8725c5322ae261e343f66d1c87f0a5e",
"x86_64-unknown-linux-gnu-0.15.14":
"e9fe1bb63fb8950cf8596d069f5c61b5a962bdd6c75edbdd2c8320871c40d6dd",
"x86_64-unknown-linux-musl-0.15.14":
"e380c9cf14764b7134434c49d445d13c6e3db732cfd18d72c4bc2316a6dbb82e",
"aarch64-apple-darwin-0.15.13":
"66ab3abeeba319266612e675dfbe0ca9c63f1e5f6ca3c6174482b0b088b8f6ff",
"aarch64-pc-windows-msvc-0.15.13":
"b16d436afac64f8032831d2f7874fe5fa6b1b238bfae97924d2233904372deed",
"aarch64-unknown-linux-gnu-0.15.13":
"8b8ee2a0f7e9ace1a734b2993ab2bfd68112d8a9c151ffa3025dbccaed4da52f",
"aarch64-unknown-linux-musl-0.15.13":
"1084d8038eeb51f55c7b7c9779bb37e5f53b28b987767df61333e9d0da7b2701",
"arm-unknown-linux-musleabihf-0.15.13":
"2b5b01e5e4ea6b72d75f20b7eac05305b29057e983c42839d62a7a5b9059867a",
"armv7-unknown-linux-gnueabihf-0.15.13":
"e579f3068d8011dc8fb845e3939407facb2fb129a5bc31c7db216a5a0e9e15d6",
"armv7-unknown-linux-musleabihf-0.15.13":
"d470b0fa4ba9cf94675dd8769f44bcdd16970fa6d8f48c34eb576299638a0742",
"i686-pc-windows-msvc-0.15.13":
"418907ac98fed6d3a4011d9c3f4b6bc525d4572aa926368f7b37311bb163b15c",
"i686-unknown-linux-gnu-0.15.13":
"8f7dc9bf2f7e1cbb6cbcb009d3759614085a32658271aff97eec6f51fa456eab",
"i686-unknown-linux-musl-0.15.13":
"0ea01a5614197f7058af27fd067ae8a178f6d2e3ce3114bf22f74921289cd38a",
"powerpc64le-unknown-linux-gnu-0.15.13":
"bd07a50f51069f130c8a66472260a6acc88a81735f92c40b5c735119f4155cc2",
"riscv64gc-unknown-linux-gnu-0.15.13":
"e689a47ef17dd49c75d957b11f95b0bc102d8240817e209c0fea2907ea638a71",
"s390x-unknown-linux-gnu-0.15.13":
"879a0888431b78fce4bd934d794921723d549c558021b8f7b3fb952be05abf53",
"x86_64-apple-darwin-0.15.13":
"3ff692a61d5231ea6c94e6dafc575f87582a089d1281fac6b31cef2b4f2e4b6b",
"x86_64-pc-windows-msvc-0.15.13":
"59892c6773df805effe7928e69796447585b42af54310cb6f1552536b746c39a",
"x86_64-unknown-linux-gnu-0.15.13":
"fd09d74c60963714b9b0fbda226c669e85624e5a8488398b3e0d0b5ee7684dd1",
"x86_64-unknown-linux-musl-0.15.13":
"66ac0b0582c8d24badc210b8e933e9aa0ad6707414e236409c9b9ad638972fb9",
"aarch64-apple-darwin-0.15.12":
"bd59107e125f9c9f64a75be13b6ff21bf7cbc61c4b9529bbb75c8184e3dd6c3c",
"aarch64-pc-windows-msvc-0.15.12":
"310a94d0c708691905d7614385732a2cbdede0979b93b81c01d55ab9de7ae754",
"aarch64-unknown-linux-gnu-0.15.12":
"4dcd4cd8beb525d04294eeab67d84d199cce2c44b66858978ca729b694fe29ff",
"aarch64-unknown-linux-musl-0.15.12":
"8996fe01814d00480c81da2b8ce9e8274f63eb07992318f082f7071772699635",
"arm-unknown-linux-musleabihf-0.15.12":
"aca8dadd6bd0e7c2192bbe5cbb39efdd5bbca47460c0ef73e6102a8c858890c6",
"armv7-unknown-linux-gnueabihf-0.15.12":
"3b8278e41d29ecac9f9ae07d95f8dc6707c45a28322503ea48d8c0cae53056b4",
"armv7-unknown-linux-musleabihf-0.15.12":
"d60776dd376f82f8bbbf346e1fbb81ef844a6445068b0594e36102c1121f57e9",
"i686-pc-windows-msvc-0.15.12":
"a466127b868dc22095dc717856cc94f283a01ccffa3a9814d35b5d42661e73be",
"i686-unknown-linux-gnu-0.15.12":
"64c4c076d6cc45f65b058bf5e2c3dd3150f80964a5a47f76c4245cdaa40ff7bb",
"i686-unknown-linux-musl-0.15.12":
"44462b0989328e7f7bbf4396e6d18152909ffeb43f49991e5b2f9fb28738af6d",
"powerpc64le-unknown-linux-gnu-0.15.12":
"36d42837ce7526663c707572c91615f4b1457c5d22f6ec4f96ea8921f83cc38d",
"riscv64gc-unknown-linux-gnu-0.15.12":
"69e5f9d477ea238f7fd00c2ac39ea78a3d7c7369124f62620a8c8a6b195d4f3a",
"s390x-unknown-linux-gnu-0.15.12":
"a612eaae54928a170403de322ab659d82fc233a3d1bb0337c8c39b48e9e5b41f",
"x86_64-apple-darwin-0.15.12":
"f88b6ad8338ad7acf83497e4bc2f2b5d3cac46c3e71363b7263c1bb9027c9b37",
"x86_64-pc-windows-msvc-0.15.12":
"d378033392042377491ef2541457af47cc4652c4e984639c95413b0c4876b0f1",
"x86_64-unknown-linux-gnu-0.15.12":
"5e26a7811f7db364864ace00cced53003556a37b63f3c987e340b18207776f1c",
"x86_64-unknown-linux-musl-0.15.12":
"e0b074f722f87efe2e09de3a77820a012d1417ebabbd308cccdf6b98ba8d4bb3",
"aarch64-apple-darwin-0.15.11":
"89cd4370b5c5d7ab01f436bd5d878293c9858e4e964a3da3013c49549198dc8b",
"aarch64-pc-windows-msvc-0.15.11":
"35ee4e8f0e784100aa1f3325705f2a764319d673fbeb8ba74ce169d4da13a675",
"aarch64-unknown-linux-gnu-0.15.11":
"6c7004eb732951ecce2059b3d5ef93010f8410ececaf96c24dbac41155780eee",
"aarch64-unknown-linux-musl-0.15.11":
"4d196496a51d4bbefc4a2a65049364c920b470afd320befa20516e59d1c065e2",
"arm-unknown-linux-musleabihf-0.15.11":
"caf1d8a5fbeb85389f9aa354bb9e5444f8d16ce7c6e5483681ef83ec6f9f7aa5",
"armv7-unknown-linux-gnueabihf-0.15.11":
"c8733ca5191f6503853a1f908e005ce9f2984ed58623f7544c56c28ce5d737db",
"armv7-unknown-linux-musleabihf-0.15.11":
"758d7fa7f7258dce3074a17432d6425bd6eec3be0a0558bf46729bbcf011cae7",
"i686-pc-windows-msvc-0.15.11":
"4af40e2bd1c6044ab1e0cb94ed836773237a4d88697a72c0babb16aede6e352f",
"i686-unknown-linux-gnu-0.15.11":
"db2c037d01bef8a5bc790f10fb7bfe82d97d287b5cda88acdd3a8d0bfcf20069",
"i686-unknown-linux-musl-0.15.11":
"c0c8456deb03520b4c8cea4f7dcd6e1fbaaa810a751b031fa79db09c41eae800",
"powerpc64le-unknown-linux-gnu-0.15.11":
"d68248684191d453897040fe5aab170a5bea7298fdaf0796f6eafa17d820e3cf",
"riscv64gc-unknown-linux-gnu-0.15.11":
"33beb06489d7b4b8295c0a696ef2125afe6f31e8e9355ae61bff6aa46b118702",
"s390x-unknown-linux-gnu-0.15.11":
"8b20efbb2a004fa27be6fda2c821a82d55cfaae5a0878ad210a12d976e3f0b60",
"x86_64-apple-darwin-0.15.11":
"c6f2c36fc5f3d14ffc2db1e205aa69756de485771f39848afe848c16b8a1a648",
"x86_64-pc-windows-msvc-0.15.11":
"d1f76eac07c279aafc594d9ba4c6f658f7cb433002aeeadbe2959229fc368881",
"x86_64-unknown-linux-gnu-0.15.11":
"8018f4c746c98e3a4128295c5da294d8b04c2bb14e1868e2da6fd02faf93ebe4",
"x86_64-unknown-linux-musl-0.15.11":
"ea286f17b66054a3aac2672158b9ac194030d89043ff108357edd9b20123219a",
"aarch64-apple-darwin-0.15.10":
"77c1df502dcfaaec52c6ce203b504b8554c88ab66ac01313410fa68ad9aafd5b",
"aarch64-pc-windows-msvc-0.15.10":
"1776bf104277b3fbb3b3e4b481655f492f6df10210e2e00cd94132e66e999bd4",
"aarch64-unknown-linux-gnu-0.15.10":
"b775a5a09484549ac3fd377b5ce34955cf633165169671d1c4a215c113ce15df",
"aarch64-unknown-linux-musl-0.15.10":
"84754f0e58f58fb123ea49b8d22b8ce2cc96e4046b7c1b1ed99f6af7fe76f8ef",
"arm-unknown-linux-musleabihf-0.15.10":
"e94eb3061a263217fad7219a0b20e5a746a31f44387cd21c31eb5103357dbb8c",
"armv7-unknown-linux-gnueabihf-0.15.10":
"7a348dd60f4c5563482504acabba49def8a8cd5d957b6e145a1dbdef7cdf0663",
"armv7-unknown-linux-musleabihf-0.15.10":
"8c85562d2ed09bc4edbbea151aec386e8f7e9f5499e6c5d0324fd26fe7c5dc8a",
"i686-pc-windows-msvc-0.15.10":
"a8b4132914f197d1fef5f48fd7f0f8e840546a814daf9f680109344407da79ac",
"i686-unknown-linux-gnu-0.15.10":
"6f9b23d07d90ef3ac148c8b81fc8ea37647f1241e4db18be1b0a24df43d479f8",
"i686-unknown-linux-musl-0.15.10":
"63d80b9427a8299c8bf08d34621e187fe5fe5f696f36b54635e00248d3ca7e5f",
"powerpc64le-unknown-linux-gnu-0.15.10":
"49cfcb83828844f61e00c643dd81182a015140b9bf58cee5b115f705e99749e6",
"riscv64gc-unknown-linux-gnu-0.15.10":
"8d7eeef9d9abd9e88b92d9420021be5fbd1dbd60e10ef7f989060213cd68c4c7",
"s390x-unknown-linux-gnu-0.15.10":
"80056787672af7b4e9217afdd666a8c19eebbe5eb2fe626a6a9a779ea6778a1b",
"x86_64-apple-darwin-0.15.10":
"7210e06196de876771cc0bad0f1d57678e709d039f184b491fdaa600d6a95a5e",
"x86_64-pc-windows-msvc-0.15.10":
"6f8f9a445102107ee3c0a05c8f386bacb32238199ecbc0983b9b06c5ea3d7c5e",
"x86_64-unknown-linux-gnu-0.15.10":
"e3e9e5c791542f00d95edc74a506e1ac24efc0af9574de01ab338187bf1ff9f6",
"x86_64-unknown-linux-musl-0.15.10":
"8b0a16bae81e371c9b6176a27fdb9db1deaa04c4cfe87e8604a898cc31686500",
"aarch64-apple-darwin-0.15.9":
"013d878f17c625550e4a6b19235c22fc229639f66f563bb72cb2c896aeca11e8",
"aarch64-pc-windows-msvc-0.15.9":
"813c3b9cf0c01ef913bfbc8d2dd17e140a8c3d54ae1b8f8c20ac53e2871674f0",
"aarch64-unknown-linux-gnu-0.15.9":
"ea71b14433318bed364e0dbb04203e57027cf134ab909d5e452be28d87d0fd08",
"aarch64-unknown-linux-musl-0.15.9":
"e017dd0c1fd7475aaddc49bde8cddcee3c27d42f6ce139a96df0c1022e06d85b",
"arm-unknown-linux-musleabihf-0.15.9":
"593382b4b5271cf63b8ea9972c7475a299f341ce8a9c22127ce1f8b7d82fcfe1",
"armv7-unknown-linux-gnueabihf-0.15.9":
"3c12ad897c62954fdc6a5c0f7baddd06f1cb37f1987618fa243ebd00a7b67d86",
"armv7-unknown-linux-musleabihf-0.15.9":
"9d9ebfe0075c7a6411f771ba79e210a3dffc4ba9706b542c5db72dc39d922893",
"i686-pc-windows-msvc-0.15.9":
"46cdcacb4522e19a26a18d586abb6addc68b4254bea958e93a347d7566add1c3",
"i686-unknown-linux-gnu-0.15.9":
"c04ddaa542d36b0483e325d29b00520242cf6b4e78e4cea0b73f0c74c77459ef",
"i686-unknown-linux-musl-0.15.9":
"97145060b15819b7d31df7c3fd160b02397e89c40069d485915ce6fbe92cb769",
"powerpc64le-unknown-linux-gnu-0.15.9":
"56e522a316427281d590aff0bcece176aedb15e9329799c1ada5cd8fd5e17b71",
"riscv64gc-unknown-linux-gnu-0.15.9":
"e0499cfd557515133118cc4dafb65a805c9656833a66b2fddaee9a9f8e847de4",
"s390x-unknown-linux-gnu-0.15.9":
"7402b9bdb1aaa79387f5b702696ec75d9ebed26f0a8e5097fc58924418c92097",
"x86_64-apple-darwin-0.15.9":
"7e0fe9daba25848f85cb3d43e47ecd7d23f14e92e8799f92c1bcd8319a4ce4f8",
"x86_64-pc-windows-msvc-0.15.9":
"e38fddd19805bc8f7329003c2abdaf49d8ca9e5bc0c6702e8472e16f127bcd44",
"x86_64-unknown-linux-gnu-0.15.9":
"223ce40fbea2245b0a650abf9f5093a6009b56a04e5e63c036f446cab328dcf7",
"x86_64-unknown-linux-musl-0.15.9":
"e30e6e50dbf925b42335f28e2fa296d404294f294159b314dca47b88317fc477",
"aarch64-apple-darwin-0.15.8":
"94fc061f928c8f2b04c4b3a98aad2b1b04f38b4c808839bc5b33a2f0a63a47a3",
"aarch64-pc-windows-msvc-0.15.8":
"5e2941bff2f14fa9b48532bba67d1bbeec2c26d5fecc5a4bc0f76d813ad644dc",
"aarch64-unknown-linux-gnu-0.15.8":
"7df2a2c86f1017936d8ce7b74d451ed05f2c648af8cf89add7ac0e4f3635f386",
"aarch64-unknown-linux-musl-0.15.8":
"15e6a6c21696bbe59c56d0f1c437452b960bcdfe81ecc3bc19fa89e6a7d70eb6",
"arm-unknown-linux-musleabihf-0.15.8":
"49847e5d218aa17da9be67df543d7c635e67558356e67406f97db0742776ed6b",
"armv7-unknown-linux-gnueabihf-0.15.8":
"4116ec65accfc2463a30720407f239339f72b1e40a66b341a9450651f3b43976",
"armv7-unknown-linux-musleabihf-0.15.8":
"e35ad1094bc8ef40b4d60fd32dbc6b129c4d9d4283f7ac3c838189f614c81d83",
"i686-pc-windows-msvc-0.15.8":
"01fd5224726810986121e1618602f2c24332a6abd2476d37c73d695be36679a7",
"i686-unknown-linux-gnu-0.15.8":
"66bf0839b384700624d946390ceb4493a787b5e56b93dd17795e2d5111db01f7",
"i686-unknown-linux-musl-0.15.8":
"098040cc3fdcb01efffbefa83bad0ccf4ecbcc00b7d0b5489f405d633fad1d5a",
"powerpc64le-unknown-linux-gnu-0.15.8":
"b3664a2793b002ef65888eb2818d449ede3d34b16ee71f17e8f1851b3d22eb23",
"riscv64gc-unknown-linux-gnu-0.15.8":
"7557039907a61cd7f1c7bad06fd77bdc38a7b2436471fe0b58e662d78ae13964",
"s390x-unknown-linux-gnu-0.15.8":
"127a53b418e6828816e156bf7ae959f70394ed67882bf9d69bcd15f3b34c6eba",
"x86_64-apple-darwin-0.15.8":
"153d1801068df606290e832058ce2e5601584ac302788a055d0390adf6c772ce",
"x86_64-pc-windows-msvc-0.15.8":
"1e6ebd021dc9cefa8b9f15b5d6500c275ec49a0f2da968824845c34f30060c78",
"x86_64-unknown-linux-gnu-0.15.8":
"45a6ed01c5b32873d4f6239f5fd6d9adde782295261b4a65962d754b3e37a849",
"x86_64-unknown-linux-musl-0.15.8":
"d541beae99d550ed4abb3a1d026b907886c7cdf44a533b24624871e3d8c81330",
"aarch64-apple-darwin-0.15.7":
"e573cdb504fce521af501cc16b7018fb6560ac0e7af5d05056c942b3a1ad5a79",
"aarch64-pc-windows-msvc-0.15.7":
"beb2eb063e52f197694fb79045cef276735a7becbbd8f8f79e1c99613a12d7e7",
"aarch64-unknown-linux-gnu-0.15.7":
"aee72470734c4220d367b269c3e901b7134485ecc70fe8635ff8141f09d9d11f",
"aarch64-unknown-linux-musl-0.15.7":
"a6e07403e1a2287ba87676e1e9fd3c6f392d2042ed4eb0ca83cda9d549926bfa",
"arm-unknown-linux-musleabihf-0.15.7":
"d5f527f70067a4cfaa7218383781ee5e0a5dcc0bbe9122c4fa8f6d1a88a991f8",
"armv7-unknown-linux-gnueabihf-0.15.7":
"0daac568f418577bc5e31c6c5d53f590c99dc42b740310a627ba0dc99dfcfe66",
"armv7-unknown-linux-musleabihf-0.15.7":
"9fbc574fcc28e104d0da617ab2655c904185f990660029483897aa76d2e60cea",
"i686-pc-windows-msvc-0.15.7":
"d6e02b3f65c29c64f443d7f0ebeb3a5a08b12fccfb3325d7ba2e026dedd2855b",
"i686-unknown-linux-gnu-0.15.7":
"18aa2cb27a5e383574189bf1983c9fdb9b1c6b9c1331f90cf03a5b0032c8325b",
"i686-unknown-linux-musl-0.15.7":
"02b6f4a2f6abf35e8e657e579e91718fbb36cf75e8f3095f13908d3d33dc4e66",
"powerpc64le-unknown-linux-gnu-0.15.7":
"ab153c03e331accb127098a8467e415524d8b2035a277f250e0b50908588bcf4",
"riscv64gc-unknown-linux-gnu-0.15.7":
"cf71700aed80851a8d32cd663e00d22b6a3eb03e17e057e0a63c7aaff4f20c57",
"s390x-unknown-linux-gnu-0.15.7":
"c854a5cf6537eeaae380d1b4515a9f09a8ee2da45ed71cc230b39b0b6d18638c",
"x86_64-apple-darwin-0.15.7":
"2cff55e19e759addc871a8ed13cca38a3ad576d483fd2092b8072bad972ecfa0",
"x86_64-pc-windows-msvc-0.15.7":
"2971a4d16a6b20efc2e51c1f4290ae5d4a85935bb964eb298c9b73f682160379",
"x86_64-unknown-linux-gnu-0.15.7":
"2253ba7f064023def4b77aaef127756d3724e92a0fc69d666d4692f5c019af6d",
"x86_64-unknown-linux-musl-0.15.7":
"a70f33ff907cbd05cd3fdcdd3c5dde828123295aaf46b4011e82dce8f806c3cc",
"aarch64-apple-darwin-0.15.6":
"e90a351ddc2e5e411168b5cbfc7d694231e793948d0a29aa82618ea6f17225f1",
"aarch64-pc-windows-msvc-0.15.6":
"5e5e28e52fd3246f89d5b46e168b485cb9cdd76cda95b5a46a5d130d7bd85afa",
"aarch64-unknown-linux-gnu-0.15.6":
"b9183a35a2693941e1089a5a80fd45724df886c135beebb0f8f1e103704af25e",
"aarch64-unknown-linux-musl-0.15.6":
"750164384c78e208b5e5bf764916f23270844c6a6ea0057dec1f7951bce02fca",
"arm-unknown-linux-musleabihf-0.15.6":
"8553be3deedfdcecda058bb1e12e06d71ad56a35cb5b1e5a935a71c80f3f2001",
"armv7-unknown-linux-gnueabihf-0.15.6":
"f778861adf8448f07c12100d7d058b729937c282319c1bbc396864ba658daa77",
"armv7-unknown-linux-musleabihf-0.15.6":
"db109618d4472d103582428ded89a744c3b6aca56568dbde105eb1e792a06404",
"i686-pc-windows-msvc-0.15.6":
"5d964d1001a685ecb0ca7d25d0a0584535d4e911140f2f795cfb400711d58f05",
"i686-unknown-linux-gnu-0.15.6":
"169690de71a2ef6fc2fdea5347f2309d2b016fda4b5d445ce7fb2c9a9f6375a1",
"i686-unknown-linux-musl-0.15.6":
"84284d143429d886c9a8d38d44770f5f64cf3486ac497df16ac0e90e35bceaa4",
"powerpc64le-unknown-linux-gnu-0.15.6":
"c6322e95b105fa14728a68993fdab25591a1eb0e9de0c5bdb35081c0bec7bfda",
"riscv64gc-unknown-linux-gnu-0.15.6":
"7b714890a8de0af70f6e48d4d37b33808d57d343c723a136bf0a68fa16e2c1ab",
"s390x-unknown-linux-gnu-0.15.6":
"c5f7ac20b04fe31471ee255d8ea27b77b9e4b29e2697492460c977f5e9ff8b24",
"x86_64-apple-darwin-0.15.6":
"aef752bd803e58a732651d7ecd8434288f2ce7f252e415ecfa5b4a1518aea218",
"x86_64-pc-windows-msvc-0.15.6":
"34548ff94a9f52c96c75672d34eee6068a4bf4fdcb3ca12d10e9f4b8abedad96",
"x86_64-unknown-linux-gnu-0.15.6":
"c253b106eb136f9cb4a319b5e3b4a9df78ec31fe15a3822efb69e7887ef9166e",
"x86_64-unknown-linux-musl-0.15.6":
"7ca0d590da2274429e8033cdaae1d923229ad19b2df7d8c2c3b94451527ab45c",
"aarch64-apple-darwin-0.15.5":
"f39400f9504c940fa4eb46b02e2d4889a9f86b81ce4c57bda02ea3568894d094",
"aarch64-pc-windows-msvc-0.15.5":
"c2d4ddc435005768f2cc2c87651474d07c20b9f18150a772b56485966840f7cf",
"aarch64-unknown-linux-gnu-0.15.5":
"cd01e3c9df2d6c1324744883a796e6693642494f2f6222a4f158da36f84908ce",
"aarch64-unknown-linux-musl-0.15.5":
"ae22fb3b6ad85cff59abf147d572d266397f42b73e51e6d55dba56fb3430fe1d",
"arm-unknown-linux-musleabihf-0.15.5":
"a3480f0daefa03518b66fd038cf8461348cbc8efe5e29dc01bd68c89e98f9c15",
"armv7-unknown-linux-gnueabihf-0.15.5":
"cb8cbe66b2a5face3bc5a94e565ffba313e428f75df4cd62ef84beb164dc83e4",
"armv7-unknown-linux-musleabihf-0.15.5":
"adeb46fa64c7434503c9958a99a84b3f523ce43f9a1233b54008137101e1c348",
"i686-pc-windows-msvc-0.15.5":
"aa23d64f605deaa39d0fdccd7b63a4cf12f447013b8791deb79c236dc75364b9",
"i686-unknown-linux-gnu-0.15.5":
"de7fa298d723016be9a8b025524bfa13faa7f91d36472fdeb4fbc1646be4327e",
"i686-unknown-linux-musl-0.15.5":
"96398e8a459cd5960aa654dcd0fb158e80cf91f5b1684dad1ee1582158e92683",
"powerpc64le-unknown-linux-gnu-0.15.5":
"c4b959cc3190bba098596435252ce300bf9e4a4650c7d5f7ebfc94ef0ac8f1ba",
"riscv64gc-unknown-linux-gnu-0.15.5":
"1a719ae01affd85902322878cb942ce13112fc32c5463146e1613752b47ba538",
"s390x-unknown-linux-gnu-0.15.5":
"590c125b598fcc84182627d46bd1de22380c95825eea5fbf15b701adbb6c3557",
"x86_64-apple-darwin-0.15.5":
"1e01b0d6354f14dac0f4de7a89488d18e00f8aa37ce3635b55bcaf1357b2a375",
"x86_64-pc-windows-msvc-0.15.5":
"9b71926a755bdefa151b7579cd5e19840e9df0ab2828c762241f1b773882d939",
"x86_64-unknown-linux-gnu-0.15.5":
"ac0336520fd986b3bd7c510b1f9d3049e0d150ddaab754cfa5c5eaf8cbb840ce",
"x86_64-unknown-linux-musl-0.15.5":
"da9b5c8ba7a789fe3bcf6287ea58ccbac9328a711b7674681706106e7580a836",
"aarch64-apple-darwin-0.15.4":
"2d63cc9fd12c9cc3b524563bbeb50470cf3f68f3194002228a417a53a2a56164",
"aarch64-pc-windows-msvc-0.15.4":
"7de874b0d667fe04c2cd15629c19baff6dfff55e1fd99dfb14cb9850b09e7a20",
"aarch64-unknown-linux-gnu-0.15.4":
"7e436cedadb1bac0166448b05c0b5d69bb1d7879b0b26696bfc198ebdffb7b2f",
"aarch64-unknown-linux-musl-0.15.4":
"f34909dacbebaf3773ececc0d321e0fa0599729e9b4570f1dbcec91b4c435913",
"arm-unknown-linux-musleabihf-0.15.4":
"c6b3dfa714d8f2b27db1339b9730d2ff41cff513069c2980870dc1db0c8e6267",
"armv7-unknown-linux-gnueabihf-0.15.4":
"98c3f14394f9300a66c5415b7daf71959fc72be40ad5654cd5f90e9049eeb328",
"armv7-unknown-linux-musleabihf-0.15.4":
"1469ec15b7c7a918bc2f34ece38549f5ab77b535c65e1fe983ec167f6afa9966",
"i686-pc-windows-msvc-0.15.4":
"6c3090b5724dd664b742165fb2dc92b1a8e3c19e84122dab1d2e121982fe1c62",
"i686-unknown-linux-gnu-0.15.4":
"db95fc72c326f8712a02bdf980f42cccd9745ed2de2f6a8ae02f5be123691707",
"i686-unknown-linux-musl-0.15.4":
"e54933995834a628dbaeeec26ec96ef506cbeb673c91666f08be36fd07658503",
"powerpc64le-unknown-linux-gnu-0.15.4":
"b7c3eb76a448e47df049ec721af91c5f5972f9dc6583c3bc6583a9d29c932a2b",
"riscv64gc-unknown-linux-gnu-0.15.4":
"7f1816df2f56e606a17d39c405295ede5c068566630e9112964ffd3977050d0a",
"s390x-unknown-linux-gnu-0.15.4":
"934a870f75f006fc9c151f9d65d604bb78ecf8ca8c3f3b9eba2b3c1464116808",
"x86_64-apple-darwin-0.15.4":
"f40e16784c867b60850fbe96a2cccd123589c90d6db71ad8ade62efdeabccc84",
"x86_64-pc-windows-msvc-0.15.4":
"ca4db783ce3a1b942e67aa4002ca9f3c6ff1b150a85cb4ca1345c4299ad12a0f",
"x86_64-unknown-linux-gnu-0.15.4":
"6e24501f753416bc84456383ccf62239889ab9fec8318549db9bee791612bd85",
"x86_64-unknown-linux-musl-0.15.4":
"e39111195ef761569773562209bbb7f943c834c961f2c1ed28e2126a15c4cd35",
"aarch64-apple-darwin-0.15.3":
"9135019481619b3d2d797784ed422cd8622d91ea14b0dfe5c7ebe177a98fabb6",
"aarch64-pc-windows-msvc-0.15.3":
"48716216a212f5555b8cd74f7b6c17e10baf4f4859309d978c347cebd285ea6f",
"aarch64-unknown-linux-gnu-0.15.3":
"14a4eedcf55d998c317b02670293f7a9fa6d55f7263951358f49e6a0be0121a3",
"aarch64-unknown-linux-musl-0.15.3":
"dea1c50f06820f27b34a56b3f358522df72b197e3d6d935c0d591562b7d8ceef",
"arm-unknown-linux-musleabihf-0.15.3":
"9b095c3207272b2fc8ec3426b7efee310e2c205dd98dcf9170ccdfb43c4b7ff7",
"armv7-unknown-linux-gnueabihf-0.15.3":
"28f0afb652685f810b9ec016b9f215cb0248614f2215c6d4cc7dae408332bfd3",
"armv7-unknown-linux-musleabihf-0.15.3":
"a5d0c2d46bcfa1636cd74f707220e2f9bc64b2ef2148107e9e99e31a451aa22f",
"i686-pc-windows-msvc-0.15.3":
"4c2ff5518c9b36493e6f7e3e6b804222f35bce9b8d22a9d8efb4c3a2e9d96678",
"i686-unknown-linux-gnu-0.15.3":
"9b7a248eb5e9e975cedba6d5abfed63cdb0325b83a7d93a0a4ee24092175e2b2",
"i686-unknown-linux-musl-0.15.3":
"c47c97dd8f1fdc8a2614db2bb677a72e4f2257f36edf069f97cf082239d036b2",
"powerpc64le-unknown-linux-gnu-0.15.3":
"4e11a1da6cad901dbd02878808d7f03de857e8e27589b35b3e823775382c7983",
"riscv64gc-unknown-linux-gnu-0.15.3":
"db8cffb6bb7ed3d299d0d4fe38bc4b53dc8ee04e5c70ddba4b41a130209ee72f",
"s390x-unknown-linux-gnu-0.15.3":
"16dd01475bdfc2ce4f254570415a04c8803401181d165be8b8be3aea6e6c8cd9",
"x86_64-apple-darwin-0.15.3":
"890958c88c244902171878209bc91d237d87f55518fb5e3f40ab76a5bb36e4bb",
"x86_64-pc-windows-msvc-0.15.3":
"dcef126c1d8aa26545149b2e3b43999100b927e4cdd498263669c506982c796d",
"x86_64-unknown-linux-gnu-0.15.3":
"49eda8819c92b862879b43fa506f5084a9dd2ce0376feed3ffa42a07648cd58f",
"x86_64-unknown-linux-musl-0.15.3":
"c3bbd085bc0a1438fccc912bf3b25a390fcf9f2bb46dbe67491a9589bff618ee",
"aarch64-apple-darwin-0.15.2":
"59a3a08a077e81d0fd99566604556687b834edb2da34a69522cfb5168a07123b",
"aarch64-pc-windows-msvc-0.15.2":
"8cbc83ae19c8653aa7d2127dc70e20730b0c93cb4cd9f1284c1d3973390c253d",
"aarch64-unknown-linux-gnu-0.15.2":
"4bef1a53089b3ae92cb271d3fd170cfb4834eff7c738d4faae379bb872b9a8e6",
"aarch64-unknown-linux-musl-0.15.2":
"b1417ad2977d38c93a40cc77b467b4c68d6b5578031852c38033f9b53b26a543",
"arm-unknown-linux-musleabihf-0.15.2":
"0762e6914f18be4717036b35926763bd36536cd4dc1a22a4e34bbb7fabdb5bce",
"armv7-unknown-linux-gnueabihf-0.15.2":
"16e5bc1787cad23396df645317267f55495c1d08c2878dc7e3c079042901f95c",
"armv7-unknown-linux-musleabihf-0.15.2":
"a31f545501d9693162c7d60afcb8362c72a307effb5fcbfbc418e304775e4538",
"i686-pc-windows-msvc-0.15.2":
"52110333dbf636948facf3a6fd2889acbc658258ead37f3185d0a5fbee925ba9",
"i686-unknown-linux-gnu-0.15.2":
"b20d9632e412ffb24828e79ad5aed435a067e56cb382c2af529eee51c3b2791b",
"i686-unknown-linux-musl-0.15.2":
"bfba760dc9f1806bcdacd2c42f5b0c559cd3fa2771429214d9797d1dd9f22d61",
"powerpc64le-unknown-linux-gnu-0.15.2":
"cf83cabdab0361b3f2707f548a23cc13d018d2e12893cf65b4e438d0882d691f",
"riscv64gc-unknown-linux-gnu-0.15.2":
"3e151c16226fe40db56d838fc90aee3bf7bf9a437fb1d8fdc1efb285899a37a8",
"s390x-unknown-linux-gnu-0.15.2":
"9d913855d0c4924df440f28961e49f8e8f460d6a88b7e4ee307f4cb80e0f3c8a",
"x86_64-apple-darwin-0.15.2":
"78702adcccc2309696f19442f18b5fbe6a4bf4211efa157576c2b5f498a4cc0f",
"x86_64-pc-windows-msvc-0.15.2":
"82a797f212d2e0c16e6ded37e3768c82af3a8b25d49887fafa56f6417f7fb5a9",
"x86_64-unknown-linux-gnu-0.15.2":
"278b307eccb4eef6a153d811466dd8170d4fd74970cc4a44c793b40bd897e403",
"x86_64-unknown-linux-musl-0.15.2":
"2b11788c9457ba8350f9b55bc302adf7f440d2f92a1d9660cbc3b20b6abf5e1c",
"aarch64-apple-darwin-0.15.1":
"196f6d4bd380f4a03f9d2d3bcfe17b991145a110f5fc9f5999521cd5e5335e1c",
"aarch64-pc-windows-msvc-0.15.1":
"f29b448a5a3648f4aae87ed1c778f0d9b2cccd40ec5892b2be06c1b568410829",
"aarch64-unknown-linux-gnu-0.15.1":
"003848ac89a6d2ca1a2ce4e663daec4f90212fc9fd6f338d7cd692c7c8ae9480",
"aarch64-unknown-linux-musl-0.15.1":
"05a41b8b7c068633b27e8f9149c70154ac2090e61d48e5ea9983de769593d29c",
"arm-unknown-linux-musleabihf-0.15.1":
"7101665b77808d7756fca4c4a989d23f48f42b06acfb843d98b83644ad9fe61e",
"armv7-unknown-linux-gnueabihf-0.15.1":
"8985e23e151deafb61015e03e564315805545bb59d2aa0032c28906a94dbaae4",
"armv7-unknown-linux-musleabihf-0.15.1":
"225fe8ab820c362bcea3c84cb7d60376b85c790bafa938a4ef2f2be0b4963213",
"i686-pc-windows-msvc-0.15.1":
"d9cfcea09126f510a675374f308b9b8abd7537e87ed1db3dbebf7211ed6d8fec",
"i686-unknown-linux-gnu-0.15.1":
"7a500cbb644e78d9fef68eaceeb01d224db1003d482a54e3412104bc616afc69",
"i686-unknown-linux-musl-0.15.1":
"dc55081b5eadd32cce183077862f29a693d38ff0b93992ed1144d8167fa39d94",
"powerpc64le-unknown-linux-gnu-0.15.1":
"6ccdf65f4258385b1d30e542c7eca97770f29a534619d66aa2eacce44ce6f5ce",
"riscv64gc-unknown-linux-gnu-0.15.1":
"1372c269badc8c6b8116b8871d41e2af38bb8ad11cbdcfc6e4b4a2865afc20af",
"s390x-unknown-linux-gnu-0.15.1":
"04fd2f54ac619916ada72d1566c9edbaaee4e241e01897918e0efacafa251248",
"x86_64-apple-darwin-0.15.1":
"55fd4437b4b6b0c75793525c980bb1d49d9723044edbdd7dcc962cb595d26d51",
"x86_64-pc-windows-msvc-0.15.1":
"4bd4d5bb5b3c3fa44a4c3d4748dc03f9fbd53808ff2d3adda75e50b1ec1374e2",
"x86_64-unknown-linux-gnu-0.15.1":
"f63d467b81ef1a7a8552fef001784215bb2fc4d7a7e32216c48aaa3bad066779",
"x86_64-unknown-linux-musl-0.15.1":
"c8d04b4f6ce053809d98d1ef99f897c52e6204708bb157551ede7fbab505fea2",
"aarch64-apple-darwin-0.15.0":
"093d355ac33c6b8e91e80b8497d5581c61b028c0405e265cf38fd88f9a291c5c",
"aarch64-pc-windows-msvc-0.15.0":
"14ff501eb6e436f7a8062c6def7c41888926e8890fcb44db169ae033c88751b3",
"aarch64-unknown-linux-gnu-0.15.0":
"abfde61faa5381537feb0493065120b1c363bbac1d608ec92fd6cd84daa8d6b1",
"aarch64-unknown-linux-musl-0.15.0":
"b9860c394ea814a9e9560e80bc798c53b8a3a5120ce961a2555fb563cee9f73b",
"arm-unknown-linux-musleabihf-0.15.0":
"fbafa4c0e3c125f80b32faf575a7fc21a7d6e4d6889fcfc2425d03ac52a23fa8",
"armv7-unknown-linux-gnueabihf-0.15.0":
"f622dfb00984e2850b5da02db6c6469214115634a4a3e7875f75d5b097302354",
"armv7-unknown-linux-musleabihf-0.15.0":
"093fb4d9e3e8cfda389ccf6b1a1030d83cadb429de889eca045fa2daf848a725",
"i686-pc-windows-msvc-0.15.0":
"09f178ebce6bd6b48422ba0fbff00d76a838aa24fc074c081929a6faa4bdbd31",
"i686-unknown-linux-gnu-0.15.0":
"8e1f5f5c62d82a5e8aaa316c0236380282f89b5e4027eaa631c159c7f8daf7d2",
"i686-unknown-linux-musl-0.15.0":
"cf125895296f7f22714f01298d79885aaec9c25cdd7b648cfbf92a583dfd7ad8",
"powerpc64le-unknown-linux-gnu-0.15.0":
"d62f860244824c34bc66d2941cf95842bb5988cce13cf2a9bde5d361557e7fdc",
"riscv64gc-unknown-linux-gnu-0.15.0":
"e76a0be3e0ad5733aed2f69e242d3d4af92ed2522007a0ebe846b4f37383d2ab",
"s390x-unknown-linux-gnu-0.15.0":
"3e414eabe22173a5b246a2b5b432ef9232b605fb1b3323f9b2968ea99e1d05fe",
"x86_64-apple-darwin-0.15.0":
"09fa6fe0d4172e1bb84cc6d937a0e1f42ff84c90b61163fff4f31c51a9c14879",
"x86_64-pc-windows-msvc-0.15.0":
"d1613f4231095d160ab4d3487bf56de23f29945888bcbe4521fcb0cb5e695d92",
"x86_64-unknown-linux-gnu-0.15.0":
"b38e69fb31501ebee3aba8e1778547bb2557adaf6b8f9dfae6f05980ee68b8ed",
"x86_64-unknown-linux-musl-0.15.0":
"f2bd69f091517ebc49405319a6e7818b1037e250b8d539336aa0b91c44ffa4aa",
"aarch64-apple-darwin-0.14.14":
"76a9b0ebe57d0eee56940dbe0b62462578d1369cca8314ed0d2a6f2102292d4f",
"aarch64-pc-windows-msvc-0.14.14":
+146 -146
View File
@@ -2,16 +2,16 @@ import { promises as fs } from "node:fs";
import * as path from "node:path";
import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import { Octokit } from "@octokit/core";
import { paginateRest } from "@octokit/plugin-paginate-rest";
import { restEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
import * as pep440 from "@renovatebot/pep440";
import * as semver from "semver";
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
import {
ASTRAL_MIRROR_PREFIX,
GITHUB_RELEASES_PREFIX,
TOOL_CACHE_NAME,
VERSIONS_MANIFEST_URL,
} from "../utils/constants";
import type { Architecture, Platform } from "../utils/platforms";
import { validateChecksum } from "./checksum/checksum";
const PaginatingOctokit = Octokit.plugin(paginateRest, restEndpointMethods);
import { getArtifact } from "./manifest";
export function tryGetFromToolCache(
arch: Architecture,
@@ -32,31 +32,48 @@ export async function downloadVersion(
platform: Platform,
arch: Architecture,
version: string,
checkSum: string | undefined,
checksum: string | undefined,
githubToken: string,
manifestUrl?: string,
downloadFromAstralMirror = true,
): Promise<{ version: string; cachedToolDir: string }> {
const artifact = `ruff-${arch}-${platform}`;
let extension = ".tar.gz";
if (platform === "pc-windows-msvc") {
extension = ".zip";
}
const downloadUrl = constructDownloadUrl(version, platform, arch);
core.debug(`Downloading ruff from "${downloadUrl}" ...`);
const artifact = await getArtifact(version, arch, platform, manifestUrl);
const downloadPath = await tc.downloadTool(
downloadUrl,
undefined,
githubToken,
if (!artifact) {
throw new Error(
getMissingArtifactMessage(version, arch, platform, manifestUrl),
);
}
// For the default astral-sh/versions source, checksum validation relies on
// user input or the built-in KNOWN_CHECKSUMS table, not manifest sha256 values.
const resolvedChecksum =
manifestUrl === undefined
? checksum
: resolveChecksum(checksum, artifact.checksum);
const downloadPath = await downloadArtifact(
artifact.downloadUrl,
platform,
arch,
version,
getDownloadToken(artifact.downloadUrl, githubToken),
downloadFromAstralMirror,
);
await validateChecksum(
resolvedChecksum,
downloadPath,
arch,
platform,
version,
);
core.debug(`Downloaded ruff to "${downloadPath}"`);
await validateChecksum(checkSum, downloadPath, arch, platform, version);
const extractedDir = await extractDownloadedArtifact(
version,
downloadPath,
extension,
getExtension(platform),
platform,
artifact,
`ruff-${arch}-${platform}`,
);
const cachedToolDir = await tc.cacheDir(
@@ -65,25 +82,67 @@ export async function downloadVersion(
version,
arch,
);
return { cachedToolDir, version: version };
return { cachedToolDir, version };
}
function constructDownloadUrl(
version: string,
/**
* Rewrite a GitHub Releases URL to the Astral mirror.
* Returns `undefined` if the URL does not match the expected GitHub prefix.
*/
export function rewriteToMirror(url: string): string | undefined {
if (!url.startsWith(GITHUB_RELEASES_PREFIX)) {
return undefined;
}
return ASTRAL_MIRROR_PREFIX + url.slice(GITHUB_RELEASES_PREFIX.length);
}
async function downloadArtifact(
downloadUrl: string,
platform: Platform,
arch: Architecture,
): string {
const artifactVersionSuffix =
semver.lte(version, "v0.4.10") && semver.gte(version, "v0.1.8")
? `-${version}`
: "";
const artifact = `ruff${artifactVersionSuffix}-${arch}-${platform}`;
let extension = ".tar.gz";
if (platform === "pc-windows-msvc") {
extension = ".zip";
version: string,
githubToken: string | undefined,
downloadFromAstralMirror: boolean,
): Promise<string> {
const mirrorUrl = downloadFromAstralMirror
? rewriteToMirror(downloadUrl)
: undefined;
const resolvedDownloadUrl = mirrorUrl ?? downloadUrl;
try {
return await downloadFile(
resolvedDownloadUrl,
mirrorUrl !== undefined ? undefined : githubToken,
);
} catch (err) {
if (mirrorUrl === undefined) {
throw err;
}
const versionPrefix = semver.lte(version, "v0.4.10") ? "v" : "";
return `https://github.com/${OWNER}/${REPO}/releases/download/${versionPrefix}${version}/${artifact}${extension}`;
core.warning(
`Failed to download from mirror, falling back to GitHub Releases: ${(err as Error).message}`,
);
return await downloadFile(
constructDownloadUrl(version, platform, arch),
githubToken,
);
}
}
async function downloadFile(
downloadUrl: string,
githubToken: string | undefined,
): Promise<string> {
core.info(`Downloading ruff from "${downloadUrl}" ...`);
const downloadPath = await tc.downloadTool(
downloadUrl,
undefined,
githubToken,
);
core.debug(`Downloaded ruff to "${downloadPath}"`);
return downloadPath;
}
async function extractDownloadedArtifact(
@@ -98,7 +157,7 @@ async function extractDownloadedArtifact(
const fullPathWithExtension = `${downloadPath}${extension}`;
await fs.copyFile(downloadPath, fullPathWithExtension);
ruffDir = await tc.extractZip(fullPathWithExtension);
// On windows extracting the zip does not create an intermediate directory
// On windows extracting the zip does not create an intermediate directory.
} else {
ruffDir = await tc.extractTar(downloadPath);
if (semver.gte(version, "v0.5.0")) {
@@ -111,116 +170,57 @@ async function extractDownloadedArtifact(
return ruffDir;
}
export async function resolveVersion(
versionInput: string,
githubToken: string,
): Promise<string> {
core.debug(`Resolving ${versionInput}...`);
const version =
versionInput === "latest"
? await getLatestVersion(githubToken)
: versionInput;
if (tc.isExplicitVersion(version)) {
core.debug(`Version ${version} is an explicit version.`);
return version;
}
const availableVersions = await getAvailableVersions(githubToken);
const resolvedVersion = maxSatisfying(availableVersions, version);
if (resolvedVersion === undefined) {
throw new Error(`No version found for ${version}`);
}
core.debug(`Resolved version: ${resolvedVersion}`);
return resolvedVersion;
}
async function getAvailableVersions(githubToken: string): Promise<string[]> {
try {
const octokit = new PaginatingOctokit({
auth: githubToken,
});
return await getReleaseTagNames(octokit);
} catch (err) {
if ((err as Error).message.includes("Bad credentials")) {
core.info(
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
);
const octokit = new PaginatingOctokit();
return await getReleaseTagNames(octokit);
}
throw err;
}
}
async function getReleaseTagNames(
octokit: InstanceType<typeof PaginatingOctokit>,
): Promise<string[]> {
const response = await octokit.paginate(octokit.rest.repos.listReleases, {
owner: OWNER,
repo: REPO,
});
const releaseTagNames = response.map((release) => release.tag_name);
if (releaseTagNames.length === 0) {
throw Error(
"Github API request failed while getting releases. Check the GitHub status page for outages. Try again later.",
);
}
return response.map((release) => release.tag_name);
}
async function getLatestVersion(githubToken: string) {
const octokit = new PaginatingOctokit({
auth: githubToken,
});
let latestRelease: { tag_name: string } | undefined;
try {
latestRelease = await getLatestRelease(octokit);
} catch (err) {
if ((err as Error).message.includes("Bad credentials")) {
core.info(
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
);
const octokit = new PaginatingOctokit();
latestRelease = await getLatestRelease(octokit);
} else {
core.error(
"Github API request failed while getting latest release. Check the GitHub status page for outages. Try again later.",
);
throw err;
}
}
if (!latestRelease) {
throw new Error("Could not determine latest release.");
}
return latestRelease.tag_name;
}
async function getLatestRelease(
octokit: InstanceType<typeof PaginatingOctokit>,
) {
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
owner: OWNER,
repo: REPO,
});
return latestRelease;
}
function maxSatisfying(
versions: string[],
function getMissingArtifactMessage(
version: string,
arch: Architecture,
platform: Platform,
manifestUrl?: string,
): string {
if (manifestUrl === undefined) {
return `Could not find artifact for version ${version}, arch ${arch}, platform ${platform} in ${VERSIONS_MANIFEST_URL} .`;
}
return `manifest-file does not contain version ${version}, arch ${arch}, platform ${platform}.`;
}
function resolveChecksum(
checksum: string | undefined,
manifestChecksum: string,
): string {
return checksum !== undefined && checksum !== ""
? checksum
: manifestChecksum;
}
function getDownloadToken(
downloadUrl: string,
githubToken: string,
): string | undefined {
const maxSemver = tc.evaluateVersions(versions, version);
if (maxSemver !== "") {
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
return maxSemver;
return downloadUrl.startsWith(GITHUB_RELEASES_PREFIX)
? githubToken
: undefined;
}
const maxPep440 = pep440.maxSatisfying(versions, version);
if (maxPep440 !== null) {
core.debug(
`Found a version that satisfies the pep440 specifier: ${maxPep440}`,
);
return maxPep440;
function constructDownloadUrl(
version: string,
platform: Platform,
arch: Architecture,
): string {
const normalizedVersion = stripVersionPrefix(version);
const artifactVersionSuffix =
semver.lte(version, "v0.4.10") && semver.gte(version, "v0.1.8")
? `-${normalizedVersion}`
: "";
const artifact = `ruff${artifactVersionSuffix}-${arch}-${platform}`;
const versionPrefix = semver.lte(version, "v0.4.10") ? "v" : "";
return `${GITHUB_RELEASES_PREFIX}${versionPrefix}${normalizedVersion}/${artifact}${getExtension(platform)}`;
}
return undefined;
function stripVersionPrefix(version: string): string {
return version.startsWith("v") ? version.slice(1) : version;
}
function getExtension(platform: Platform): string {
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
}
+243
View File
@@ -0,0 +1,243 @@
import * as core from "@actions/core";
import { VERSIONS_MANIFEST_URL } from "../utils/constants";
import { fetch } from "../utils/fetch";
import { selectDefaultVariant } from "./variant-selection";
export interface ManifestArtifact {
platform: string;
variant?: string;
url: string;
archive_format: string;
sha256: string;
}
export interface ManifestVersion {
version: string;
artifacts: ManifestArtifact[];
}
export interface ArtifactResult {
archiveFormat: string;
checksum: string;
downloadUrl: string;
}
const cachedManifestData = new Map<string, ManifestVersion[]>();
export async function fetchManifest(
manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<ManifestVersion[]> {
const cachedVersions = cachedManifestData.get(manifestUrl);
if (cachedVersions !== undefined) {
core.debug(`Using cached manifest data from ${manifestUrl}`);
return cachedVersions;
}
core.info(`Fetching manifest data from ${manifestUrl} ...`);
const response = await fetch(manifestUrl, {});
if (!response.ok) {
throw new Error(
`Failed to fetch manifest data: ${response.status} ${response.statusText}`,
);
}
const body = await response.text();
const versions = parseManifest(body, manifestUrl);
cachedManifestData.set(manifestUrl, versions);
return versions;
}
export function parseManifest(
data: string,
sourceDescription: string,
): ManifestVersion[] {
const trimmed = data.trim();
if (trimmed === "") {
throw new Error(`Manifest at ${sourceDescription} is empty.`);
}
if (trimmed.startsWith("[")) {
throw new Error(
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`,
);
}
const versions: ManifestVersion[] = [];
for (const [index, line] of data.split("\n").entries()) {
const record = line.trim();
if (record === "") {
continue;
}
let parsed: unknown;
try {
parsed = JSON.parse(record);
} catch (error) {
throw new Error(
`Failed to parse manifest data from ${sourceDescription} at line ${index + 1}: ${(error as Error).message}`,
);
}
if (!isManifestVersion(parsed)) {
throw new Error(
`Invalid manifest record in ${sourceDescription} at line ${index + 1}.`,
);
}
versions.push(parsed);
}
if (versions.length === 0) {
throw new Error(`No manifest data found in ${sourceDescription}.`);
}
return versions;
}
export async function getLatestVersion(
manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<string> {
const latestVersion = (await fetchManifest(manifestUrl))[0]?.version;
if (latestVersion === undefined) {
throw new Error("No versions found in manifest data");
}
core.debug(`Latest version from manifest: ${latestVersion}`);
return latestVersion;
}
export async function getAllVersions(
manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<string[]> {
core.info(
`Getting available versions from ${manifestSource(manifestUrl)} ...`,
);
const versions = await fetchManifest(manifestUrl);
return versions.map((versionData) => versionData.version);
}
export async function getArtifact(
version: string,
arch: string,
platform: string,
manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<ArtifactResult | undefined> {
const versions = await fetchManifest(manifestUrl);
const versionData = versions.find((candidate) =>
matchesManifestVersion(candidate.version, version),
);
if (!versionData) {
core.debug(`Version ${version} not found in manifest ${manifestUrl}`);
return undefined;
}
const targetPlatforms = getTargetPlatforms(
versionData.version,
arch,
platform,
);
const matchingArtifacts = versionData.artifacts.filter((candidate) =>
targetPlatforms.includes(candidate.platform),
);
if (matchingArtifacts.length === 0) {
core.debug(
`Artifact for ${targetPlatforms.join(" or ")} not found in version ${version}. Available platforms: ${versionData.artifacts
.map((candidate) => candidate.platform)
.join(", ")}`,
);
return undefined;
}
const artifact = selectDefaultVariant(
matchingArtifacts,
`Multiple artifacts found for ${targetPlatforms.join(" or ")} in version ${version}`,
);
return {
archiveFormat: artifact.archive_format,
checksum: artifact.sha256,
downloadUrl: artifact.url,
};
}
export function clearManifestCache(manifestUrl?: string): void {
if (manifestUrl === undefined) {
cachedManifestData.clear();
return;
}
cachedManifestData.delete(manifestUrl);
}
function manifestSource(manifestUrl: string): string {
if (manifestUrl === VERSIONS_MANIFEST_URL) {
return VERSIONS_MANIFEST_URL;
}
return `manifest-file ${manifestUrl}`;
}
function matchesManifestVersion(
manifestVersion: string,
requestedVersion: string,
): boolean {
return (
manifestVersion === requestedVersion ||
manifestVersion === withVersionPrefix(requestedVersion)
);
}
function getTargetPlatforms(
manifestVersion: string,
arch: string,
platform: string,
): string[] {
const targetPlatform = `${arch}-${platform}`;
const versionPrefixedTargetPlatform = `${stripVersionPrefix(manifestVersion)}-${targetPlatform}`;
return [targetPlatform, versionPrefixedTargetPlatform];
}
function withVersionPrefix(version: string): string {
return version.startsWith("v") ? version : `v${version}`;
}
function stripVersionPrefix(version: string): string {
return version.startsWith("v") ? version.slice(1) : version;
}
function isManifestVersion(value: unknown): value is ManifestVersion {
if (!isRecord(value)) {
return false;
}
if (typeof value.version !== "string" || !Array.isArray(value.artifacts)) {
return false;
}
return value.artifacts.every(isManifestArtifact);
}
function isManifestArtifact(value: unknown): value is ManifestArtifact {
if (!isRecord(value)) {
return false;
}
const variantIsValid =
typeof value.variant === "string" || value.variant === undefined;
return (
typeof value.archive_format === "string" &&
typeof value.platform === "string" &&
typeof value.sha256 === "string" &&
typeof value.url === "string" &&
variantIsValid
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
+39
View File
@@ -0,0 +1,39 @@
interface VariantAwareEntry {
variant?: string;
}
export function selectDefaultVariant<T extends VariantAwareEntry>(
entries: T[],
duplicateEntryDescription: string,
): T {
const firstEntry = entries[0];
if (firstEntry === undefined) {
throw new Error("selectDefaultVariant requires at least one candidate.");
}
if (entries.length === 1) {
return firstEntry;
}
const defaultEntries = entries.filter((entry) =>
isDefaultVariant(entry.variant),
);
if (defaultEntries.length === 1) {
return defaultEntries[0];
}
throw new Error(
`${duplicateEntryDescription} with variants ${formatVariants(entries)}. ruff-action currently requires a single default variant for duplicate platform entries.`,
);
}
function isDefaultVariant(variant: string | undefined): boolean {
return variant === undefined || variant === "default";
}
function formatVariants<T extends VariantAwareEntry>(entries: T[]): string {
return entries
.map((entry) => entry.variant ?? "default")
.sort((left, right) => left.localeCompare(right))
.join(", ");
}
+40 -49
View File
@@ -4,13 +4,14 @@ import * as exec from "@actions/exec";
import * as semver from "semver";
import {
downloadVersion,
resolveVersion,
tryGetFromToolCache,
} from "./download/download-version";
import {
args,
checkSum,
downloadFromAstralMirror,
githubToken,
manifestFile,
src,
version,
versionFile as versionFileInput,
@@ -21,8 +22,12 @@ import {
getPlatform,
type Platform,
} from "./utils/platforms";
import { getRuffVersionFromRequirementsFile } from "./utils/pyproject";
import { findPyprojectToml } from "./utils/pyproject-finder";
import {
expandSourceInput,
getVersionSourceDirectory,
splitInput,
} from "./utils/source-input";
import { resolveRuffVersion } from "./version/resolve";
async function run(): Promise<void> {
const platform = getPlatform();
@@ -35,7 +40,13 @@ async function run(): Promise<void> {
if (arch === undefined) {
throw new Error(`Unsupported architecture: ${process.arch}`);
}
const setupResult = await setupRuff(platform, arch, checkSum, githubToken);
const setupResult = await setupRuff(
platform,
arch,
checkSum,
githubToken,
downloadFromAstralMirror,
);
addRuffToPath(setupResult.ruffDir);
setOutputFormat();
@@ -43,11 +54,7 @@ async function run(): Promise<void> {
core.setOutput("ruff-version", setupResult.version);
core.info(`Successfully installed ruff version ${setupResult.version}`);
await runRuff(
path.join(setupResult.ruffDir, "ruff"),
args.split(" "),
src.split(" "),
);
await runRuff(path.join(setupResult.ruffDir, "ruff"), args, src);
process.exit(0);
} catch (err) {
@@ -60,8 +67,10 @@ async function setupRuff(
arch: Architecture,
checkSum: string | undefined,
githubToken: string,
downloadFromAstralMirror: boolean,
): Promise<{ ruffDir: string; version: string }> {
const resolvedVersion = await determineVersion();
const manifestUrl = manifestFile || undefined;
if (semver.lt(resolvedVersion, "v0.0.247")) {
throw Error(
"This action does not support ruff versions older than 0.0.247",
@@ -82,6 +91,8 @@ async function setupRuff(
resolvedVersion,
checkSum,
githubToken,
manifestUrl,
downloadFromAstralMirror,
);
return {
@@ -91,38 +102,17 @@ async function setupRuff(
}
async function determineVersion(): Promise<string> {
if (versionFileInput !== "" && version !== "") {
throw Error("It is not allowed to specify both version and version-file");
}
if (version !== "") {
return await resolveVersion(version, githubToken);
}
if (versionFileInput !== "") {
const versionFromPyproject =
getRuffVersionFromRequirementsFile(versionFileInput);
if (versionFromPyproject === undefined) {
core.warning(
`Could not parse version from ${versionFileInput}. Using latest version.`,
);
}
return await resolveVersion(versionFromPyproject || "latest", githubToken);
}
const pyProjectPath = findPyprojectToml(
return await resolveRuffVersion({
manifestFile: manifestFile || undefined,
sourceDirectory: await getVersionSourceDirectory(
src,
process.env.GITHUB_WORKSPACE || ".",
);
if (!pyProjectPath) {
core.info(`Could not find pyproject.toml. Using latest version.`);
return await resolveVersion("latest", githubToken);
}
const versionFromPyproject =
getRuffVersionFromRequirementsFile(pyProjectPath);
if (versionFromPyproject === undefined) {
core.info(
`Could not parse version from ${pyProjectPath}. Using latest version.`,
);
}
return await resolveVersion(versionFromPyproject || "latest", githubToken);
version,
versionFileInput,
),
version,
versionFile: versionFileInput,
workspaceRoot: process.env.GITHUB_WORKSPACE || ".",
});
}
function addRuffToPath(cachedPath: string): void {
@@ -136,22 +126,23 @@ function setOutputFormat() {
}
function addMatchers(): void {
const matchersPath = path.join(
__dirname,
`..${path.sep}..`,
".github",
"matchers",
);
const actionRoot = getActionRoot();
const matchersPath = path.join(actionRoot, ".github", "matchers");
core.info(`##[add-matcher]${path.join(matchersPath, "check.json")}`);
core.info(`##[add-matcher]${path.join(matchersPath, "format.json")}`);
}
function getActionRoot(): string {
const entrypoint = process.argv[1] ?? process.cwd();
return path.resolve(path.dirname(entrypoint), "..", "..");
}
async function runRuff(
ruffExecutablePath: string,
args: string[],
src: string[],
args: string,
src: string,
): Promise<void> {
const execArgs = [...args, ...src];
const execArgs = [...splitInput(args), ...(await expandSourceInput(src))];
await exec.exec(ruffExecutablePath, execArgs);
}
+2 -2
View File
@@ -10,9 +10,9 @@ const PaginatingOctokit = Octokit.plugin(paginateRest, restEndpointMethods);
async function run(): Promise<void> {
const checksumFilePath = process.argv.slice(2)[0];
const github_token = process.argv.slice(2)[1];
const githubToken = process.argv.slice(2)[1];
const octokit = new PaginatingOctokit({ auth: github_token });
const octokit = new PaginatingOctokit({ auth: githubToken });
const response = await octokit.paginate(octokit.rest.repos.listReleases, {
owner: OWNER,
+6
View File
@@ -1,3 +1,9 @@
export const REPO = "ruff";
export const OWNER = "astral-sh";
export const TOOL_CACHE_NAME = "ruff";
export const VERSIONS_MANIFEST_URL =
"https://raw.githubusercontent.com/astral-sh/versions/main/v1/ruff.ndjson";
export const GITHUB_RELEASES_PREFIX =
"https://github.com/astral-sh/ruff/releases/download/";
export const ASTRAL_MIRROR_PREFIX =
"https://releases.astral.sh/github/ruff/releases/download/";
+21
View File
@@ -0,0 +1,21 @@
import { ProxyAgent, type RequestInit, fetch as undiciFetch } from "undici";
export function getProxyAgent() {
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
if (httpProxy) {
return new ProxyAgent(httpProxy);
}
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
if (httpsProxy) {
return new ProxyAgent(httpsProxy);
}
return undefined;
}
export const fetch = async (url: string, opts: RequestInit) =>
await undiciFetch(url, {
dispatcher: getProxyAgent(),
...opts,
});
+12
View File
@@ -6,3 +6,15 @@ export const githubToken = core.getInput("github-token");
export const args = core.getInput("args");
export const src = core.getInput("src");
export const versionFile = core.getInput("version-file");
export const manifestFile = core.getInput("manifest-file");
export const downloadFromAstralMirror = getBooleanInput(
"download-from-astral-mirror",
true,
);
function getBooleanInput(name: string, defaultValue: boolean): boolean {
if (core.getInput(name) === "") {
return defaultValue;
}
return core.getBooleanInput(name);
}
-183
View File
@@ -1,183 +0,0 @@
import * as path from "node:path";
import * as core from "@actions/core";
import { findPyprojectToml } from "./pyproject-finder";
jest.mock("@actions/core", () => ({
debug: jest.fn(),
info: jest.fn(),
}));
describe("findPyprojectToml", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("when pyproject.toml exists in src directory", () => {
it("should return the exact path", () => {
const fixturesDir = path.join(
__dirname,
"..",
"..",
"__tests__",
"fixtures",
);
const workspaceRoot = path.join(__dirname, "..", "..");
const result = findPyprojectToml(fixturesDir, workspaceRoot);
expect(result).toContain("pyproject.toml");
expect(result).toContain("fixtures");
expect(core.info).toHaveBeenCalled();
});
});
describe("when pyproject.toml exists only in parent directory", () => {
it("should search upwards and find the parent's pyproject.toml", () => {
// subproject doesn't have a pyproject.toml, but its parent (parent-config-project) does
const subprojectDir = path.join(
__dirname,
"..",
"..",
"__tests__",
"fixtures",
"parent-config-project",
"subproject",
);
const workspaceRoot = path.join(__dirname, "..", "..");
const result = findPyprojectToml(subprojectDir, workspaceRoot);
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
expect(result).toContain("parent-config-project");
expect(core.info).toHaveBeenCalled();
});
});
describe("boundary conditions", () => {
it("should stop searching at workspace root and return undefined when not found", () => {
// Create a path that won't have pyproject.toml above it
const nodeModulesDir = path.join(
__dirname,
"..",
"..",
"node_modules",
"@actions",
);
const workspaceRoot = path.join(__dirname, "..", "..");
const result = findPyprojectToml(nodeModulesDir, workspaceRoot);
// Should return undefined since there's no pyproject.toml in the search path
expect(result).toBeUndefined();
expect(core.info).not.toHaveBeenCalledWith(
expect.stringContaining("Found pyproject.toml"),
);
});
it("should find pyproject.toml when it exists at workspace root", () => {
// Use parent-config-project as the "workspace root" for this test
// Start from subproject (which has no pyproject.toml) to search up to workspace root
const subprojectDir = path.join(
__dirname,
"..",
"..",
"__tests__",
"fixtures",
"parent-config-project",
"subproject",
);
const workspaceRoot = path.join(
__dirname,
"..",
"..",
"__tests__",
"fixtures",
"parent-config-project",
);
const result = findPyprojectToml(subprojectDir, workspaceRoot);
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
expect(result).toContain("parent-config-project");
});
it("should stop at workspace root even if searching from it", () => {
const workspaceRoot = path.join(
__dirname,
"..",
"..",
"__tests__",
"fixtures",
);
const result = findPyprojectToml(workspaceRoot, workspaceRoot);
// Should find pyproject.toml at workspace root
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
expect(result).toContain("fixtures");
});
});
describe("edge cases", () => {
it("should handle relative paths", () => {
const srcDir = "./__tests__/fixtures";
const workspaceRoot = ".";
const result = findPyprojectToml(srcDir, workspaceRoot);
// Should work with relative paths
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
});
it("should handle when src equals workspace root", () => {
const workspaceRoot = path.join(
__dirname,
"..",
"..",
"__tests__",
"fixtures",
);
const result = findPyprojectToml(workspaceRoot, workspaceRoot);
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
expect(result).toContain("fixtures");
});
it("should log debug messages for each checked path", () => {
const pythonProjectDir = path.join(
__dirname,
"..",
"..",
"__tests__",
"fixtures",
"python-project",
);
const workspaceRoot = path.join(__dirname, "..", "..");
findPyprojectToml(pythonProjectDir, workspaceRoot);
expect(core.debug).toHaveBeenCalled();
const debugCalls = (core.debug as jest.Mock).mock.calls;
expect(debugCalls.length).toBeGreaterThan(0);
// First debug call should be for the starting directory
expect(debugCalls[0][0]).toContain("Checking for");
expect(debugCalls[0][0]).toContain("python-project");
});
it("should handle paths with trailing slashes", () => {
const fixturesDir = `${path.join(__dirname, "..", "..", "__tests__", "fixtures")}/`;
const workspaceRoot = path.join(__dirname, "..", "..");
const result = findPyprojectToml(fixturesDir, workspaceRoot);
expect(result).toBeTruthy();
expect(result).toContain("pyproject.toml");
});
});
});
+26 -31
View File
@@ -3,19 +3,22 @@ import * as path from "node:path";
import * as core from "@actions/core";
/**
* Search for a pyproject.toml file starting from the given directory
* Search for a pyproject.toml file starting from the given source path
* and traversing upwards through parent directories until reaching
* the GitHub workspace root.
*
* @param startDir The directory to start the search from (e.g., the src input)
* If the source path points to a file, the search begins in that file's
* parent directory.
*
* @param startPath The source path to start the search from (file or directory)
* @param workspaceRoot The GitHub workspace directory (GITHUB_WORKSPACE)
* @returns The path to the found pyproject.toml, or undefined if not found
*/
export function findPyprojectToml(
startDir: string,
startPath: string,
workspaceRoot: string,
): string | undefined {
let currentDir = path.resolve(startDir);
let currentDir = resolveStartDirectory(startPath);
const resolvedWorkspaceRoot = path.resolve(workspaceRoot);
while (true) {
@@ -26,30 +29,33 @@ export function findPyprojectToml(
core.info(`Found pyproject.toml at ${pyprojectPath}`);
return pyprojectPath;
}
// Check if we've reached the workspace root
if (currentDir === resolvedWorkspaceRoot) {
// If we're at workspace root and didn't find it, stop searching
break;
return undefined;
}
// Move up to parent directory
const parentDir = path.dirname(currentDir);
// If parent is the same as current, we've reached the filesystem root
if (parentDir === currentDir) {
break;
if (
parentDir === currentDir ||
!isPathWithinWorkspace(parentDir, resolvedWorkspaceRoot)
) {
return undefined;
}
currentDir = parentDir;
// If we've gone past the workspace root, stop searching
if (isPathWithinWorkspace(currentDir, resolvedWorkspaceRoot) === false) {
break;
}
}
return undefined;
function resolveStartDirectory(startPath: string): string {
const resolvedStartPath = path.resolve(startPath);
if (!fs.existsSync(resolvedStartPath)) {
return resolvedStartPath;
}
const stats = fs.statSync(resolvedStartPath);
return stats.isDirectory()
? resolvedStartPath
: path.dirname(resolvedStartPath);
}
/**
@@ -62,18 +68,7 @@ export function findPyprojectToml(
function isPathWithinWorkspace(
checkPath: string,
workspaceRoot: string,
): boolean | undefined {
try {
const checkPathResolved = path.resolve(checkPath);
const workspaceRootResolved = path.resolve(workspaceRoot);
// Check if checkPath starts with workspaceRoot (case-insensitive on Windows)
const relativePath = path.relative(
workspaceRootResolved,
checkPathResolved,
);
): boolean {
const relativePath = path.relative(workspaceRoot, checkPath);
return !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
} catch {
return undefined;
}
}
-188
View File
@@ -1,188 +0,0 @@
import * as core from "@actions/core";
import { findRuffVersionInSpec } from "./pyproject";
jest.mock("@actions/core", () => ({
info: jest.fn(),
warning: jest.fn(),
}));
describe("findRuffVersionInSpec", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("ruff dependency strings", () => {
it("should extract version from 'ruff==0.9.3'", () => {
const result = findRuffVersionInSpec("ruff==0.9.3");
expect(result).toBe("0.9.3");
expect(core.info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(core.warning).not.toHaveBeenCalled();
});
it("should extract version from 'ruff>=0.14'", () => {
const result = findRuffVersionInSpec("ruff>=0.14");
expect(result).toBe(">=0.14");
expect(core.warning).not.toHaveBeenCalled();
});
it("should extract version from 'ruff ~=1.0.0'", () => {
const result = findRuffVersionInSpec("ruff ~=1.0.0");
expect(result).toBe("~=1.0.0");
expect(core.warning).not.toHaveBeenCalled();
});
it("should extract version from 'ruff>=0.14,<1.0'", () => {
const result = findRuffVersionInSpec("ruff>=0.14,<1.0");
expect(result).toBe(">=0.14,<1.0");
expect(core.warning).not.toHaveBeenCalled();
});
it("should extract version from 'ruff>=0.14,<2.0,!=1.5.0'", () => {
const result = findRuffVersionInSpec("ruff>=0.14,<2.0,!=1.5.0");
expect(result).toBe(">=0.14,<2.0,!=1.5.0");
expect(core.warning).not.toHaveBeenCalled();
});
it("should return undefined for non-ruff dependency 'another-dep 0.1.6'", () => {
const result = findRuffVersionInSpec("another-dep 0.1.6");
expect(result).toBeUndefined();
expect(core.info).not.toHaveBeenCalled();
expect(core.warning).not.toHaveBeenCalled();
});
it("should return undefined for non-ruff dependency 'another-dep==0.1.6'", () => {
const result = findRuffVersionInSpec("another-dep==0.1.6");
expect(result).toBeUndefined();
expect(core.info).not.toHaveBeenCalled();
expect(core.warning).not.toHaveBeenCalled();
});
it("should strip trailing backslash", () => {
const result = findRuffVersionInSpec("ruff==0.9.3 \\");
expect(result).toBe("0.9.3");
expect(core.info).toHaveBeenCalledWith(
"Found ruff version in requirements file: 0.9.3",
);
expect(core.warning).not.toHaveBeenCalled();
});
it("should strip trailing backslash with whitespace", () => {
const result = findRuffVersionInSpec(" ruff==0.9.3 \\ ");
expect(result).toBe("0.9.3");
expect(core.warning).not.toHaveBeenCalled();
});
});
describe("environment markers", () => {
it("should strip python_version environment marker", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14");
expect(core.info).toHaveBeenCalledWith(
"Found ruff version in requirements file: >=0.14",
);
expect(core.warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("should strip sys_platform environment marker", () => {
const result = findRuffVersionInSpec(
"ruff==0.9.3 ; sys_platform == 'linux'",
);
expect(result).toBe("0.9.3");
expect(core.warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("should strip multiple environment markers", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11" and sys_platform == "linux"',
);
expect(result).toBe(">=0.14");
expect(core.warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("should handle environment markers with multiple constraints", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14,<1.0 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14,<1.0");
expect(core.warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
});
describe("edge cases", () => {
it("should handle whitespace", () => {
const result = findRuffVersionInSpec(" ruff >=0.14 ");
expect(result).toBe(">=0.14");
expect(core.warning).not.toHaveBeenCalled();
});
it("should handle whitespace with environment markers", () => {
const result = findRuffVersionInSpec(
" ruff >=0.14 ; python_version >= '3.11' ",
);
expect(result).toBe(">=0.14");
expect(core.warning).toHaveBeenCalled();
});
it("should return undefined for empty string", () => {
const result = findRuffVersionInSpec("");
expect(result).toBeUndefined();
expect(core.info).not.toHaveBeenCalled();
expect(core.warning).not.toHaveBeenCalled();
});
it("should return undefined for whitespace only", () => {
const result = findRuffVersionInSpec(" ");
expect(result).toBeUndefined();
expect(core.info).not.toHaveBeenCalled();
expect(core.warning).not.toHaveBeenCalled();
});
it("should return undefined for just semicolon", () => {
const result = findRuffVersionInSpec(";");
expect(result).toBeUndefined();
expect(core.info).not.toHaveBeenCalled();
expect(core.warning).not.toHaveBeenCalled();
});
it("should handle exact example from issue #256", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14");
expect(core.info).toHaveBeenCalledWith(
"Found ruff version in requirements file: >=0.14",
);
expect(core.warning).toHaveBeenCalledWith(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
});
it("should handle single-quoted environment markers", () => {
const result = findRuffVersionInSpec(
"ruff>=0.14 ; python_version >= '3.11'",
);
expect(result).toBe(">=0.14");
expect(core.warning).toHaveBeenCalled();
});
it("should handle double-quoted environment markers", () => {
const result = findRuffVersionInSpec(
'ruff>=0.14 ; python_version >= "3.11"',
);
expect(result).toBe(">=0.14");
expect(core.warning).toHaveBeenCalled();
});
});
});
-130
View File
@@ -1,130 +0,0 @@
import * as fs from "node:fs";
import * as core from "@actions/core";
import * as toml from "smol-toml";
/**
* Find ruff version in a dependency specification.
* Only handles strings that start with "ruff" (e.g., "ruff==0.9.3").
* Returns undefined for non-ruff dependencies.
* Strips environment markers (everything after ';').
* Strips leading '==' from exact version specifiers (PEP 440) for downstream compatibility.
*
* @internal This is exported for testing purposes only.
*/
export function findRuffVersionInSpec(spec: string): string | undefined {
const trimmedSpec = spec.trim();
const fullDepMatch = trimmedSpec.match(/^ruff\s*(.+)$/);
let versionSpec: string;
if (fullDepMatch) {
versionSpec = fullDepMatch[1];
} else {
return undefined;
}
// Strip trailing backslash (line continuation)
versionSpec = versionSpec.replace(/\\$/, "").trim();
// Strip environment markers (everything after ';')
const match = versionSpec.match(/^([^;]+)(?:;.*)?$/);
if (match) {
let version = match[1].trim();
if (version) {
// Strip leading '==' from exact version specifiers for compatibility with semver
if (version.startsWith("==")) {
version = version.slice(2);
}
if (trimmedSpec.includes(";")) {
core.warning(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
}
core.info(`Found ruff version in requirements file: ${version}`);
return version;
}
}
return undefined;
}
function getRuffVersionFromAllDependencies(
allDependencies: string[],
): string | undefined {
return allDependencies
.map((dep) => findRuffVersionInSpec(dep))
.find((version) => version !== undefined);
}
interface Pyproject {
project?: {
dependencies?: string[];
"optional-dependencies"?: Record<string, string[]>;
};
"dependency-groups"?: Record<string, Array<string | object>>;
tool?: {
poetry?: {
dependencies?: Record<string, string | object>;
group?: Record<string, { dependencies: Record<string, string | object> }>;
};
};
}
function parsePyproject(pyprojectContent: string): string | undefined {
const pyproject: Pyproject = toml.parse(pyprojectContent);
const dependencies: string[] = pyproject?.project?.dependencies || [];
const optionalDependencies: string[] = Object.values(
pyproject?.project?.["optional-dependencies"] || {},
).flat();
const devDependencies: string[] = Object.values(
pyproject?.["dependency-groups"] || {},
)
.flat()
.filter((item: string | object) => typeof item === "string");
return (
getRuffVersionFromAllDependencies(
dependencies.concat(optionalDependencies, devDependencies),
) || getRuffVersionFromPoetryGroups(pyproject)
);
}
function getRuffVersionFromPoetryGroups(
pyproject: Pyproject,
): string | undefined {
// Special handling for Poetry until it supports PEP 735
// See: <https://github.com/python-poetry/poetry/issues/9751>
const poetry = pyproject?.tool?.poetry || {};
const poetryGroups = Object.values(poetry.group || {});
if (poetry.dependencies) {
poetryGroups.unshift({ dependencies: poetry.dependencies });
}
return poetryGroups
.flatMap((group) => Object.entries(group.dependencies))
.map(([name, spec]) => {
if (typeof spec === "string") {
return findRuffVersionInSpec(`${name} ${spec}`);
}
return undefined;
})
.find((version) => version !== undefined);
}
export function getRuffVersionFromRequirementsFile(
filePath: string,
): string | undefined {
if (!fs.existsSync(filePath)) {
core.warning(`Could not find file: ${filePath}`);
return undefined;
}
const pyprojectContent = fs.readFileSync(filePath, "utf-8");
if (filePath.endsWith(".txt")) {
return getRuffVersionFromAllDependencies(pyprojectContent.split("\n"));
}
try {
return parsePyproject(pyprojectContent);
} catch (err) {
const message = (err as Error).message;
core.warning(`Error while parsing ${filePath}: ${message}`);
return undefined;
}
}
+161
View File
@@ -0,0 +1,161 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as glob from "@actions/glob";
const GLOB_PATTERN = /[*?[]/;
/**
* Split an action input into whitespace-delimited tokens.
*
* The action has historically documented both `args` and `src` as plain
* whitespace-delimited inputs. Keep that behavior instead of invoking a shell.
*/
export function splitInput(input: string): string[] {
return input.trim().split(/\s+/).filter(Boolean);
}
/**
* Expand glob patterns from the `src` input in a cross-platform way.
*
* Glob syntax is defined by @actions/glob. This action does not emulate shell
* expansion. Unmatched patterns are preserved so Ruff can report the missing
* path.
*/
export async function expandSourceInput(srcInput: string): Promise<string[]> {
const sources = splitInput(srcInput);
if (sources.length === 0) {
return [];
}
const expandedSources: string[] = [];
for (const source of sources) {
if (!hasGlobPattern(source)) {
expandedSources.push(source);
continue;
}
const matches = await globMatches(source);
expandedSources.push(...(matches.length > 0 ? matches : [source]));
}
return expandedSources;
}
/**
* Resolve the starting path for version discovery from the `src` input.
*
* Takes the first whitespace-delimited token. Literal paths are preserved. Glob
* patterns use their first sorted match when available, so pyproject.toml
* discovery starts from a path Ruff will actually check. Unmatched globs fall
* back to @actions/glob's search path.
*/
export async function getVersionSourceDirectory(
srcInput: string,
versionInput: string | undefined,
versionFileInput: string | undefined,
): Promise<string> {
if (hasInput(versionInput) || hasInput(versionFileInput)) {
return ".";
}
return await getSourceBasePath(srcInput);
}
export async function getSourceBasePath(srcInput: string): Promise<string> {
const firstSource = splitInput(srcInput)[0] ?? ".";
if (!hasGlobPattern(firstSource)) {
return stripTrailingSeparators(firstSource);
}
const globber = await createGlobber(firstSource);
const matches = (await globber.glob()).sort();
if (matches.length > 0) {
return (
(await preserveSourcePathStyle(firstSource, [matches[0] ?? "."]))[0] ??
"."
);
}
const searchPaths = globber.getSearchPaths().sort();
if (searchPaths.length > 0) {
return (
(
await preserveSourcePathStyle(firstSource, [searchPaths[0] ?? "."])
)[0] ?? "."
);
}
return ".";
}
function hasInput(input: string | undefined): boolean {
return input !== undefined && input.trim() !== "";
}
function hasGlobPattern(source: string): boolean {
return GLOB_PATTERN.test(source);
}
function stripTrailingSeparators(source: string): string {
const root = path.parse(source).root;
const strippedSource = source.replace(/[/\\]+$/, "");
if (root !== "" && strippedSource.length < root.length) {
return root;
}
return strippedSource || root || ".";
}
async function globMatches(pattern: string): Promise<string[]> {
const globber = await createGlobber(pattern);
const matches = await globber.glob();
return await preserveSourcePathStyle(pattern, matches.sort());
}
async function createGlobber(pattern: string): Promise<glob.Globber> {
return await glob.create(normalizeGlobPattern(pattern), {
excludeHiddenFiles: !explicitlyTargetsHiddenPath(pattern),
followSymbolicLinks: false,
implicitDescendants: false,
});
}
async function preserveSourcePathStyle(
pattern: string,
matches: string[],
): Promise<string[]> {
if (path.isAbsolute(pattern)) {
return matches;
}
const cwd = await fs.realpath(process.cwd());
return matches.map((match) => path.relative(cwd, match) || ".");
}
function explicitlyTargetsHiddenPath(pattern: string): boolean {
return splitSourcePath(pattern).some(isHiddenPathSegment);
}
function splitSourcePath(source: string): string[] {
const root = path.parse(source).root;
const relativePath = source.slice(root.length);
const separatorPattern = process.platform === "win32" ? /[/\\]+/ : /\/+/;
return relativePath.split(separatorPattern).filter(Boolean);
}
function isHiddenPathSegment(segment: string): boolean {
return segment.startsWith(".") && segment !== "." && segment !== "..";
}
function normalizeGlobPattern(pattern: string): string {
if (process.platform !== "win32") {
return pattern;
}
return pattern.replace(/\\/g, "/");
}
+209
View File
@@ -0,0 +1,209 @@
import fs from "node:fs";
import * as core from "@actions/core";
import * as toml from "smol-toml";
import { normalizeVersionSpecifier } from "./specifier";
import type { ParsedVersionFile, VersionFileFormat } from "./types";
interface VersionFileParser {
format: VersionFileFormat;
parse(filePath: string): string | undefined;
supports(filePath: string): boolean;
}
interface Pyproject {
project?: {
dependencies?: string[];
"optional-dependencies"?: Record<string, string[]>;
};
"dependency-groups"?: Record<string, Array<string | object>>;
tool?: {
poetry?: {
dependencies?: Record<string, string | object>;
group?: Record<string, { dependencies: Record<string, string | object> }>;
};
};
}
interface UvLock {
package?: Array<{ name?: string; version?: string }>;
}
const VERSION_FILE_PARSERS: VersionFileParser[] = [
{
format: "pyproject.toml",
parse: (filePath) => {
const fileContent = fs.readFileSync(filePath, "utf-8");
return getRuffVersionFromPyprojectContent(fileContent);
},
supports: (filePath) => filePath.endsWith("pyproject.toml"),
},
{
format: "uv.lock",
parse: (filePath) => {
const fileContent = fs.readFileSync(filePath, "utf-8");
return getRuffVersionFromUvLockContent(fileContent);
},
supports: (filePath) => filePath.endsWith("uv.lock"),
},
{
format: "requirements",
parse: (filePath) => {
const fileContent = fs.readFileSync(filePath, "utf-8");
return getRuffVersionFromRequirementsText(fileContent);
},
supports: (filePath) => filePath.endsWith(".txt"),
},
];
export function getParsedVersionFile(
filePath: string,
): ParsedVersionFile | undefined {
core.info(`Trying to find version for ruff in: ${filePath}`);
if (!fs.existsSync(filePath)) {
core.warning(`Could not find file: ${filePath}`);
return undefined;
}
const parser = getVersionFileParser(filePath);
if (parser === undefined) {
return undefined;
}
try {
const specifier = parser.parse(filePath);
if (specifier === undefined) {
return undefined;
}
const normalizedSpecifier = normalizeVersionSpecifier(specifier);
core.info(`Found version for ruff in ${filePath}: ${normalizedSpecifier}`);
return {
format: parser.format,
specifier: normalizedSpecifier,
};
} catch (error) {
core.warning(
`Error while parsing ${filePath}: ${(error as Error).message}`,
);
return undefined;
}
}
export function getRuffVersionFromFile(filePath: string): string | undefined {
return getParsedVersionFile(filePath)?.specifier;
}
export function findRuffVersionInSpec(spec: string): string | undefined {
const trimmedSpec = spec.trim();
if (!trimmedSpec.startsWith("ruff")) {
return undefined;
}
let versionSpec = trimmedSpec.slice("ruff".length);
if (!versionSpec.match(/^(?:\s+|[=<>~!])/)) {
return undefined;
}
versionSpec = versionSpec.replace(/\\$/, "").trim();
const match = versionSpec.match(/^([^;]+)(?:;.*)?$/);
if (match) {
let version = match[1].trim();
if (version) {
version = normalizeVersionSpecifier(version);
if (trimmedSpec.includes(";")) {
core.warning(
"Environment markers are ignored. ruff is a standalone tool that works independently of Python version.",
);
}
core.info(`Found ruff version in requirements file: ${version}`);
return version;
}
}
return undefined;
}
export function getRuffVersionFromRequirementsText(
fileContent: string,
): string | undefined {
return getRuffVersionFromAllDependencies(fileContent.split("\n"));
}
export function getRuffVersionFromPyprojectContent(
pyprojectContent: string,
): string | undefined {
const pyproject = parsePyprojectContent(pyprojectContent);
return getRuffVersionFromParsedPyproject(pyproject);
}
export function parsePyprojectContent(pyprojectContent: string): Pyproject {
return toml.parse(pyprojectContent) as Pyproject;
}
export function getRuffVersionFromUvLockContent(
uvLockContent: string,
): string | undefined {
const uvLock = toml.parse(uvLockContent) as UvLock;
const ruffPackage = (uvLock.package || []).find((pkg) => pkg.name === "ruff");
if (ruffPackage?.version === undefined) {
return undefined;
}
core.info(`Found ruff version in uv.lock: ${ruffPackage.version}`);
return ruffPackage.version;
}
function getVersionFileParser(filePath: string): VersionFileParser | undefined {
return VERSION_FILE_PARSERS.find((parser) => parser.supports(filePath));
}
function getRuffVersionFromParsedPyproject(
pyproject: Pyproject,
): string | undefined {
const dependencies: string[] = pyproject.project?.dependencies || [];
const optionalDependencies: string[] = Object.values(
pyproject.project?.["optional-dependencies"] || {},
).flat();
const devDependencies: string[] = Object.values(
pyproject["dependency-groups"] || {},
)
.flat()
.filter((item: string | object) => typeof item === "string");
return (
getRuffVersionFromAllDependencies(
dependencies.concat(optionalDependencies, devDependencies),
) || getRuffVersionFromPoetryGroups(pyproject)
);
}
function getRuffVersionFromPoetryGroups(
pyproject: Pyproject,
): string | undefined {
const poetry = pyproject.tool?.poetry || {};
const poetryGroups = Object.values(poetry.group || {});
if (poetry.dependencies) {
poetryGroups.unshift({ dependencies: poetry.dependencies });
}
return poetryGroups
.flatMap((group) => Object.entries(group.dependencies))
.map(([name, spec]) => {
if (typeof spec === "string") {
return findRuffVersionInSpec(`${name} ${spec}`);
}
return undefined;
})
.find((version) => version !== undefined);
}
function getRuffVersionFromAllDependencies(
allDependencies: string[],
): string | undefined {
return allDependencies
.map((dependency) => findRuffVersionInSpec(dependency))
.find((version) => version !== undefined);
}
+125
View File
@@ -0,0 +1,125 @@
import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import * as pep440 from "@renovatebot/pep440";
import { getAllVersions, getLatestVersion } from "../download/manifest";
import {
type ParsedVersionSpecifier,
parseVersionSpecifier,
} from "./specifier";
import type { ResolveRuffVersionOptions } from "./types";
import { resolveVersionRequest } from "./version-request-resolver";
interface ConcreteVersionResolutionContext {
manifestUrl?: string;
parsedSpecifier: ParsedVersionSpecifier;
}
interface ConcreteVersionResolver {
resolve(
context: ConcreteVersionResolutionContext,
): Promise<string | undefined>;
}
class ExactVersionResolver implements ConcreteVersionResolver {
async resolve(
context: ConcreteVersionResolutionContext,
): Promise<string | undefined> {
if (context.parsedSpecifier.kind !== "exact") {
return undefined;
}
core.debug(
`Version ${context.parsedSpecifier.normalized} is an explicit version.`,
);
return context.parsedSpecifier.normalized;
}
}
class LatestVersionResolver implements ConcreteVersionResolver {
async resolve(
context: ConcreteVersionResolutionContext,
): Promise<string | undefined> {
if (context.parsedSpecifier.kind !== "latest") {
return undefined;
}
return await getLatestVersion(context.manifestUrl);
}
}
class RangeVersionResolver implements ConcreteVersionResolver {
async resolve(
context: ConcreteVersionResolutionContext,
): Promise<string | undefined> {
if (context.parsedSpecifier.kind !== "range") {
return undefined;
}
const availableVersions = await getAllVersions(context.manifestUrl);
const resolvedVersion = maxSatisfying(
availableVersions,
context.parsedSpecifier.normalized,
);
if (resolvedVersion === undefined) {
throw new Error(`No version found for ${context.parsedSpecifier.raw}`);
}
core.debug(`Resolved version: ${resolvedVersion}`);
return resolvedVersion;
}
}
const CONCRETE_VERSION_RESOLVERS: ConcreteVersionResolver[] = [
new ExactVersionResolver(),
new LatestVersionResolver(),
new RangeVersionResolver(),
];
export async function resolveRuffVersion(
options: ResolveRuffVersionOptions,
): Promise<string> {
const request = resolveVersionRequest(options);
return await resolveVersion(request.specifier, options.manifestFile);
}
export async function resolveVersion(
versionInput: string,
manifestUrl?: string,
): Promise<string> {
core.debug(`Resolving ${versionInput}...`);
const context: ConcreteVersionResolutionContext = {
manifestUrl,
parsedSpecifier: parseVersionSpecifier(versionInput),
};
for (const resolver of CONCRETE_VERSION_RESOLVERS) {
const version = await resolver.resolve(context);
if (version !== undefined) {
return version;
}
}
throw new Error(`No version found for ${versionInput}`);
}
function maxSatisfying(
versions: string[],
version: string,
): string | undefined {
const maxSemver = tc.evaluateVersions(versions, version);
if (maxSemver !== "") {
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
return maxSemver;
}
const maxPep440 = pep440.maxSatisfying(versions, version);
if (maxPep440 !== null) {
core.debug(
`Found a version that satisfies the pep440 specifier: ${maxPep440}`,
);
return maxPep440;
}
return undefined;
}
+57
View File
@@ -0,0 +1,57 @@
import * as tc from "@actions/tool-cache";
export type ParsedVersionSpecifier =
| {
kind: "exact";
normalized: string;
raw: string;
}
| {
kind: "latest";
normalized: "latest";
raw: string;
}
| {
kind: "range";
normalized: string;
raw: string;
};
export function normalizeVersionSpecifier(specifier: string): string {
const trimmedSpecifier = specifier.trim();
if (trimmedSpecifier.startsWith("==")) {
return trimmedSpecifier.slice(2);
}
return trimmedSpecifier;
}
export function parseVersionSpecifier(
specifier: string,
): ParsedVersionSpecifier {
const raw = specifier.trim();
const normalized = normalizeVersionSpecifier(raw);
if (normalized === "latest") {
return {
kind: "latest",
normalized: "latest",
raw,
};
}
if (tc.isExplicitVersion(normalized)) {
return {
kind: "exact",
normalized,
raw,
};
}
return {
kind: "range",
normalized,
raw,
};
}
+27
View File
@@ -0,0 +1,27 @@
export type VersionSource =
| "input"
| "version-file"
| "pyproject.toml"
| "default";
export type VersionFileFormat = "pyproject.toml" | "requirements" | "uv.lock";
export interface ParsedVersionFile {
format: VersionFileFormat;
specifier: string;
}
export interface ResolveRuffVersionOptions {
manifestFile?: string;
sourceDirectory: string;
version?: string;
versionFile?: string;
workspaceRoot: string;
}
export interface VersionRequest {
format?: VersionFileFormat;
source: VersionSource;
sourcePath?: string;
specifier: string;
}
+162
View File
@@ -0,0 +1,162 @@
import * as core from "@actions/core";
import { findPyprojectToml } from "../utils/pyproject-finder";
import { getParsedVersionFile } from "./file-parser";
import { normalizeVersionSpecifier } from "./specifier";
import type {
ParsedVersionFile,
ResolveRuffVersionOptions,
VersionRequest,
} from "./types";
export interface VersionRequestResolver {
resolve(context: VersionRequestContext): VersionRequest | undefined;
}
export class VersionRequestContext {
readonly sourceDirectory: string;
readonly version: string | undefined;
readonly versionFile: string | undefined;
readonly workspaceRoot: string;
private readonly parsedFiles = new Map<
string,
ParsedVersionFile | undefined
>();
constructor(
version: string | undefined,
versionFile: string | undefined,
sourceDirectory: string,
workspaceRoot: string,
) {
this.version = version;
this.versionFile = versionFile;
this.sourceDirectory = sourceDirectory;
this.workspaceRoot = workspaceRoot;
}
getVersionFile(filePath: string): ParsedVersionFile | undefined {
const cachedResult = this.parsedFiles.get(filePath);
if (cachedResult !== undefined || this.parsedFiles.has(filePath)) {
return cachedResult;
}
const result = getParsedVersionFile(filePath);
this.parsedFiles.set(filePath, result);
return result;
}
getWorkspacePyprojectPath(): string | undefined {
return findPyprojectToml(this.sourceDirectory, this.workspaceRoot);
}
}
export class ExplicitInputVersionResolver implements VersionRequestResolver {
resolve(context: VersionRequestContext): VersionRequest | undefined {
if (context.version === undefined) {
return undefined;
}
return {
source: "input",
specifier: normalizeVersionSpecifier(context.version),
};
}
}
export class VersionFileVersionResolver implements VersionRequestResolver {
resolve(context: VersionRequestContext): VersionRequest | undefined {
if (context.versionFile === undefined) {
return undefined;
}
const versionFile = context.getVersionFile(context.versionFile);
if (versionFile === undefined) {
core.warning(
`Could not parse version from ${context.versionFile}. Using latest version.`,
);
return undefined;
}
return {
format: versionFile.format,
source: "version-file",
sourcePath: context.versionFile,
specifier: versionFile.specifier,
};
}
}
export class WorkspaceVersionResolver implements VersionRequestResolver {
resolve(context: VersionRequestContext): VersionRequest | undefined {
const pyprojectPath = context.getWorkspacePyprojectPath();
if (!pyprojectPath) {
core.info("Could not find pyproject.toml. Using latest version.");
return undefined;
}
const versionFile = context.getVersionFile(pyprojectPath);
if (versionFile === undefined) {
core.info(
`Could not parse version from ${pyprojectPath}. Using latest version.`,
);
return undefined;
}
return {
format: versionFile.format,
source: "pyproject.toml",
sourcePath: pyprojectPath,
specifier: versionFile.specifier,
};
}
}
export class LatestVersionResolver implements VersionRequestResolver {
resolve(): VersionRequest {
return {
source: "default",
specifier: "latest",
};
}
}
const VERSION_REQUEST_RESOLVERS: VersionRequestResolver[] = [
new ExplicitInputVersionResolver(),
new VersionFileVersionResolver(),
new WorkspaceVersionResolver(),
new LatestVersionResolver(),
];
export function resolveVersionRequest(
options: ResolveRuffVersionOptions,
): VersionRequest {
const version = emptyToUndefined(options.version);
const versionFile = emptyToUndefined(options.versionFile);
if (version !== undefined && versionFile !== undefined) {
throw new Error(
"It is not allowed to specify both version and version-file",
);
}
const context = new VersionRequestContext(
version,
versionFile,
options.sourceDirectory,
options.workspaceRoot,
);
for (const resolver of VERSION_REQUEST_RESOLVERS) {
const request = resolver.resolve(context);
if (request !== undefined) {
return request;
}
}
throw new Error("Could not resolve a requested Ruff version.");
}
function emptyToUndefined(value: string | undefined): string | undefined {
return value === undefined || value === "" ? undefined : value;
}
+8 -8
View File
@@ -1,12 +1,12 @@
{
"compilerOptions": {
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"outDir": "./lib" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"strict": true /* Enable all strict type-checking options. */,
"target": "ES2022" /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true,
"isolatedModules": true,
"module": "esnext",
"moduleResolution": "bundler",
"noImplicitAny": true,
"strict": true,
"target": "ES2022"
},
"exclude": ["node_modules", "**/*.test.ts"]
"include": ["src/**/*.ts"]
}