Compare commits

..
Author SHA1 Message Date
dependabot[bot]andGitHub 8ba040397c Bump peter-evans/create-pull-request from 8.1.0 to 8.1.1
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 8.1.0 to 8.1.1.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/c0f553fe549906ede9cf27b5156039d195d2ece0...5f6978faf089d4d20b00c7766989d076bb2fc7f1)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: 8.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 04:58:46 +00: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
eifinger-botandGitHub 4919ec5cf1 fix: persist credentials for git push in update-major-minor-tags workflow (#312)
## Problem

After upgrading to `actions/checkout@v6`, the `update-major-minor-tags`
workflow fails with:

```
fatal: could not read Username for 'https://github.com': No such device or address
```

Example:
https://github.com/astral-sh/ruff-action/actions/runs/21509988231/job/61974754475

## Root Cause

`actions/checkout@v6` changed the behavior of `persist-credentials:
false`. In v6, credentials are immediately cleaned up after checkout,
whereas in v5 they remained available during the job.

The workflow sets `persist-credentials: false` and then tries to `git
push` — but the auth credentials are already gone.

## Fix

Explicitly set `persist-credentials: true` (removing the `false` value).
The workflow already has minimal permissions (`contents: write`) scoped
only to this job, so this is safe.

Added a `zizmor: ignore[artipacked]` comment in case the security linter
flags this pattern.
2026-01-30 19:43:42 +01:00
eifinger-botandGitHub 1977806bc6 ci: add zizmor security linting (#313)
## Summary

Adds [zizmor](https://github.com/woodruffw/zizmor) security linting to
the CI workflow to scan GitHub Actions workflows for security issues.

## Changes

- Added `permissions: security-events: write` to the `lint` job
(required for zizmor)
- Added `zizmorcore/zizmor-action@v0.4.1` step after actionlint

Mirrors the setup in
[astral-sh/setup-uv](https://github.com/astral-sh/setup-uv/blob/803947b9bd8e9f986429fa0c5a41c367cd732b41/.github/workflows/test.yml#L29-L30).
2026-01-30 18:37:28 +00:00
44 changed files with 66371 additions and 78062 deletions
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "es2022",
"types": ["node"]
},
"include": ["check-all-tests-passed-needs.ts"]
}
+1 -1
View File
@@ -17,6 +17,6 @@ jobs:
pull-requests: read
steps:
- name: 🚀 Run Release Drafter
uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0
uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+58
View File
@@ -0,0 +1,58 @@
name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Release version (e.g., 8.1.0)"
required: true
type: string
permissions: {}
jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Validate version
env:
VERSION: ${{ 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
- name: Publish release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: v${{ inputs.version }}
run: |
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
+34 -3
View File
@@ -15,22 +15,27 @@ permissions: {}
jobs:
lint:
runs-on: ubuntu-latest
permissions:
security-events: write # for zizmor
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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
@@ -426,6 +431,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:
@@ -451,6 +481,7 @@ jobs:
- test-failure
- test-multiple-src
- test-parent-directory-pyproject
- test-custom-manifest-file
if: always()
steps:
- name: All tests passed
+4 -3
View File
@@ -16,15 +16,16 @@ jobs:
persist-credentials: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.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:
@@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
persist-credentials: true
- name: Update Major Minor Tags
run: |
set -x
+1
View File
@@ -0,0 +1 @@
24
+44 -16
View File
@@ -20,20 +20,25 @@ 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)
## Usage
| Input | Description | Default |
|----------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| `version` | The version of Ruff to install. See [Install specific versions](#install-specific-versions) | `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 |
| `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` |
| Input | Description | Default |
|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|--------------------|
| `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 |
| `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 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
@@ -90,10 +95,10 @@ you can use the `args` input to overwrite the default value (`check`):
### 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.
@@ -146,7 +151,8 @@ 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` and `requirements.txt` 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
@@ -155,6 +161,26 @@ Currently `pyproject.toml` and `requirements.txt` are supported.
version-file: "my-path/to/pyproject.toml-or-requirements.txt"
```
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@v3
with:
version: "latest"
manifest-file: "https://example.com/ruff.ndjson"
```
### Validate checksum
You can specify a checksum to validate the downloaded executable. Checksums up to the default version
@@ -171,9 +197,11 @@ 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 uv 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.
You can provide a token via `github-token` to authenticate those 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)
@@ -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"}]}
+511
View File
@@ -0,0 +1,511 @@
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("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",
);
});
});
});
+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");
});
});
});
+128
View File
@@ -0,0 +1,128 @@
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 } = 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");
});
});
});
@@ -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");
});
});
+8 -6
View File
@@ -11,27 +11,29 @@ inputs:
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 or requirements.txt 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
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
+29363
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
+29544
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"],
};
+4692 -746
View File
File diff suppressed because it is too large Load Diff
+20 -17
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,26 @@
"author": "@eifinger",
"license": "Apache-2.0",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/tool-cache": "^2.0.2",
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/tool-cache": "^4.0.0",
"@octokit/core": "^7.0.3",
"@octokit/plugin-paginate-rest": "^13.1.1",
"@octokit/plugin-rest-endpoint-methods": "^16.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",
});
}),
);
+374
View File
@@ -1,5 +1,379 @@
// AUTOGENERATED_DO_NOT_EDIT
export const KNOWN_CHECKSUMS: { [key: string]: string } = {
"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":
+139 -148
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,46 @@ export async function downloadVersion(
platform: Platform,
arch: Architecture,
version: string,
checkSum: string | undefined,
checksum: string | undefined,
githubToken: string,
manifestUrl?: string,
): 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),
);
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 +80,60 @@ export async function downloadVersion(
version,
arch,
);
return { cachedToolDir, version: version };
return { cachedToolDir, version };
}
function constructDownloadUrl(
version: string,
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,
): Promise<string> {
const mirrorUrl = rewriteToMirror(downloadUrl);
const resolvedDownloadUrl = mirrorUrl ?? downloadUrl;
try {
return await downloadFile(
resolvedDownloadUrl,
mirrorUrl !== undefined ? undefined : githubToken,
);
} catch (err) {
if (mirrorUrl === undefined) {
throw err;
}
core.warning(
`Failed to download from mirror, falling back to GitHub Releases: ${(err as Error).message}`,
);
return await downloadFile(
constructDownloadUrl(version, platform, arch),
githubToken,
);
}
const versionPrefix = semver.lte(version, "v0.4.10") ? "v" : "";
return `https://github.com/${OWNER}/${REPO}/releases/download/${versionPrefix}${version}/${artifact}${extension}`;
}
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 +148,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 +161,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,
): string | undefined {
const maxSemver = tc.evaluateVersions(versions, version);
if (maxSemver !== "") {
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
return maxSemver;
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} .`;
}
const maxPep440 = pep440.maxSatisfying(versions, version);
if (maxPep440 !== null) {
core.debug(
`Found a version that satisfies the pep440 specifier: ${maxPep440}`,
);
return maxPep440;
}
return undefined;
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 {
return downloadUrl.startsWith(GITHUB_RELEASES_PREFIX)
? githubToken
: undefined;
}
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)}`;
}
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(", ");
}
+18 -41
View File
@@ -4,13 +4,13 @@ import * as exec from "@actions/exec";
import * as semver from "semver";
import {
downloadVersion,
resolveVersion,
tryGetFromToolCache,
} from "./download/download-version";
import {
args,
checkSum,
githubToken,
manifestFile,
src,
version,
versionFile as versionFileInput,
@@ -21,8 +21,7 @@ import {
getPlatform,
type Platform,
} from "./utils/platforms";
import { getRuffVersionFromRequirementsFile } from "./utils/pyproject";
import { findPyprojectToml } from "./utils/pyproject-finder";
import { resolveRuffVersion } from "./version/resolve";
async function run(): Promise<void> {
const platform = getPlatform();
@@ -62,6 +61,7 @@ async function setupRuff(
githubToken: string,
): 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 +82,7 @@ async function setupRuff(
resolvedVersion,
checkSum,
githubToken,
manifestUrl,
);
return {
@@ -91,38 +92,13 @@ 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(
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);
return await resolveRuffVersion({
manifestFile: manifestFile || undefined,
sourceDirectory: src,
version,
versionFile: versionFileInput,
workspaceRoot: process.env.GITHUB_WORKSPACE || ".",
});
}
function addRuffToPath(cachedPath: string): void {
@@ -136,16 +112,17 @@ 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[],
+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,
});
+1
View File
@@ -6,3 +6,4 @@ 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");
-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");
});
});
});
+9 -30
View File
@@ -26,30 +26,20 @@ 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;
}
/**
@@ -62,18 +52,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,
);
return !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
} catch {
return undefined;
}
): boolean {
const relativePath = path.relative(workspaceRoot, checkPath);
return !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
}
-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;
}
}
+185
View File
@@ -0,0 +1,185 @@
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> }>;
};
};
}
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: "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;
}
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";
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"]
}