mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-10-01 00:29:51 +00:00
Compare commits
574 Commits
example
...
zb/setup-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cc9e96834 | ||
|
|
bcf05803b5 | ||
|
|
b92a892142 | ||
|
|
227a0f6bea | ||
|
|
525b679e45 | ||
|
|
c18668ad3c | ||
|
|
ffe1476305 | ||
|
|
f5548c5552 | ||
|
|
a761a4e9af | ||
|
|
3377a30666 | ||
|
|
dfb5f38677 | ||
|
|
45c121f982 | ||
|
|
8073452fd4 | ||
|
|
bec219d24c | ||
|
|
b90ec40d15 | ||
|
|
421feb646d | ||
|
|
f634bf473a | ||
|
|
a6772c8f0a | ||
|
|
e105c8fb1d | ||
|
|
cd13f92170 | ||
|
|
3aef7b92c5 | ||
|
|
d08d816a1e | ||
|
|
19b4d1e990 | ||
|
|
7211c71869 | ||
|
|
5ec49509d7 | ||
|
|
92a7c9ffa6 | ||
|
|
51a7fe0131 | ||
|
|
42bafdadf9 | ||
|
|
b555614122 | ||
|
|
a1e5847df3 | ||
|
|
20cfd1bf94 | ||
|
|
d73a0cab66 | ||
|
|
ae3b92d1bd | ||
|
|
ae62891fec | ||
|
|
f9cdb47d48 | ||
|
|
4f6036f71c | ||
|
|
8d6402c9b7 | ||
|
|
46f427bd47 | ||
|
|
8ed89c5114 | ||
|
|
8473c7fea4 | ||
|
|
18d451d679 | ||
|
|
f45168497b | ||
|
|
b68407c192 | ||
|
|
696e4e1bf2 | ||
|
|
7cd5f7fd2e | ||
|
|
d64009a7c5 | ||
|
|
7e7e21ddc3 | ||
|
|
094aa226ed | ||
|
|
d269b9917d | ||
|
|
c6081965dd | ||
|
|
c771a70e62 | ||
|
|
2f537ca87c | ||
|
|
2269552d54 | ||
|
|
47a7f4fb2e | ||
|
|
71966eff34 | ||
|
|
f12b1f0a84 | ||
|
|
ecd24dd710 | ||
|
|
6a19136684 | ||
|
|
11f9893b08 | ||
|
|
f798556032 | ||
|
|
e80544d808 | ||
|
|
f98e069381 | ||
|
|
cd462639a9 | ||
|
|
11245c7e12 | ||
|
|
d31148d669 | ||
|
|
17c398959b | ||
|
|
3cc3c11fdf | ||
|
|
9225f843d7 | ||
|
|
fc16fa3bbf | ||
|
|
a1a7345c8e | ||
|
|
a5e9cbfd5f | ||
|
|
c5680ec91f | ||
|
|
c86fe4ef1f | ||
|
|
224c887d48 | ||
|
|
b173788282 | ||
|
|
e53da17296 | ||
|
|
a9b33f0240 | ||
|
|
3faa3174e6 | ||
|
|
ca5ddd015e | ||
|
|
e2f6a928de | ||
|
|
ed73b5df24 | ||
|
|
21d5da3bc3 | ||
|
|
fac544c07d | ||
|
|
7390f777b0 | ||
|
|
363c64a728 | ||
|
|
c4fcbafce4 | ||
|
|
8e642c5e62 | ||
|
|
a92cb43098 | ||
|
|
e07f2ac4b7 | ||
|
|
bc4034eedf | ||
|
|
df42d4f6ba | ||
|
|
b9c8c4c7ba | ||
|
|
80cc27528e | ||
|
|
818affc359 | ||
|
|
feda7fc6a9 | ||
|
|
8dc20b2aca | ||
|
|
e7108c6ccc | ||
|
|
12d13f90bc | ||
|
|
7470949a2c | ||
|
|
ed07c76224 | ||
|
|
ba17a16c0a | ||
|
|
853401723d | ||
|
|
7568f55a9a | ||
|
|
a81585cbb0 | ||
|
|
88aa608651 | ||
|
|
00714ea9dc | ||
|
|
c4fec0d78d | ||
|
|
9d91aa17e1 | ||
|
|
363818fa0d | ||
|
|
05143d3dcd | ||
|
|
2ae9516c03 | ||
|
|
c0c76fcf76 | ||
|
|
dff86cf972 | ||
|
|
c0b7f63f92 | ||
|
|
d854a6dce4 | ||
|
|
08807647e7 | ||
|
|
717d6aba0f | ||
|
|
5a911eb3a3 | ||
|
|
080c31e04c | ||
|
|
b3e97d2ba1 | ||
|
|
7dd591db95 | ||
|
|
1541b77626 | ||
|
|
cdfb2ee6dd | ||
|
|
cb84d12dc6 | ||
|
|
1912cc65f2 | ||
|
|
a0b52019f1 | ||
|
|
7b222e12b6 | ||
|
|
1c15d185f0 | ||
|
|
d7fe1a5a18 | ||
|
|
16592cddee | ||
|
|
cec208311d | ||
|
|
4dd8ab4520 | ||
|
|
7fdbe7cf0c | ||
|
|
485abd05e5 | ||
|
|
f82eb19c06 | ||
|
|
868d1f74d9 | ||
|
|
447e6d02b1 | ||
|
|
5c62c59261 | ||
|
|
e1a7373adb | ||
|
|
89709315bb | ||
|
|
8cc8d1cbfc | ||
|
|
c20049fc26 | ||
|
|
37802adc94 | ||
|
|
9f00d186ce | ||
|
|
fd8f376b22 | ||
|
|
f9070de1ea | ||
|
|
cadb67bdc9 | ||
|
|
e06108dd0a | ||
|
|
0f6ec07aaf | ||
|
|
821e5c9815 | ||
|
|
6ee6290f1c | ||
|
|
9f332a133a | ||
|
|
0acf9708ce | ||
|
|
fe3617d6e9 | ||
|
|
2ff70eebcc | ||
|
|
5ba8a7e5d0 | ||
|
|
4bc8fabc0c | ||
|
|
950b623541 | ||
|
|
09ff6fe0ae | ||
|
|
bd870193dd | ||
|
|
f8858e6756 | ||
|
|
5a095e7a20 | ||
|
|
b12532f27f | ||
|
|
0098a7571c | ||
|
|
2e7ed0e2bb | ||
|
|
04224aa8ca | ||
|
|
2bc602ff89 | ||
|
|
dd9d748439 | ||
|
|
14eede1834 | ||
|
|
c452423b2c | ||
|
|
eac588ad8d | ||
|
|
a97c6cbe9c | ||
|
|
02182fa02a | ||
|
|
a3b3eaea92 | ||
|
|
78cebeceac | ||
|
|
b6b8e2cd6a | ||
|
|
e31bec8546 | ||
|
|
db2b65ebae | ||
|
|
3511ff7054 | ||
|
|
99b0f0474b | ||
|
|
db4d6bf3d6 | ||
|
|
98e1309028 | ||
|
|
5ed2ede620 | ||
|
|
5fca386933 | ||
|
|
803947b9bd | ||
|
|
24553ac46d | ||
|
|
085087a5d3 | ||
|
|
9cfd029643 | ||
|
|
dd9d55bc18 | ||
|
|
8512ad0289 | ||
|
|
cc5581700e | ||
|
|
61cb8a9741 | ||
|
|
11050edb83 | ||
|
|
1d22fafd8b | ||
|
|
f4ed82a8ce | ||
|
|
e0409b43c0 | ||
|
|
702b425af1 | ||
|
|
2630c86ac3 | ||
|
|
45cfcb3be5 | ||
|
|
ce0a8994de | ||
|
|
9c8d030b7f | ||
|
|
681c641aba | ||
|
|
2e85713bb0 | ||
|
|
58b6d7b303 | ||
|
|
e8b52af86e | ||
|
|
ed21f2f24f | ||
|
|
93202d8fbe | ||
|
|
5ce090076d | ||
|
|
4180991cd9 | ||
|
|
0439606c8e | ||
|
|
7dd56c18e9 | ||
|
|
9c12baee96 | ||
|
|
64f7f4e15f | ||
|
|
5ae467fbf9 | ||
|
|
06e4edb239 | ||
|
|
8f1d388b4b | ||
|
|
d500d41ebf | ||
|
|
1e64fb113b | ||
|
|
be7fc19b41 | ||
|
|
1e862dfacb | ||
|
|
d7d33e16d4 | ||
|
|
486d0b8872 | ||
|
|
5a7eac68fb | ||
|
|
b49dc9e882 | ||
|
|
30ce38e206 | ||
|
|
0d20755a23 | ||
|
|
8491d1d9a3 | ||
|
|
85856786d1 | ||
|
|
22d500a65c | ||
|
|
14d557131d | ||
|
|
29cd2350cd | ||
|
|
2ddd2b9cb3 | ||
|
|
b7bf78939d | ||
|
|
cb6c0a53d9 | ||
|
|
dffc6292f2 | ||
|
|
6e346e1653 | ||
|
|
3ccd0fd498 | ||
|
|
ce6dbd84e1 | ||
|
|
2382069a66 | ||
|
|
b1daf91f4e | ||
|
|
3259c6206f | ||
|
|
bf8e8ed895 | ||
|
|
9c6b5e9fb5 | ||
|
|
a5129e99f4 | ||
|
|
d18bcc753a | ||
|
|
bd1f875aba | ||
|
|
1a91c3851d | ||
|
|
c79f606987 | ||
|
|
e0249f1599 | ||
|
|
6d2eb15b49 | ||
|
|
3495667518 | ||
|
|
eb1897b8dc | ||
|
|
d78d791822 | ||
|
|
535dc2664c | ||
|
|
f610be5ff9 | ||
|
|
3deccc0075 | ||
|
|
d9ee7e2f26 | ||
|
|
59a0868fea | ||
|
|
c952556164 | ||
|
|
51c3328db2 | ||
|
|
f2859da213 | ||
|
|
f9c6974d8b | ||
|
|
82f21a54fe | ||
|
|
d8a37f6566 | ||
|
|
d0cc045d04 | ||
|
|
2841f9f5c1 | ||
|
|
e554b93b80 | ||
|
|
c7d85d9988 | ||
|
|
07f2cb5db9 | ||
|
|
208b0c0ee4 | ||
|
|
b75a909f75 | ||
|
|
ffff8aa2b5 | ||
|
|
95d0e233fa | ||
|
|
dc724a12b6 | ||
|
|
f67343ac2e | ||
|
|
4dd9f52a47 | ||
|
|
e1e6fe7910 | ||
|
|
b1836110f7 | ||
|
|
557e51de59 | ||
|
|
1b46e13ec8 | ||
|
|
26cf676705 | ||
|
|
4e1e303f7d | ||
|
|
4959332f0f | ||
|
|
adeb28643f | ||
|
|
fce199e243 | ||
|
|
f758a4a1eb | ||
|
|
c0e7e93474 | ||
|
|
fda2399cb3 | ||
|
|
d9e0f98d3f | ||
|
|
e5d42a2b46 | ||
|
|
d664c2a1d1 | ||
|
|
c35b8eac36 | ||
|
|
4109b4033f | ||
|
|
1463845d3c | ||
|
|
ad5ded2d63 | ||
|
|
142240426d | ||
|
|
632449003a | ||
|
|
2a967c9b97 | ||
|
|
43f37368c9 | ||
|
|
4fb0c07c55 | ||
|
|
e92bafb625 | ||
|
|
2c7142f755 | ||
|
|
23482a31a8 | ||
|
|
4ac06a054e | ||
|
|
7edac99f96 | ||
|
|
05273c154d | ||
|
|
de545d4421 | ||
|
|
b75ff7d7b8 | ||
|
|
c893ac1cb2 | ||
|
|
a905f0040b | ||
|
|
d4219d1620 | ||
|
|
aaefb91b77 | ||
|
|
c05b3e180b | ||
|
|
1bf1493664 | ||
|
|
b4c8c41d99 | ||
|
|
8e581f64a9 | ||
|
|
bd01e18f51 | ||
|
|
c6a5ebaafe | ||
|
|
790df8f465 | ||
|
|
445689ea25 | ||
|
|
a02a550bdd | ||
|
|
60cc2b4585 | ||
|
|
7bbb36f434 | ||
|
|
60ecb381b4 | ||
|
|
252c995424 | ||
|
|
477a814f2d | ||
|
|
9b19f8f4b1 | ||
|
|
d44461ea9f | ||
|
|
c19c1b1ffd | ||
|
|
f3121ef80e | ||
|
|
f0ec1fc3b3 | ||
|
|
e3d2ea5ff3 | ||
|
|
b3d7ca7ac0 | ||
|
|
0e0f4bfefa | ||
|
|
71bb8825e4 | ||
|
|
1417e89049 | ||
|
|
1761eea391 | ||
|
|
9864bc9c96 | ||
|
|
0e9cccb4b0 | ||
|
|
b6f9e9c734 | ||
|
|
b87cce2fc5 | ||
|
|
9641fcd493 | ||
|
|
a4f1f549e9 | ||
|
|
023b8ec2bc | ||
|
|
ced7c1dde4 | ||
|
|
41bd088443 | ||
|
|
75567654b6 | ||
|
|
6b9c6063ab | ||
|
|
ef6bcdff59 | ||
|
|
9a311713f4 | ||
|
|
c7f87aa956 | ||
|
|
aadfaf08d6 | ||
|
|
a0f9da6273 | ||
|
|
ec4c691628 | ||
|
|
aa1290542e | ||
|
|
fcaddda076 | ||
|
|
fb3a0a97fa | ||
|
|
d4b2f3b6ec | ||
|
|
594f292eef | ||
|
|
9ad030384d | ||
|
|
9e2c33a082 | ||
|
|
839076380b | ||
|
|
9bf3815166 | ||
|
|
0c5e2b8115 | ||
|
|
794ea9455c | ||
|
|
2d49baf2b6 | ||
|
|
4fa25599ce | ||
|
|
224dce1d79 | ||
|
|
22695119d7 | ||
|
|
bf8ec1ea35 | ||
|
|
1fb7cdfc29 | ||
|
|
57fe17c2c5 | ||
|
|
72002e8b87 | ||
|
|
19df292e24 | ||
|
|
7d9a2d93c4 | ||
|
|
389b596663 | ||
|
|
04c950a723 | ||
|
|
d02c4c2d68 | ||
|
|
a4fd982317 | ||
|
|
a05a582c56 | ||
|
|
0e855c90d0 | ||
|
|
d8a276f11f | ||
|
|
59ae1ec55b | ||
|
|
f94ec6bedd | ||
|
|
0313224678 | ||
|
|
754a7d4c2d | ||
|
|
b498c74bf4 | ||
|
|
b9ef7bd2eb | ||
|
|
1edb52594c | ||
|
|
a4fbf7b827 | ||
|
|
c122541d0b | ||
|
|
7c47ef9ebd | ||
|
|
e2e9087257 | ||
|
|
bb8d247e1a | ||
|
|
1ffa6dc3ad | ||
|
|
ee84cf5cb8 | ||
|
|
f95cd8710c | ||
|
|
61ee7954c6 | ||
|
|
cad8337f4e | ||
|
|
a4c8ae423e | ||
|
|
afa3c8c42b | ||
|
|
4db96194c3 | ||
|
|
2625dd350b | ||
|
|
f9e15a1be8 | ||
|
|
1c21f62d98 | ||
|
|
982fbca0f8 | ||
|
|
35cf70845a | ||
|
|
7cf65ded99 | ||
|
|
6ade4fc248 | ||
|
|
6e6e5a74f6 | ||
|
|
20980170aa | ||
|
|
02dfe76bef | ||
|
|
3548439624 | ||
|
|
9d3a8b144e | ||
|
|
14dc0be27c | ||
|
|
b5f58b2abc | ||
|
|
4e3dbecc19 | ||
|
|
2487ffc9aa | ||
|
|
118b7214ec | ||
|
|
d942048030 | ||
|
|
77cc1aee22 | ||
|
|
169ed2a5f2 | ||
|
|
9fffe05b88 | ||
|
|
5ce9ee0011 | ||
|
|
d577e74f98 | ||
|
|
7174288630 | ||
|
|
94a861f4b5 | ||
|
|
e9f61537d9 | ||
|
|
4cd05096c3 | ||
|
|
7768fe6bf0 | ||
|
|
7b290f7b85 | ||
|
|
949720bc7f | ||
|
|
d837751086 | ||
|
|
9869cbc19a | ||
|
|
03fe035094 | ||
|
|
887a942a15 | ||
|
|
d174a24c07 | ||
|
|
12c852e6ba | ||
|
|
180f8b4439 | ||
|
|
e3fb95a689 | ||
|
|
2af22b5b2d | ||
|
|
dd578776bb | ||
|
|
85aa0bf0c1 | ||
|
|
1f2cbfa7bb | ||
|
|
25b3ce6330 | ||
|
|
856099c958 | ||
|
|
e3017a763c | ||
|
|
3460fe1a9a | ||
|
|
884a30e33c | ||
|
|
f064c84ddb | ||
|
|
be4207d29e | ||
|
|
bdcda7e77f | ||
|
|
1e4d4ea9ff | ||
|
|
f0b64e0d53 | ||
|
|
38f3f10444 | ||
|
|
8bdd012be5 | ||
|
|
5f42d5af6c | ||
|
|
26ddfef6e1 | ||
|
|
ee4fa33003 | ||
|
|
420915557e | ||
|
|
9839fa9fb5 | ||
|
|
196fe5f098 | ||
|
|
49d8a3d9a8 | ||
|
|
d8db0a86d3 | ||
|
|
ed171c292b | ||
|
|
691a091485 | ||
|
|
9b71657bb2 | ||
|
|
caf0cab7a6 | ||
|
|
7c238111e6 | ||
|
|
3eca4c2715 | ||
|
|
aee2e918ee | ||
|
|
4ffb6d766c | ||
|
|
e779db7426 | ||
|
|
cb1ce8a914 | ||
|
|
cf7bbf8f13 | ||
|
|
2e657c127d | ||
|
|
a7e15805d2 | ||
|
|
2a578ce17f | ||
|
|
6f467a02b3 | ||
|
|
d2242d1901 | ||
|
|
5552ab3709 | ||
|
|
3e4fe09ab3 | ||
|
|
56f89d8124 | ||
|
|
3b9817b1bf | ||
|
|
cf841c25e2 | ||
|
|
864c48a352 | ||
|
|
64311bdf43 | ||
|
|
59604e6118 | ||
|
|
260f4a22de | ||
|
|
273d3782a2 | ||
|
|
ed0a39790e | ||
|
|
2d11fcb2c1 | ||
|
|
a714a3589c | ||
|
|
59ca521371 | ||
|
|
f3bcaebff5 | ||
|
|
10d8740fc2 | ||
|
|
f731690a1d | ||
|
|
77c28f02b3 | ||
|
|
00c695b84c | ||
|
|
2422c84f47 | ||
|
|
680950fd0f | ||
|
|
c9aa747934 | ||
|
|
97dc6041e9 | ||
|
|
c11f8674f8 | ||
|
|
023eb7875f | ||
|
|
315da29189 | ||
|
|
4cda7d7332 | ||
|
|
8114e5e81b | ||
|
|
7ee921e18a | ||
|
|
8c3a35e468 | ||
|
|
abac0ce7b0 | ||
|
|
03e245b158 | ||
|
|
aeb46491c7 | ||
|
|
dbb680fbd2 | ||
|
|
8de9ba90a2 | ||
|
|
118bd876d2 | ||
|
|
adcf5c892e | ||
|
|
0c326cbd51 | ||
|
|
8205eab75b | ||
|
|
ce0062aac7 | ||
|
|
8e09161b4b | ||
|
|
bf2778f5d6 | ||
|
|
223591d242 | ||
|
|
79fb68ebfe | ||
|
|
9f1f1fece2 | ||
|
|
6343f7c79b | ||
|
|
bfea2a2a76 | ||
|
|
8d65000ec9 | ||
|
|
4456fa154a | ||
|
|
8af341e206 | ||
|
|
6beca84284 | ||
|
|
6dfebec6dd | ||
|
|
756f81db94 | ||
|
|
8e854494e0 | ||
|
|
d70817ce85 | ||
|
|
49df72dfcf | ||
|
|
89ebbb66d2 | ||
|
|
b463f5b8ca | ||
|
|
417c97acee | ||
|
|
4beb0eca42 | ||
|
|
428055c3da | ||
|
|
20d812c7a3 | ||
|
|
d9a2b6b6fa | ||
|
|
606b0d67da | ||
|
|
67736b0f01 | ||
|
|
fc672aa0a3 | ||
|
|
79643936e1 | ||
|
|
663d8f7082 | ||
|
|
d8c8151c2b | ||
|
|
040c0c3cf0 | ||
|
|
902c4a5691 | ||
|
|
b3cf8231d5 | ||
|
|
bfebbf80cb | ||
|
|
a6cb892a36 | ||
|
|
bd99ba67dc | ||
|
|
1f78a33fdb | ||
|
|
182c9c7e92 | ||
|
|
1785c7bde0 | ||
|
|
43f9a7cdf9 | ||
|
|
0fa3b93f6e | ||
|
|
6f825bd8e7 | ||
|
|
e4d1d0eef8 | ||
|
|
162b8acf39 | ||
|
|
5db96a6748 | ||
|
|
1916dfcc44 | ||
|
|
f2caa9145e | ||
|
|
4b68ead320 | ||
|
|
6a540e2907 | ||
|
|
b0c1123987 | ||
|
|
b038954f55 | ||
|
|
606dd5e44e | ||
|
|
4e90b426ca | ||
|
|
e9305af4bc | ||
|
|
c69e1d6616 |
51
.agents/skills/dependabot-pr-rollup/SKILL.md
Normal file
51
.agents/skills/dependabot-pr-rollup/SKILL.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: dependabot-pr-rollup
|
||||||
|
description: Find open Dependabot PRs for the current GitHub repo, compare each PR head to its base branch, replay only the net dependency changes in a fresh worktree and branch, run npm validation, then commit, push, and open a pull request labeled dependencies. Use when you want to batch or manually replicate active Dependabot updates.
|
||||||
|
license: MIT
|
||||||
|
compatibility: Requires git, git worktree, gh CLI auth, npm, and a GitHub repo with an origin remote.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dependabot PR Rollup
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
Use this skill when the user wants to:
|
||||||
|
- find all open Dependabot PRs in the current repo
|
||||||
|
- reproduce their net effect in one local branch
|
||||||
|
- validate the result with the repo's standard npm checks
|
||||||
|
- commit and push the validated changes, then open a PR labeled `dependencies`
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Inspect the current checkout state, but do not reuse a dirty worktree.
|
||||||
|
2. List open Dependabot PRs with `gh pr list --state open --author app/dependabot`.
|
||||||
|
3. For each PR, collect the title, base branch, head branch, changed files, and relevant diffs.
|
||||||
|
4. Compare each PR head against `origin/<base>` instead of trusting the PR title. Dependabot PRs can already be partially merged, superseded by newer versions, or have no remaining net effect.
|
||||||
|
5. Create a new worktree and branch from `origin/<base>`.
|
||||||
|
6. Reproduce only the remaining dependency changes in the new worktree.
|
||||||
|
- Inspect `package.json` before editing.
|
||||||
|
- Run `npm ci --ignore-scripts` before applying updates.
|
||||||
|
- Use `npm install ... --ignore-scripts` for direct dependency changes so `package-lock.json` stays in sync.
|
||||||
|
- When updating `@biomejs/biome`, also update the Biome schema URL version in `biome.json` to match the installed Biome version.
|
||||||
|
7. Run `npm run all`.
|
||||||
|
8. Commit the changed source, lockfile, and generated artifacts, then push the branch.
|
||||||
|
9. Always open a pull request for the rollup and add the `dependencies` label to it. Pass `--label dependencies` to `gh pr create`, or add the label immediately afterward with `gh pr edit --add-label dependencies`.
|
||||||
|
|
||||||
|
## Repo-specific notes
|
||||||
|
|
||||||
|
- Use `gh` for GitHub operations.
|
||||||
|
- Keep the user's original checkout untouched by working in a separate worktree.
|
||||||
|
- In this repo, `npm run all` is the safest validation command because it runs build, check, package, and test.
|
||||||
|
- If dependency changes affect bundled output, include the regenerated `dist/` files.
|
||||||
|
|
||||||
|
## Report back
|
||||||
|
|
||||||
|
Always report:
|
||||||
|
- open Dependabot PRs found
|
||||||
|
- which PRs required no net changes
|
||||||
|
- new branch name
|
||||||
|
- new worktree path
|
||||||
|
- files changed
|
||||||
|
- `npm run all` result
|
||||||
|
- commit SHA and PR URL
|
||||||
|
- confirmation that the PR has the `dependencies` label
|
||||||
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Check http://editorconfig.org for more information
|
||||||
|
# This is the main config file for this project:
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{rs,py,pyi}]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.snap]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = 100
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
dist/
|
|
||||||
lib/
|
|
||||||
node_modules/
|
|
||||||
jest.config.js
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": ["jest", "@typescript-eslint"],
|
|
||||||
"extends": ["plugin:github/recommended"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 9,
|
|
||||||
"sourceType": "module",
|
|
||||||
"project": "./tsconfig.json"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-shadow": "off",
|
|
||||||
"@typescript-eslint/no-shadow": ["error"],
|
|
||||||
"i18n-text/no-en": "off",
|
|
||||||
"eslint-comments/no-use": "off",
|
|
||||||
"import/no-namespace": "off",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
|
||||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
|
||||||
"@typescript-eslint/no-require-imports": "error",
|
|
||||||
"@typescript-eslint/array-type": "error",
|
|
||||||
"@typescript-eslint/await-thenable": "error",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "error",
|
|
||||||
"camelcase": "off",
|
|
||||||
"@typescript-eslint/consistent-type-assertions": "error",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
|
||||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
|
||||||
"@typescript-eslint/no-array-constructor": "error",
|
|
||||||
"@typescript-eslint/no-empty-interface": "error",
|
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
|
||||||
"@typescript-eslint/no-extraneous-class": "error",
|
|
||||||
"@typescript-eslint/no-for-in-array": "error",
|
|
||||||
"@typescript-eslint/no-inferrable-types": "error",
|
|
||||||
"@typescript-eslint/no-misused-new": "error",
|
|
||||||
"@typescript-eslint/no-namespace": "error",
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
|
||||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
|
||||||
"@typescript-eslint/no-useless-constructor": "error",
|
|
||||||
"@typescript-eslint/no-var-requires": "error",
|
|
||||||
"@typescript-eslint/prefer-for-of": "warn",
|
|
||||||
"@typescript-eslint/prefer-function-type": "warn",
|
|
||||||
"@typescript-eslint/prefer-includes": "error",
|
|
||||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
|
||||||
"@typescript-eslint/promise-function-async": "error",
|
|
||||||
"@typescript-eslint/require-array-sort-compare": "error",
|
|
||||||
"@typescript-eslint/restrict-plus-operands": "error",
|
|
||||||
"semi": "off",
|
|
||||||
"@typescript-eslint/semi": ["error", "never"],
|
|
||||||
"@typescript-eslint/type-annotation-spacing": "error",
|
|
||||||
"@typescript-eslint/unbound-method": "error"
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"es6": true,
|
|
||||||
"jest/globals": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
.git-blame-ignore-revs
Normal file
0
.git-blame-ignore-revs
Normal file
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,2 +1,3 @@
|
|||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
dist/** -diff linguist-generated=true
|
dist/** -diff linguist-generated=true
|
||||||
|
src/download/checksum/known-checksums.json linguist-generated=true
|
||||||
|
|||||||
17
.github/actionlint.yaml
vendored
Normal file
17
.github/actionlint.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
self-hosted-runner:
|
||||||
|
# Custom labels of self-hosted or large GitHub hosted runners
|
||||||
|
# so that actionlint knows that they are not a typo
|
||||||
|
labels:
|
||||||
|
- selfhosted-ubuntu-arm64
|
||||||
|
# Configuration variables in array of strings defined in your repository or
|
||||||
|
# organization. `null` means disabling configuration variables check.
|
||||||
|
# Empty array means no configuration variable is allowed.
|
||||||
|
config-variables: null
|
||||||
|
paths:
|
||||||
|
.github/workflows/**/*.{yml,yaml}:
|
||||||
|
ignore:
|
||||||
|
- 'specifying action "\$/" in invalid format because ref is missing'
|
||||||
|
- 'reusable workflow call "\$/.+" at "uses" is not following the format'
|
||||||
|
.github/workflows/test.yml:
|
||||||
|
ignore:
|
||||||
|
- 'invalid runner name.+'
|
||||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -4,8 +4,12 @@ updates:
|
|||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
|
cooldown:
|
||||||
|
default-days: 30
|
||||||
|
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
|
cooldown:
|
||||||
|
default-days: 30
|
||||||
|
|||||||
30
.github/python.json
vendored
30
.github/python.json
vendored
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"problemMatcher": [
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "python",
|
||||||
|
"pattern": [
|
||||||
{
|
{
|
||||||
"owner": "python",
|
"file": 1,
|
||||||
"pattern": [
|
"line": 2,
|
||||||
{
|
"regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$"
|
||||||
"regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$",
|
},
|
||||||
"file": 1,
|
{
|
||||||
"line": 2
|
"message": 2,
|
||||||
},
|
"regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$"
|
||||||
{
|
|
||||||
"regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$",
|
|
||||||
"message": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
24
.github/release-drafter.yml
vendored
24
.github/release-drafter.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name-template: 'v$RESOLVED_VERSION 🌈'
|
name-template: "v$RESOLVED_VERSION 🌈"
|
||||||
tag-template: 'v$RESOLVED_VERSION'
|
tag-template: "v$RESOLVED_VERSION"
|
||||||
categories:
|
categories:
|
||||||
- title: "🚨 Breaking changes"
|
- title: "🚨 Breaking changes"
|
||||||
labels:
|
labels:
|
||||||
@@ -19,30 +19,32 @@ categories:
|
|||||||
labels:
|
labels:
|
||||||
- "maintenance"
|
- "maintenance"
|
||||||
- "ci"
|
- "ci"
|
||||||
|
- "update-known-checksums"
|
||||||
- title: "📚 Documentation"
|
- title: "📚 Documentation"
|
||||||
labels:
|
labels:
|
||||||
- "documentation"
|
- "documentation"
|
||||||
- title: "⬆️ Dependency updates"
|
- title: "⬆️ Dependency updates"
|
||||||
labels:
|
labels:
|
||||||
- "dependencies"
|
- "dependencies"
|
||||||
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
||||||
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
||||||
version-resolver:
|
version-resolver:
|
||||||
major:
|
major:
|
||||||
labels:
|
labels:
|
||||||
- 'major'
|
- "major"
|
||||||
- 'breaking-change'
|
- "breaking-change"
|
||||||
minor:
|
minor:
|
||||||
labels:
|
labels:
|
||||||
- 'minor'
|
- "minor"
|
||||||
- 'new-feature'
|
- "new-feature"
|
||||||
- 'enhancement'
|
- "enhancement"
|
||||||
patch:
|
patch:
|
||||||
labels:
|
labels:
|
||||||
- 'patch'
|
- "patch"
|
||||||
- 'bugfix'
|
- "bugfix"
|
||||||
|
- "default-version-update"
|
||||||
default: patch
|
default: patch
|
||||||
template: |
|
template: |
|
||||||
## Changes
|
## Changes
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
|
|||||||
35
.github/scripts/check-all-tests-passed-needs.ts
vendored
Normal file
35
.github/scripts/check-all-tests-passed-needs.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as yaml from "js-yaml";
|
||||||
|
|
||||||
|
interface WorkflowJob {
|
||||||
|
needs?: string[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Workflow {
|
||||||
|
jobs: Record<string, WorkflowJob>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflow = yaml.load(
|
||||||
|
fs.readFileSync("../workflows/test.yml", "utf8"),
|
||||||
|
) as Workflow;
|
||||||
|
const jobs = Object.keys(workflow.jobs);
|
||||||
|
const allTestsPassed = workflow.jobs["all-tests-passed"];
|
||||||
|
const needs: string[] = allTestsPassed.needs || [];
|
||||||
|
|
||||||
|
const expectedNeeds = jobs.filter((j) => j !== "all-tests-passed");
|
||||||
|
const missing = expectedNeeds.filter((j) => !needs.includes(j));
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.error(
|
||||||
|
`Missing jobs in all-tests-passed needs: ${missing.join(", ")}`,
|
||||||
|
);
|
||||||
|
console.info(
|
||||||
|
"Please add the missing jobs to the needs section of all-tests-passed in test.yml.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"All jobs in test.yml are in the needs section of all-tests-passed.",
|
||||||
|
);
|
||||||
9
.github/scripts/tsconfig.json
vendored
Normal file
9
.github/scripts/tsconfig.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"target": "es2022",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["check-all-tests-passed-needs.ts"]
|
||||||
|
}
|
||||||
53
.github/workflows/check-dist.yml
vendored
53
.github/workflows/check-dist.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
# `dist/index.js` is a special file in Actions.
|
|
||||||
# When you reference an action with `uses:` in a workflow,
|
|
||||||
# `index.js` is the code that will run.
|
|
||||||
# For our project, we generate this file through a build process from other source files.
|
|
||||||
# We need to make sure the checked-in `index.js` actually matches what we expect it to be.
|
|
||||||
name: Check dist/
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths-ignore:
|
|
||||||
- '**.md'
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- '**.md'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-dist:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Node.js 20
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Rebuild the dist/ directory
|
|
||||||
run: |
|
|
||||||
npm run build
|
|
||||||
npm run package
|
|
||||||
|
|
||||||
- name: Compare the expected and actual dist/ directories
|
|
||||||
run: |
|
|
||||||
if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then
|
|
||||||
echo "Detected uncommitted changes after build. See status below:"
|
|
||||||
git diff --text -v
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
id: diff
|
|
||||||
|
|
||||||
# If index.js was different than expected, upload the expected version as an artifact
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
|
||||||
with:
|
|
||||||
name: dist
|
|
||||||
path: dist/
|
|
||||||
67
.github/workflows/codeql-analysis.yml
vendored
67
.github/workflows/codeql-analysis.yml
vendored
@@ -12,13 +12,16 @@
|
|||||||
name: "CodeQL"
|
name: "CodeQL"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ main ]
|
branches:
|
||||||
schedule:
|
- main
|
||||||
- cron: '31 7 * * 3'
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
@@ -32,40 +35,42 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'TypeScript' ]
|
language: ["TypeScript"]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
# Learn more about CodeQL language support at https://git.io/codeql-language-support
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
source-root: src
|
source-root: src
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
# By default, queries listed here will override any specified in a config file.
|
# By default, queries listed here will override any specified in a config file.
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
# and modify them (or add more) to build your code if your project
|
# and modify them (or add more) to build your code if your project
|
||||||
# uses a compiled language
|
# uses a compiled language
|
||||||
|
|
||||||
#- run: |
|
#- run: |
|
||||||
# make bootstrap
|
# make bootstrap
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||||
|
|||||||
15
.github/workflows/release-drafter.yml
vendored
15
.github/workflows/release-drafter.yml
vendored
@@ -3,17 +3,24 @@ name: Release Drafter
|
|||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
# yamllint disable-line rule:truthy
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update_release_draft:
|
update_release_draft:
|
||||||
name: ✏️ Draft release
|
name: ✏️ Draft release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04-arm
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: read
|
||||||
steps:
|
steps:
|
||||||
- name: 🚀 Run Release Drafter
|
- name: 🚀 Run Release Drafter
|
||||||
uses: release-drafter/release-drafter@v6.0.0
|
uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0
|
||||||
|
with:
|
||||||
|
commitish: ${{ github.sha }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
124
.github/workflows/release.yml
vendored
Normal file
124
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Release version (e.g., 8.1.0)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-release:
|
||||||
|
name: Validate release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Validate version and draft release
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
TAG: v${{ inputs.version }}
|
||||||
|
run: |
|
||||||
|
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
|
||||||
|
echo "::error::Version must match MAJOR.MINOR.PATCH (e.g., 8.1.0)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_JSON=$(gh release view "$TAG" --json isDraft,targetCommitish 2>&1) || {
|
||||||
|
echo "::error::No release found for $TAG"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
IS_DRAFT=$(echo "$RELEASE_JSON" | jq -r '.isDraft')
|
||||||
|
TARGET=$(echo "$RELEASE_JSON" | jq -r '.targetCommitish')
|
||||||
|
|
||||||
|
if [[ "$IS_DRAFT" != "true" ]]; then
|
||||||
|
echo "::error::Release $TAG already exists and is not a draft"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TARGET" != "$GITHUB_SHA" ]]; then
|
||||||
|
echo "::error::Draft release target ($TARGET) does not match current commit ($GITHUB_SHA)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
release-gate:
|
||||||
|
# N.B. This name should not change, it is used for downstream checks.
|
||||||
|
name: release-gate
|
||||||
|
needs:
|
||||||
|
- validate-release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: release-gate
|
||||||
|
steps:
|
||||||
|
- run: echo "Release approved"
|
||||||
|
|
||||||
|
create-deployment:
|
||||||
|
name: create-deployment
|
||||||
|
needs:
|
||||||
|
- validate-release
|
||||||
|
- release-gate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: release
|
||||||
|
steps:
|
||||||
|
- run: echo "Release deployment created"
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
needs:
|
||||||
|
- validate-release
|
||||||
|
- release-gate
|
||||||
|
- create-deployment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Publish release
|
||||||
|
env:
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
TAG: v${{ inputs.version }}
|
||||||
|
run: |
|
||||||
|
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
|
||||||
|
echo "::error::Version must match MAJOR.MINOR.PATCH (e.g., 8.1.0)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RELEASE_JSON=$(gh release view "$TAG" --json isDraft,targetCommitish 2>&1) || {
|
||||||
|
echo "::error::No release found for $TAG"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
IS_DRAFT=$(echo "$RELEASE_JSON" | jq -r '.isDraft')
|
||||||
|
TARGET=$(echo "$RELEASE_JSON" | jq -r '.targetCommitish')
|
||||||
|
|
||||||
|
if [[ "$IS_DRAFT" != "true" ]]; then
|
||||||
|
echo "::error::Release $TAG already exists and is not a draft"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$TARGET" != "$GITHUB_SHA" ]]; then
|
||||||
|
echo "::error::Draft release target ($TARGET) does not match current commit ($GITHUB_SHA)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Publishing draft release $TAG"
|
||||||
|
gh release edit "$TAG" --draft=false
|
||||||
|
|
||||||
|
update-docs:
|
||||||
|
name: Update docs
|
||||||
|
needs:
|
||||||
|
- release
|
||||||
|
uses: $/.github/workflows/update-docs.yml
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
with:
|
||||||
|
tag: v${{ inputs.version }}
|
||||||
49
.github/workflows/test-cache-windows.yml
vendored
49
.github/workflows/test-cache-windows.yml
vendored
@@ -1,49 +0,0 @@
|
|||||||
name: 'test-cache-windows'
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-setup-cache:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [windows-latest]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup with cache
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
- run: uv sync
|
|
||||||
working-directory: __tests__\fixtures\uv-project
|
|
||||||
test-restore-cache:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [windows-latest]
|
|
||||||
needs: test-setup-cache
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Restore with cache
|
|
||||||
id: restore
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
- name: Cache was hit
|
|
||||||
run: |
|
|
||||||
if ($env:CACHE_HIT -ne "true") {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
env:
|
|
||||||
CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
|
|
||||||
- run: uv sync
|
|
||||||
working-directory: __tests__\fixtures\uv-project
|
|
||||||
83
.github/workflows/test-cache.yml
vendored
83
.github/workflows/test-cache.yml
vendored
@@ -1,83 +0,0 @@
|
|||||||
name: 'test-cache'
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-setup-cache:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest, macos-14]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup with cache
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
- run: uv sync
|
|
||||||
working-directory: __tests__/fixtures/uv-project
|
|
||||||
test-restore-cache:
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest, macos-14]
|
|
||||||
needs: test-setup-cache
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Restore with cache
|
|
||||||
id: restore
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
- name: Cache was hit
|
|
||||||
run: |
|
|
||||||
if [ "$CACHE_HIT" != "true" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
|
|
||||||
- run: uv sync
|
|
||||||
working-directory: __tests__/fixtures/uv-project
|
|
||||||
|
|
||||||
test-setup-cache-local:
|
|
||||||
runs-on: oracle-aarch64
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup with cache
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
cache-local-path: /tmp/uv-cache
|
|
||||||
- run: uv sync
|
|
||||||
working-directory: __tests__/fixtures/uv-project
|
|
||||||
test-restore-cache-local:
|
|
||||||
runs-on: oracle-aarch64
|
|
||||||
needs: test-setup-cache-local
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Restore with cache
|
|
||||||
id: restore
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}
|
|
||||||
cache-local-path: /tmp/uv-cache
|
|
||||||
- name: Cache was hit
|
|
||||||
run: |
|
|
||||||
if [ "$CACHE_HIT" != "true" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
CACHE_HIT: ${{ steps.restore.outputs.cache-hit }}
|
|
||||||
- run: uv sync
|
|
||||||
working-directory: __tests__/fixtures/uv-project
|
|
||||||
29
.github/workflows/test-windows.yml
vendored
29
.github/workflows/test-windows.yml
vendored
@@ -1,29 +0,0 @@
|
|||||||
name: 'test-windows'
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-default-version:
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Should not be on path
|
|
||||||
run: |
|
|
||||||
if (!(Get-Command -Name "uv" -ErrorAction SilentlyContinue)) {
|
|
||||||
exit 0
|
|
||||||
} else {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
- name: Setup uv
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- run: uv sync
|
|
||||||
working-directory: __tests__\fixtures\uv-project
|
|
||||||
1177
.github/workflows/test.yml
vendored
1177
.github/workflows/test.yml
vendored
File diff suppressed because it is too large
Load Diff
26
.github/workflows/update-default-version.yml
vendored
26
.github/workflows/update-default-version.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: 'Update default version and checksums'
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
- name: Update default version and checksums
|
|
||||||
id: update-default-version
|
|
||||||
run: node dist/update-default-version/index.js src/download/checksum/known-checksums.ts action.yml ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- run: npm install && npm run all
|
|
||||||
- name: Create Pull Request
|
|
||||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
|
||||||
with:
|
|
||||||
commit-message: "chore: update checksums"
|
|
||||||
title: "chore: update default version to ${{ steps.update-default-version.outputs.latest-version }}"
|
|
||||||
body: "chore: update default version to ${{ steps.update-default-version.outputs.latest-version }}"
|
|
||||||
base: main
|
|
||||||
labels: "automated-pr,bugfix"
|
|
||||||
branch: update-default-version-pr
|
|
||||||
delete-branch: true
|
|
||||||
75
.github/workflows/update-docs.yml
vendored
Normal file
75
.github/workflows/update-docs.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
name: "Update docs"
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Release tag to update docs for (e.g., v8.2.0)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-docs:
|
||||||
|
continue-on-error: ${{ github.event_name == 'workflow_call' }}
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: true
|
||||||
|
- name: Get tag info
|
||||||
|
id: tag-info
|
||||||
|
run: |
|
||||||
|
if [ -n "$INPUT_TAG" ]; then
|
||||||
|
TAG_NAME="$INPUT_TAG"
|
||||||
|
else
|
||||||
|
TAG_NAME=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "::error::Tag must match vMAJOR.MINOR.PATCH (e.g., v8.2.0)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMMIT_SHA=$(git rev-list -n 1 "$TAG_NAME")
|
||||||
|
echo "tag=$TAG_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "sha=$COMMIT_SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
env:
|
||||||
|
INPUT_TAG: ${{ inputs.tag }}
|
||||||
|
- name: Update references in docs
|
||||||
|
run: |
|
||||||
|
OLD_REF=$(grep -oh 'astral-sh/setup-uv@[a-f0-9]\{40\} # v[0-9][^ ]*' README.md docs/*.md | head -1)
|
||||||
|
OLD_SHA=$(echo "$OLD_REF" | sed 's/astral-sh\/setup-uv@\([a-f0-9]*\) # .*/\1/')
|
||||||
|
OLD_VERSION=$(echo "$OLD_REF" | sed 's/astral-sh\/setup-uv@[a-f0-9]* # \(v[^ ]*\)/\1/')
|
||||||
|
echo "Replacing $OLD_SHA # $OLD_VERSION with $NEW_SHA # $NEW_VERSION"
|
||||||
|
find README.md docs/ -type f \( -name "*.md" \) -exec \
|
||||||
|
sed -i "s|$OLD_SHA # $OLD_VERSION|$NEW_SHA # $NEW_VERSION|g" {} +
|
||||||
|
env:
|
||||||
|
NEW_SHA: ${{ steps.tag-info.outputs.sha }}
|
||||||
|
NEW_VERSION: ${{ steps.tag-info.outputs.tag }}
|
||||||
|
- name: Check for changes
|
||||||
|
id: changes-exist
|
||||||
|
run: |
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "changes-exist=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changes-exist=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
- name: Create Pull Request
|
||||||
|
if: ${{ steps.changes-exist.outputs.changes-exist == 'true' }}
|
||||||
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
|
with:
|
||||||
|
commit-message: "docs: update version references to ${{ steps.tag-info.outputs.tag }}"
|
||||||
|
title: "docs: update version references to ${{ steps.tag-info.outputs.tag }}"
|
||||||
|
body: |
|
||||||
|
Update `uses: astral-sh/setup-uv@...` references in documentation to
|
||||||
|
`${{ steps.tag-info.outputs.sha }} # ${{ steps.tag-info.outputs.tag }}`.
|
||||||
|
base: main
|
||||||
|
labels: "automated-pr,documentation"
|
||||||
|
branch: update-docs-${{ steps.tag-info.outputs.tag }}
|
||||||
|
delete-branch: true
|
||||||
56
.github/workflows/update-known-checksums.yml
vendored
Normal file
56
.github/workflows/update-known-checksums.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: "Update known checksums"
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 4 * * *" # Run every day at 4am UTC
|
||||||
|
repository_dispatch:
|
||||||
|
types: [ pypi_release ]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: true
|
||||||
|
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
cache: npm
|
||||||
|
- name: Update known checksums
|
||||||
|
id: update-known-checksums
|
||||||
|
run:
|
||||||
|
node dist/update-known-checksums/index.cjs
|
||||||
|
src/download/checksum/known-checksums.json
|
||||||
|
- name: Check for changes
|
||||||
|
id: changes-exist
|
||||||
|
run: |
|
||||||
|
git status --porcelain
|
||||||
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
echo "changes-exist=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changes-exist=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
- name: Compile changes
|
||||||
|
if: ${{ steps.changes-exist.outputs.changes-exist == 'true' }}
|
||||||
|
run: npm ci --ignore-scripts && npm run all
|
||||||
|
- name: Create Pull Request
|
||||||
|
if: ${{ steps.changes-exist.outputs.changes-exist == 'true' }}
|
||||||
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
|
with:
|
||||||
|
commit-message: "chore: update known checksums"
|
||||||
|
title:
|
||||||
|
"chore: update known checksums for ${{
|
||||||
|
steps.update-known-checksums.outputs.latest-version }}"
|
||||||
|
body:
|
||||||
|
"chore: update known checksums for ${{
|
||||||
|
steps.update-known-checksums.outputs.latest-version }}"
|
||||||
|
base: main
|
||||||
|
labels: "automated-pr,update-known-checksums"
|
||||||
|
branch: update-known-checksums-pr
|
||||||
|
delete-branch: true
|
||||||
19
.github/workflows/update-major-minor-tags.yml
vendored
19
.github/workflows/update-major-minor-tags.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
---
|
|
||||||
name: Update Major Minor Tags
|
|
||||||
|
|
||||||
# yamllint disable-line rule:truthy
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches-ignore:
|
|
||||||
- '**'
|
|
||||||
tags:
|
|
||||||
- 'v*.*.*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update_major_minor_tags:
|
|
||||||
name: Make sure major and minor tags are up to date on a patch release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Run Update semver
|
|
||||||
uses: haya14busa/action-update-semver@v1.2.1
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -96,4 +96,10 @@ Thumbs.db
|
|||||||
|
|
||||||
# Ignore built ts files
|
# Ignore built ts files
|
||||||
__tests__/runner/*
|
__tests__/runner/*
|
||||||
lib/**/*
|
lib/**/*
|
||||||
|
|
||||||
|
# Idea IDEs (PyCharm, WebStorm, IntelliJ, etc)
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Compiled scripts
|
||||||
|
.github/scripts/*.js
|
||||||
|
|||||||
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
engine-strict = true
|
||||||
|
ignore-scripts = true
|
||||||
|
min-release-age = 7
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
dist/
|
|
||||||
lib/
|
|
||||||
node_modules/
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 80,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"bracketSpacing": false,
|
|
||||||
"arrowParens": "avoid"
|
|
||||||
}
|
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["biomejs.biome"]
|
||||||
|
}
|
||||||
16
.vscode/settings.json
vendored
Normal file
16
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.action.useSortedAttributes.biome": "explicit",
|
||||||
|
"source.action.useSortedKeys.biome": "explicit",
|
||||||
|
"source.fixAll.biome": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"explorer.excludeGitIgnore": false,
|
||||||
|
"search.defaultViewMode": "list",
|
||||||
|
"search.exclude": {
|
||||||
|
"**/node_modules": true
|
||||||
|
},
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
18
AGENTS.md
Normal file
18
AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# setup-uv agent notes
|
||||||
|
|
||||||
|
This repository is a TypeScript-based GitHub Action for installing `uv` in GitHub Actions workflows. It also supports restoring/saving the `uv` cache and optional managed-Python caching.
|
||||||
|
|
||||||
|
- The published action runs the committed bundles in `dist/`, not the TypeScript in `src/`. After any code change, run `npm run package` and commit the resulting `dist/` updates.
|
||||||
|
- Standard local validation is:
|
||||||
|
1. `npm ci --ignore-scripts`
|
||||||
|
2. `npm run all`
|
||||||
|
- `npm run check` uses Biome (not ESLint/Prettier) and rewrites files in place.
|
||||||
|
- User-facing changes are usually multi-file changes. If you add or change inputs, outputs, or behavior, update `action.yml`, `action-types.yml`, the implementation in `src/`, tests in `__tests__/`, relevant docs/README, and then re-package.
|
||||||
|
- The easiest areas to regress are version resolution and caching. When touching them, add or update tests for precedence, cache invalidation, and cross-platform path behavior.
|
||||||
|
- Workflow edits have extra CI-only checks (`actionlint` and `zizmor`); `npm run all` does not cover them.
|
||||||
|
- Source is authored with bundler-friendly TypeScript, but published action artifacts in `dist/` are bundled as CommonJS for maximum GitHub Actions runtime compatibility with `@actions/*` dependencies.
|
||||||
|
- Keep these concerns separate when changing module formats:
|
||||||
|
- `src/` and tests may use modern ESM-friendly TypeScript patterns.
|
||||||
|
- `dist/` should prioritize runtime reliability over format purity.
|
||||||
|
- Do not switch published bundles to ESM without validating the actual committed artifacts under the target Node runtime.
|
||||||
|
- Before finishing, make sure validation does not leave generated or formatting-only diffs behind.
|
||||||
429
README.md
429
README.md
@@ -2,188 +2,383 @@
|
|||||||
|
|
||||||
Set up your GitHub Actions workflow with a specific version of [uv](https://docs.astral.sh/uv/).
|
Set up your GitHub Actions workflow with a specific version of [uv](https://docs.astral.sh/uv/).
|
||||||
|
|
||||||
* Install a version of uv and add it to the path
|
- Install a version of uv and add it to PATH
|
||||||
* Cache the installed version of uv to speed up consecutive runs on self-hosted runners
|
- Cache the installed version of uv to speed up consecutive runs on self-hosted runners
|
||||||
* Register problem matchers for error output
|
- Register problem matchers for error output
|
||||||
* Optional: Cache the uv cache
|
- (Optional) Persist the uv's cache in the GitHub Actions Cache
|
||||||
* Optional: Verify the checksum of the downloaded uv executable
|
- (Optional) Verify the checksum of the downloaded uv executable
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
* [Usage](#usage)
|
- [Usage](#usage)
|
||||||
* [Install specific version](#install-specific-version)
|
- [Install a required-version or latest (default)](#install-a-required-version-or-latest-default)
|
||||||
* [Install latest version](#install-latest-version)
|
- [Inputs](#inputs)
|
||||||
* [Validate checksum](#validate-checksum)
|
- [Outputs](#outputs)
|
||||||
* [Enable Caching](#enable-caching)
|
- [Python version](#python-version)
|
||||||
* [Local cache path](#local-cache-path)
|
- [Python architecture](#python-architecture)
|
||||||
* [Cache dependency glob](#cache-dependency-glob)
|
- [Working directory](#working-directory)
|
||||||
* [API rate limit](#api-rate-limit)
|
- [Advanced Configuration](#advanced-configuration)
|
||||||
* [How it works](#how-it-works)
|
- [How it works](#how-it-works)
|
||||||
* [FAQ](#faq)
|
- [FAQ](#faq)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Example workflow in a real world project can be found [here](https://github.com/eifinger/hass-weenect/blob/main/.github/workflows/ci.yml)
|
### Install a required-version or latest (default)
|
||||||
|
|
||||||
### Install specific version
|
|
||||||
|
|
||||||
You can also specify a specific version of uv
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Install a specific version
|
- name: Install the latest version of uv
|
||||||
uses: eifinger/setup-uv@v1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
If you do not specify a version, this action will look for a [required-version](https://docs.astral.sh/uv/reference/settings/#required-version)
|
||||||
|
in a `uv.toml` or `pyproject.toml` file in the repository root. If none is found, the latest version will be installed.
|
||||||
|
|
||||||
|
For an example workflow, see
|
||||||
|
[here](https://github.com/charliermarsh/autobot/blob/e42c66659bf97b90ca9ff305a19cc99952d0d43f/.github/workflows/ci.yaml).
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
|
||||||
|
All inputs and their defaults.
|
||||||
|
Have a look under [Advanced Configuration](#advanced-configuration) for detailed documentation on most of them.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Install uv with all available options
|
||||||
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
with:
|
with:
|
||||||
version: '0.3.0'
|
# The version of uv to install, e.g., "0.5.0", "latest", or "latest-known" (default: searches for version in config files, then latest)
|
||||||
|
version: ""
|
||||||
|
|
||||||
|
# Path to a file containing the version of uv to install, e.g., uv.toml, pyproject.toml, .tool-versions, requirements.txt or uv.lock. A selected .tool-versions file can also provide the Python version (default: searches uv.toml then pyproject.toml)
|
||||||
|
version-file: ""
|
||||||
|
|
||||||
|
# Resolution strategy when resolving version ranges: 'highest' or 'lowest'
|
||||||
|
resolution-strategy: "highest"
|
||||||
|
|
||||||
|
# The version of Python to set UV_PYTHON to (overrides the Python version from .tool-versions)
|
||||||
|
python-version: ""
|
||||||
|
|
||||||
|
# The Python architecture to set UV_PYTHON_ARCH to, e.g., x86_64 or aarch64
|
||||||
|
python-arch: ""
|
||||||
|
|
||||||
|
# Use uv venv to activate a venv ready to be used by later steps
|
||||||
|
activate-environment: "false"
|
||||||
|
|
||||||
|
# Custom path for the virtual environment when using activate-environment (default: .venv in the working directory)
|
||||||
|
venv-path: ""
|
||||||
|
|
||||||
|
# Pass --no-project when creating the venv with activate-environment.
|
||||||
|
no-project: "false"
|
||||||
|
|
||||||
|
# The directory to execute all commands in and look for files such as pyproject.toml
|
||||||
|
working-directory: ""
|
||||||
|
|
||||||
|
# The checksum of the uv version to install
|
||||||
|
checksum: ""
|
||||||
|
|
||||||
|
# Used when downloading uv from GitHub releases
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
|
||||||
|
# Enable the GitHub Actions cache for uv: true, false, or auto (enabled on GitHub-hosted runners except for release, tag push, pull_request_target, and workflow_run events; disabled on self-hosted runners)
|
||||||
|
enable-cache: "auto"
|
||||||
|
|
||||||
|
# Glob pattern to match files relative to the repository root to control the cache
|
||||||
|
cache-dependency-glob: |
|
||||||
|
**/*requirements*.txt
|
||||||
|
**/*requirements*.in
|
||||||
|
**/*constraints*.txt
|
||||||
|
**/*constraints*.in
|
||||||
|
**/pyproject.toml
|
||||||
|
**/uv.lock
|
||||||
|
**/*.py.lock
|
||||||
|
|
||||||
|
# Whether to restore the cache if found
|
||||||
|
restore-cache: "true"
|
||||||
|
|
||||||
|
# Whether to save the cache after the run: true, false, or auto (disabled for merge_group events)
|
||||||
|
save-cache: "auto"
|
||||||
|
|
||||||
|
# Suffix for the cache key
|
||||||
|
cache-suffix: ""
|
||||||
|
|
||||||
|
# Local path to store the cache (default: "" - uses system temp directory)
|
||||||
|
cache-local-path: ""
|
||||||
|
|
||||||
|
# Prune cache before saving
|
||||||
|
prune-cache: "false"
|
||||||
|
|
||||||
|
# Upload managed Python installations to the GitHub Actions cache
|
||||||
|
cache-python: "false"
|
||||||
|
|
||||||
|
# Ignore when nothing is found to cache
|
||||||
|
ignore-nothing-to-cache: "false"
|
||||||
|
|
||||||
|
# Ignore when the working directory is empty
|
||||||
|
ignore-empty-workdir: "false"
|
||||||
|
|
||||||
|
# Custom path to set UV_TOOL_DIR to
|
||||||
|
tool-dir: ""
|
||||||
|
|
||||||
|
# Custom path to set UV_TOOL_BIN_DIR to
|
||||||
|
tool-bin-dir: ""
|
||||||
|
|
||||||
|
# URL to a custom manifest file in the astral-sh/versions format
|
||||||
|
manifest-file: ""
|
||||||
|
|
||||||
|
# Download uv from the Astral mirror instead of directly from GitHub Releases
|
||||||
|
download-from-astral-mirror: "true"
|
||||||
|
|
||||||
|
# Add problem matchers
|
||||||
|
add-problem-matchers: "true"
|
||||||
|
|
||||||
|
# Suppress info-level log output. Only warnings and errors are shown
|
||||||
|
quiet: "false"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install latest version
|
### Outputs
|
||||||
|
|
||||||
By default this action installs the version defined as `default` in `action.yml`.
|
- `uv-version`: The installed uv version. Useful when using latest.
|
||||||
This gets automatically updated in a new release of this action when a new version of uv is released.
|
- `uv-path`: The path to the installed uv binary.
|
||||||
If you don't want to wait for a new release of this action you can use use `version: latest`.
|
- `uvx-path`: The path to the installed uvx binary.
|
||||||
|
- `cache-hit`: A boolean value to indicate a cache entry was found.
|
||||||
|
- `venv`: Path to the activated venv if activate-environment is true.
|
||||||
|
- `python-version`: The Python version that was set.
|
||||||
|
- `python-runtime-id`: An opaque identifier reported by uv for the activated venv's Python runtime. Empty when `activate-environment` is false.
|
||||||
|
- `python-cache-hit`: A boolean value to indicate the Python cache entry was found.
|
||||||
|
|
||||||
> [!WARNING]
|
### Python version
|
||||||
> Using the `latest` version means that the uv executable gets downloaded every single time instead of loaded from the tools cache.
|
|
||||||
> This can take up to 20s depending on the download speed.
|
You can use the input `python-version` to set the environment variable `UV_PYTHON` for the rest of your workflow
|
||||||
> This does not affect the uv cache.
|
|
||||||
|
This will override any python version specifications in `pyproject.toml`, `.python-version`, and
|
||||||
|
an explicitly selected `.tool-versions` file.
|
||||||
|
|
||||||
|
When `version-file` points to `.tool-versions`, its `python` entry is used if neither
|
||||||
|
`python-version` nor `UV_PYTHON` is set:
|
||||||
|
|
||||||
|
```text
|
||||||
|
uv 0.12.3
|
||||||
|
python 3.13
|
||||||
|
```
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Install a specific version
|
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
uses: eifinger/setup-uv@v1
|
|
||||||
with:
|
with:
|
||||||
version: 'latest'
|
version-file: ".tool-versions"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Validate checksum
|
Only a single Python version is supported. Filesystem paths are not supported for uv or Python.
|
||||||
|
Multiple Python fallback versions and the asdf `ref:`, `path:`, and `system` forms are ignored with
|
||||||
You can also specify a checksum to validate the downloaded file.
|
a warning.
|
||||||
Checksums up to the default version are automatically verified by this action.
|
|
||||||
The sha265 hashes can be found on the [releases page](https://github.com/astral-sh/uv/releases)
|
|
||||||
of the uv repo.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Install a specific version and validate the checksum
|
- name: Install the latest version of uv and set the python version to 3.13t
|
||||||
uses: eifinger/setup-uv@v1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
with:
|
with:
|
||||||
version: '0.3.1'
|
python-version: 3.13t
|
||||||
checksum: 'e11b01402ab645392c7ad6044db63d37e4fd1e745e015306993b07695ea5f9f8'
|
- run: uv pip install --python=3.13t pip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Enable caching
|
You can combine this with a matrix to test multiple Python versions:
|
||||||
|
|
||||||
If you enable caching the [uv cache](https://docs.astral.sh/uv/concepts/cache/) will
|
|
||||||
be cached to the GitHub Actions Cache. This can speed up runs which can reuse the cache
|
|
||||||
by several minutes. The cache will always be reused on self-hosted runners.
|
|
||||||
|
|
||||||
You can optionally define a custom cache key suffix.
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Enable caching and define a custom cache key suffix
|
jobs:
|
||||||
id: setup-uv
|
test:
|
||||||
uses: eifinger/setup-uv@v1
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
- name: Install the latest version of uv and set the python version
|
||||||
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Test with python ${{ matrix.python-version }}
|
||||||
|
run: uv run --frozen pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python architecture
|
||||||
|
|
||||||
|
Use `python-arch` to set `UV_PYTHON_ARCH` for the rest of the job. This selects the Python
|
||||||
|
architecture independently of the version, including versions requested by `.python-version`.
|
||||||
|
An explicit architecture or interpreter path in a Python request takes precedence.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: astral-sh/setup-uv@main
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
python-version: "3.14"
|
||||||
cache-suffix: 'optional-suffix'
|
python-arch: x86_64
|
||||||
|
- run: uv run --frozen pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
When the cache was successfully restored the output `cache-hit` will be set to `true` and you can use it in subsequent steps.
|
The input overrides an existing `UV_PYTHON_ARCH` value. When it is omitted, the action respects
|
||||||
For the example above you can use it like this:
|
`UV_PYTHON_ARCH` from the environment. Both forms require a uv version that supports the variable;
|
||||||
|
the action reports an error if the installed version does not support it. The selected architecture
|
||||||
|
is included in the [cache key](docs/caching.md#cache-key).
|
||||||
|
|
||||||
|
### Working directory
|
||||||
|
|
||||||
|
You can set the working directory with the `working-directory` input.
|
||||||
|
This controls where we look for `pyproject.toml`, `uv.toml` and `.python-version` files
|
||||||
|
which are used to determine the version of uv and python to install.
|
||||||
|
|
||||||
|
It also controls where [the venv gets created](#activate-environment), unless `venv-path` is set.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Do something if the cache was restored
|
- name: Install uv based on the config files in the working-directory
|
||||||
if: steps.setup-uv.outputs.cache-hit == 'true'
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
run: echo "Cache was restored"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Local cache path
|
|
||||||
|
|
||||||
If you want to save the cache to a local path other than the default path (`/tmp/setup-uv-cache`)
|
|
||||||
you can specify the path with the `cache-local-path` input.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Define a custom uv cache path
|
|
||||||
uses: eifinger/setup-uv@v1
|
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
working-directory: my/subproject/dir
|
||||||
cache-local-path: '/path/to/cache'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Cache dependency glob
|
## Advanced Configuration
|
||||||
|
|
||||||
If you want to control when the cache is invalidated you can specify a glob pattern with the `cache-dependency-glob` input.
|
For more advanced configuration options, see our detailed documentation:
|
||||||
The cache will be invalidated if any file matching the glob pattern changes.
|
|
||||||
The glob matches files relative to the repository root.
|
|
||||||
|
|
||||||
```yaml
|
- **[Advanced Version Configuration](docs/advanced-version-configuration.md)** - Resolution strategies and version files
|
||||||
- name: Define a cache dependency glob
|
- **[Caching](docs/caching.md)** - Complete guide to caching configuration
|
||||||
uses: eifinger/setup-uv@v1
|
- **[Environment and Tools](docs/environment-and-tools.md)** - Environment activation, tool directories, authentication, and environment variables
|
||||||
with:
|
- **[Customization](docs/customization.md)** - Checksum validation, custom manifests, and problem matchers
|
||||||
enable-cache: true
|
|
||||||
cache-dependency-glob: 'uv.lock'
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Define a cache dependency glob
|
|
||||||
uses: eifinger/setup-uv@v1
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
cache-dependency-glob: '**requirements*.txt'
|
|
||||||
```
|
|
||||||
|
|
||||||
### API rate limit
|
|
||||||
|
|
||||||
To avoid hitting the error `API rate limit exceeded` you can supply a GitHub token with the `github-token` input.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Install uv and supply a GitHub token
|
|
||||||
uses: eifinger/setup-uv@v1
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
```
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
This action downloads uv from the releases of the [uv repo](https://github.com/astral-sh/uv) and uses the [GitHub Actions Toolkit](https://github.com/actions/toolkit) to cache it as a tool to speed up consecutive runs on self-hosted runners.
|
By default, this action resolves uv versions from the
|
||||||
|
[`astral-sh/versions`](https://github.com/astral-sh/versions) manifest and downloads uv from the
|
||||||
|
official [GitHub Releases](https://github.com/astral-sh/uv).
|
||||||
|
|
||||||
The installed version of uv is then added to the runner path so other steps can just use it by calling `uv`.
|
It then uses the [GitHub Actions Toolkit](https://github.com/actions/toolkit) to cache uv as a
|
||||||
|
tool to speed up consecutive runs on self-hosted runners.
|
||||||
|
|
||||||
|
The installed version of uv is then added to the runner PATH, enabling later steps to invoke it
|
||||||
|
by name (`uv`).
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
### Do I still need actions/setup-python when using this action?
|
### Do I still need `actions/setup-python` alongside `setup-uv`?
|
||||||
|
|
||||||
No! This action was modelled as a drop-in replacement for `actions/setup-python` when using uv.
|
With `setup-uv`, you can install a specific version of Python using `uv python install` rather than
|
||||||
|
relying on `actions/setup-python`.
|
||||||
|
|
||||||
A simple example workflow could look like this:
|
Using `actions/setup-python` can be faster (~1s), because GitHub includes several Python versions in the runner image
|
||||||
|
which are available to get activated by `actions/setup-python` without having to download them.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@main
|
uses: actions/checkout@main
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: eifinger/setup-uv@v1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
with:
|
with:
|
||||||
enable-cache: true
|
enable-cache: true
|
||||||
- name: Test
|
- name: Test
|
||||||
run: uv run --frozen pytest
|
run: uv run --frozen pytest # Uses the Python version automatically installed by uv
|
||||||
|
```
|
||||||
|
|
||||||
|
To install a specific version of Python, use
|
||||||
|
[`uv python install`](https://docs.astral.sh/uv/guides/install-python/):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Install the latest version of uv
|
||||||
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
- name: Install Python 3.12
|
||||||
|
run: uv python install 3.12
|
||||||
```
|
```
|
||||||
|
|
||||||
### What is the default version?
|
### What is the default version?
|
||||||
|
|
||||||
By default this action installs the version defined as `default` in `action.yml`.
|
By default, this action installs the latest version of uv.
|
||||||
When a new release of uv is published this triggers an automatic release of this action with the new version as `default`.
|
|
||||||
|
|
||||||
If you have to know the version installed for other steps of your workflow you can use the `uv-version` output:
|
If you require the installed version in subsequent steps of your workflow, use the `uv-version`
|
||||||
|
output:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@main
|
uses: actions/checkout@main
|
||||||
- name: Install the default version of uv
|
- name: Install the default version of uv
|
||||||
id: setup-uv
|
id: setup-uv
|
||||||
uses: eifinger/setup-uv@v1
|
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
|
||||||
- name: Print the installed version
|
- name: Print the installed version
|
||||||
run: echo "Installed uv version is ${{ steps.setup-uv.outputs.uv-version }}"
|
run: echo "Installed uv version is ${{ steps.setup-uv.outputs.uv-version }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### Should I include the resolution strategy in the cache key?
|
||||||
|
|
||||||
[<img src="https://raw.githubusercontent.com/eifinger/setup-uv/main/docs/images/bmc-button.svg" width=150 height=40 style="margin: 5px"/>](https://www.buymeacoffee.com/eifinger)
|
**Yes!**
|
||||||
[<img src="https://raw.githubusercontent.com/eifinger/setup-uv/main/docs/images/paypal-button.svg" width=150 height=40 style="margin: 5px"/>](https://paypal.me/kevinstillhammer)
|
|
||||||
|
The cache key gets computed by using the cache-dependency-glob (see [Caching documentation](docs/caching.md)).
|
||||||
|
|
||||||
|
If you have jobs which use the same dependency definitions from `requirements.txt` or
|
||||||
|
`pyproject.toml` but different
|
||||||
|
[resolution strategies](https://docs.astral.sh/uv/concepts/resolution/#resolution-strategy),
|
||||||
|
each job will have different dependencies or dependency versions.
|
||||||
|
But if you do not add the resolution strategy as a cache-suffix (see [Caching documentation](docs/caching.md)),
|
||||||
|
they will have the same cache key.
|
||||||
|
|
||||||
|
This means the first job which starts uploading its cache will win and all other job will fail
|
||||||
|
uploading the cache,
|
||||||
|
because they try to upload with the same cache key.
|
||||||
|
|
||||||
|
You might see errors like
|
||||||
|
`Failed to save: Failed to CreateCacheEntry: Received non-retryable error: Failed request: (409) Conflict: cache entry with the same key, version, and scope already exists`
|
||||||
|
|
||||||
|
### Why do I see warnings like `No GitHub Actions cache found for key`
|
||||||
|
|
||||||
|
When a workflow runs for the first time on a branch and has a new cache key, because the
|
||||||
|
cache-dependency-glob (see [Caching documentation](docs/caching.md)) found changed files (changed dependencies),
|
||||||
|
the cache will not be found and the warning `No GitHub Actions cache found for key` will be printed.
|
||||||
|
|
||||||
|
While this might be irritating at first, it is expected behaviour and the cache will be created
|
||||||
|
and reused in later workflows.
|
||||||
|
|
||||||
|
The reason for the warning is that we have to way to know if this is the first run of a new
|
||||||
|
cache key or the user accidentally misconfigured the cache-dependency-glob
|
||||||
|
or cache-suffix (see [Caching documentation](docs/caching.md)) and the cache never gets used.
|
||||||
|
|
||||||
|
### Do I have to run `actions/checkout` before or after `setup-uv`?
|
||||||
|
|
||||||
|
Some workflows need uv but do not need to access the repository content.
|
||||||
|
|
||||||
|
But **if** you need to access the repository content, you have run `actions/checkout` before running `setup-uv`.
|
||||||
|
Running `actions/checkout` after `setup-uv` **is not supported**.
|
||||||
|
|
||||||
|
### Does `setup-uv` also install my project or its dependencies automatically?
|
||||||
|
|
||||||
|
No, `setup-uv` alone won't install any libraries from your `pyproject.toml` or `requirements.txt`, it only sets up `uv`.
|
||||||
|
You should run `uv sync` or `uv pip install .` separately, or use `uv run ...` to ensure necessary dependencies are installed.
|
||||||
|
|
||||||
|
### Why is a changed cache not detected and not the full cache uploaded?
|
||||||
|
|
||||||
|
When `setup-uv` starts it has to know whether it is better to download an existing cache
|
||||||
|
or start fresh and download every dependency again.
|
||||||
|
It does this by using a combination of hashes calculated on the contents of e.g. `uv.lock`.
|
||||||
|
|
||||||
|
By calculating these hashes and combining them in a key `setup-uv` can check
|
||||||
|
if an uploaded cache exists for this key.
|
||||||
|
If yes (e.g. contents of `uv.lock` did not change since last run) the dependencies in the cache
|
||||||
|
are up to date and the cache will be downloaded and used.
|
||||||
|
|
||||||
|
Details on determining which files will lead to different caches can be read in the
|
||||||
|
[Caching documentation](docs/caching.md).
|
||||||
|
|
||||||
|
Some dependencies will never be uploaded to the cache and will be downloaded again on each run
|
||||||
|
as described in the [Caching documentation](docs/caching.md).
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
`setup-uv` was initially written and published by [Kevin Stillhammer](https://github.com/eifinger)
|
||||||
|
before moving under the official [Astral](https://github.com/astral-sh) GitHub organization. You can
|
||||||
|
support Kevin's work in open source on [Buy me a coffee](https://www.buymeacoffee.com/eifinger) or
|
||||||
|
[PayPal](https://paypal.me/kevinstillhammer).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a target="_blank" href="https://astral.sh" style="background:none">
|
||||||
|
<img src="https://raw.githubusercontent.com/astral-sh/uv/main/assets/svg/Astral.svg" alt="Made by Astral">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|||||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Security policy
|
||||||
|
|
||||||
|
Report suspected vulnerabilities according to [Astral's security policy](https://github.com/astral-sh/.github/blob/main/SECURITY.md).
|
||||||
|
|
||||||
|
For this repository's security boundaries and reporting criteria, see the [setup-uv threat model](docs/threat-model.md).
|
||||||
132
__tests__/cache/restore-cache.test.ts
vendored
Normal file
132
__tests__/cache/restore-cache.test.ts
vendored
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import {
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
jest,
|
||||||
|
} from "@jest/globals";
|
||||||
|
import { createSetupInputs } from "../helpers/setup-inputs";
|
||||||
|
|
||||||
|
const mockRestoreCache = jest.fn();
|
||||||
|
const mockSaveState = jest.fn();
|
||||||
|
const mockSetOutput = jest.fn();
|
||||||
|
const mockGetArch = jest.fn(() => "x86_64");
|
||||||
|
const mockGetOSNameVersion = jest.fn(() => "ubuntu-24.04");
|
||||||
|
const mockGetPlatform = jest.fn(async () => "unknown-linux-gnu");
|
||||||
|
const ORIGINAL_UV_PYTHON_ARCH = process.env.UV_PYTHON_ARCH;
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/cache", () => ({
|
||||||
|
restoreCache: mockRestoreCache,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
saveState: mockSaveState,
|
||||||
|
setOutput: mockSetOutput,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/hash/hash-files", () => ({
|
||||||
|
hashFiles: jest.fn(async () => "dependencyhash"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/utils/logging", () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/utils/platforms", () => ({
|
||||||
|
getArch: mockGetArch,
|
||||||
|
getOSNameVersion: mockGetOSNameVersion,
|
||||||
|
getPlatform: mockGetPlatform,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { restoreCache } = await import("../../src/cache/restore-cache");
|
||||||
|
|
||||||
|
function cacheKeyOutput(): string {
|
||||||
|
const call = mockSetOutput.mock.calls.find(([name]) => name === "cache-key");
|
||||||
|
expect(call).toBeDefined();
|
||||||
|
return call?.[1] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env.UV_PYTHON_ARCH;
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockGetArch.mockReturnValue("x86_64");
|
||||||
|
mockGetOSNameVersion.mockReturnValue("ubuntu-24.04");
|
||||||
|
mockGetPlatform.mockResolvedValue("unknown-linux-gnu");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (ORIGINAL_UV_PYTHON_ARCH === undefined) {
|
||||||
|
delete process.env.UV_PYTHON_ARCH;
|
||||||
|
} else {
|
||||||
|
process.env.UV_PYTHON_ARCH = ORIGINAL_UV_PYTHON_ARCH;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("restoreCache", () => {
|
||||||
|
it("encodes Python version ranges before adding them to the cache key", async () => {
|
||||||
|
await restoreCache(createSetupInputs(), ">3.10.11,<3.11");
|
||||||
|
|
||||||
|
const cacheKey = cacheKeyOutput();
|
||||||
|
|
||||||
|
expect(cacheKey).not.toContain(",");
|
||||||
|
expect(cacheKey).toContain("-%3E3.10.11%2C%3C3.11-");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("encodes cache suffixes before adding them to the cache key", async () => {
|
||||||
|
const inputs = createSetupInputs({ cacheSuffix: "tests-3.10,3.11" });
|
||||||
|
|
||||||
|
await restoreCache(inputs, "3.11");
|
||||||
|
|
||||||
|
const cacheKey = cacheKeyOutput();
|
||||||
|
|
||||||
|
expect(cacheKey).not.toContain(",");
|
||||||
|
expect(cacheKey).toContain("-tests-3.10%2C3.11");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses an unpruned cache key by default", async () => {
|
||||||
|
const inputs = createSetupInputs({ cacheSuffix: "tests-3.11" });
|
||||||
|
|
||||||
|
await restoreCache(inputs, "3.11");
|
||||||
|
|
||||||
|
expect(cacheKeyOutput()).toBe(
|
||||||
|
"setup-uv-2-x86_64-unknown-linux-gnu-ubuntu-24.04-3.11-dependencyhash-tests-3.11",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes an inherited Python architecture in the cache key", async () => {
|
||||||
|
process.env.UV_PYTHON_ARCH = "aarch64";
|
||||||
|
|
||||||
|
await restoreCache(createSetupInputs(), "3.14");
|
||||||
|
|
||||||
|
expect(cacheKeyOutput()).toContain("-3.14-python-aarch64-");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["aarch64", "x86_64"])(
|
||||||
|
"separates %s Python caches on a Windows ARM64 runner",
|
||||||
|
async (pythonArch) => {
|
||||||
|
mockGetArch.mockReturnValue("aarch64");
|
||||||
|
mockGetOSNameVersion.mockReturnValue("windows-11");
|
||||||
|
mockGetPlatform.mockResolvedValue("pc-windows-msvc");
|
||||||
|
const inputs = createSetupInputs({
|
||||||
|
cachePython: true,
|
||||||
|
pythonArch,
|
||||||
|
restoreCache: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await restoreCache(inputs, "3.14");
|
||||||
|
|
||||||
|
const cacheKey = `setup-uv-2-aarch64-pc-windows-msvc-windows-11-3.14-python-${pythonArch}-py-dependencyhash`;
|
||||||
|
expect(cacheKeyOutput()).toBe(cacheKey);
|
||||||
|
expect(mockRestoreCache).toHaveBeenCalledWith(
|
||||||
|
[inputs.cacheLocalPath?.path],
|
||||||
|
cacheKey,
|
||||||
|
);
|
||||||
|
expect(mockRestoreCache).toHaveBeenCalledWith(
|
||||||
|
[inputs.pythonDir],
|
||||||
|
`${cacheKey}-python`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import {expect, test, it} from '@jest/globals'
|
|
||||||
import {
|
|
||||||
isknownVersion,
|
|
||||||
validateChecksum
|
|
||||||
} from '../../../src/download/checksum/checksum'
|
|
||||||
|
|
||||||
test('checksum should match', async () => {
|
|
||||||
const validChecksum =
|
|
||||||
'f3da96ec7e995debee7f5d52ecd034dfb7074309a1da42f76429ecb814d813a3'
|
|
||||||
const filePath = '__tests__/fixtures/checksumfile'
|
|
||||||
// string params don't matter only test the checksum mechanism, not known checksums
|
|
||||||
await validateChecksum(
|
|
||||||
validChecksum,
|
|
||||||
filePath,
|
|
||||||
'aarch64',
|
|
||||||
'pc-windows-msvc',
|
|
||||||
'1.2.3'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
type KnownVersionFixture = {version: string; known: boolean}
|
|
||||||
|
|
||||||
it.each<KnownVersionFixture>([
|
|
||||||
{
|
|
||||||
version: '0.3.0',
|
|
||||||
known: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
version: '0.0.15',
|
|
||||||
known: false
|
|
||||||
}
|
|
||||||
])(
|
|
||||||
'isknownVersion should return $known for version $version',
|
|
||||||
({version, known}) => {
|
|
||||||
expect(isknownVersion(version)).toBe(known)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
88
__tests__/download/checksum/checksum.test.ts
Normal file
88
__tests__/download/checksum/checksum.test.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { expect, it, test } from "@jest/globals";
|
||||||
|
import {
|
||||||
|
isknownVersion,
|
||||||
|
validateChecksum,
|
||||||
|
} from "../../../src/download/checksum/checksum";
|
||||||
|
|
||||||
|
const validChecksum =
|
||||||
|
"f3da96ec7e995debee7f5d52ecd034dfb7074309a1da42f76429ecb814d813a3";
|
||||||
|
const filePath = "__tests__/fixtures/checksumfile";
|
||||||
|
|
||||||
|
test("checksum should match", async () => {
|
||||||
|
// string params don't matter only test the checksum mechanism, not known checksums
|
||||||
|
await validateChecksum(
|
||||||
|
validChecksum,
|
||||||
|
filePath,
|
||||||
|
"aarch64",
|
||||||
|
"pc-windows-msvc",
|
||||||
|
"1.2.3",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("provided checksum beats known checksums", async () => {
|
||||||
|
await validateChecksum(
|
||||||
|
validChecksum,
|
||||||
|
filePath,
|
||||||
|
"x86_64",
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"0.3.0",
|
||||||
|
"incorrect-manifest-checksum",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("known checksums beat manifest checksums", async () => {
|
||||||
|
await expect(
|
||||||
|
validateChecksum(
|
||||||
|
undefined,
|
||||||
|
filePath,
|
||||||
|
"x86_64",
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"0.3.0",
|
||||||
|
validChecksum,
|
||||||
|
),
|
||||||
|
).rejects.toThrow("did not match");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("manifest checksums are used when no known checksum exists", async () => {
|
||||||
|
await expect(
|
||||||
|
validateChecksum(
|
||||||
|
undefined,
|
||||||
|
filePath,
|
||||||
|
"aarch64",
|
||||||
|
"pc-windows-msvc",
|
||||||
|
"1.2.3",
|
||||||
|
"incorrect-manifest-checksum",
|
||||||
|
),
|
||||||
|
).rejects.toThrow("did not match");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty manifest checksums are rejected", async () => {
|
||||||
|
await expect(
|
||||||
|
validateChecksum(
|
||||||
|
undefined,
|
||||||
|
filePath,
|
||||||
|
"aarch64",
|
||||||
|
"pc-windows-msvc",
|
||||||
|
"1.2.3",
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
).rejects.toThrow("No checksum found");
|
||||||
|
});
|
||||||
|
|
||||||
|
type KnownVersionFixture = { version: string; known: boolean };
|
||||||
|
|
||||||
|
it.each<KnownVersionFixture>([
|
||||||
|
{
|
||||||
|
known: true,
|
||||||
|
version: "0.3.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
known: false,
|
||||||
|
version: "0.0.15",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"isknownVersion should return $known for version $version",
|
||||||
|
({ version, known }) => {
|
||||||
|
expect(isknownVersion(version)).toBe(known);
|
||||||
|
},
|
||||||
|
);
|
||||||
20
__tests__/download/checksum/known-version.test.ts
Normal file
20
__tests__/download/checksum/known-version.test.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { expect, it, jest } from "@jest/globals";
|
||||||
|
|
||||||
|
jest.unstable_mockModule(
|
||||||
|
"../../../src/download/checksum/known-checksums",
|
||||||
|
() => ({
|
||||||
|
KNOWN_CHECKSUMS: {
|
||||||
|
"aarch64-apple-darwin-1.9.0": "checksum",
|
||||||
|
"x86_64-unknown-linux-gnu-1.8.0": "checksum",
|
||||||
|
"x86_64-unknown-linux-gnu-1.10.0": "checksum",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getLatestKnownVersion } = await import(
|
||||||
|
"../../../src/download/checksum/known-version"
|
||||||
|
);
|
||||||
|
|
||||||
|
it("returns the highest version with a built-in checksum", () => {
|
||||||
|
expect(getLatestKnownVersion()).toBe("1.10.0");
|
||||||
|
});
|
||||||
27
__tests__/download/checksum/update-known-checksums.test.ts
Normal file
27
__tests__/download/checksum/update-known-checksums.test.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { expect, test } from "@jest/globals";
|
||||||
|
import { updateChecksums } from "../../../src/download/checksum/update-known-checksums";
|
||||||
|
|
||||||
|
test("serializes checksum entries as JSON data", async () => {
|
||||||
|
const tempDirectory = await fs.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), "setup-uv-checksums-test-"),
|
||||||
|
);
|
||||||
|
const outputPath = path.join(tempDirectory, "known-checksums.json");
|
||||||
|
const key = 'platform-1.0.0"\n};\nglobalThis.compromised = true;';
|
||||||
|
const checksum = 'checksum"\\\nvalue';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateChecksums(outputPath, [
|
||||||
|
{ checksum, key },
|
||||||
|
{ checksum: "duplicate", key },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const content = await fs.readFile(outputPath, "utf8");
|
||||||
|
expect(JSON.parse(content)).toEqual({ [key]: checksum });
|
||||||
|
expect(content.endsWith("\n")).toBe(true);
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDirectory, { force: true, recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
1
__tests__/download/custom-manifest.ndjson
Normal file
1
__tests__/download/custom-manifest.ndjson
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":"0.9.26","artifacts":[{"platform":"x86_64-unknown-linux-gnu","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz","archive_format":"tar.gz","sha256":"30ccbf0a66dc8727a02b0e245c583ee970bdafecf3a443c1686e1b30ec4939e8"}]}
|
||||||
484
__tests__/download/download-version.test.ts
Normal file
484
__tests__/download/download-version.test.ts
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
|
import * as semver from "semver";
|
||||||
|
import { VERSIONS_MANIFEST_URL } from "../../src/utils/constants";
|
||||||
|
|
||||||
|
const mockInfo = jest.fn();
|
||||||
|
const mockWarning = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
debug: jest.fn(),
|
||||||
|
info: mockInfo,
|
||||||
|
warning: mockWarning,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockDownloadTool = jest.fn<any>();
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockExtractTar = jest.fn<any>();
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockExtractZip = jest.fn<any>();
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockCacheDir = jest.fn<any>();
|
||||||
|
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockGetLatestVersion = jest.fn<any>();
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockGetAllVersions = jest.fn<any>();
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockGetFirstMatchingVersion = jest.fn<any>();
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockGetArtifact = jest.fn<any>();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/download/manifest", () => ({
|
||||||
|
getAllVersions: mockGetAllVersions,
|
||||||
|
getArtifact: mockGetArtifact,
|
||||||
|
getFirstMatchingVersion: mockGetFirstMatchingVersion,
|
||||||
|
getLatestVersion: mockGetLatestVersion,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockValidateChecksum = jest.fn<any>();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/download/checksum/checksum", () => ({
|
||||||
|
validateChecksum: mockValidateChecksum,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockGetLatestKnownVersion = jest.fn(() => "0.9.25");
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/download/checksum/known-version", () => ({
|
||||||
|
getLatestKnownVersion: mockGetLatestKnownVersion,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { downloadVersion, resolveVersion, rewriteToMirror } = await import(
|
||||||
|
"../../src/download/download-version"
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("download-version", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockInfo.mockReset();
|
||||||
|
mockWarning.mockReset();
|
||||||
|
mockDownloadTool.mockReset();
|
||||||
|
mockExtractTar.mockReset();
|
||||||
|
mockExtractZip.mockReset();
|
||||||
|
mockCacheDir.mockReset();
|
||||||
|
mockGetLatestVersion.mockReset();
|
||||||
|
mockGetAllVersions.mockReset();
|
||||||
|
mockGetFirstMatchingVersion.mockReset();
|
||||||
|
mockGetArtifact.mockReset();
|
||||||
|
mockValidateChecksum.mockReset();
|
||||||
|
mockGetLatestKnownVersion.mockClear();
|
||||||
|
|
||||||
|
mockDownloadTool.mockResolvedValue("/tmp/downloaded");
|
||||||
|
mockExtractTar.mockResolvedValue("/tmp/extracted");
|
||||||
|
mockExtractZip.mockResolvedValue("/tmp/extracted");
|
||||||
|
mockCacheDir.mockResolvedValue("/tmp/cached");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveVersion", () => {
|
||||||
|
it("uses the default manifest to resolve latest", async () => {
|
||||||
|
mockGetLatestVersion.mockResolvedValue("0.9.26");
|
||||||
|
|
||||||
|
const version = await resolveVersion("latest", undefined);
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.26");
|
||||||
|
expect(mockGetLatestVersion).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves latest-known without reading the manifest", async () => {
|
||||||
|
const version = await resolveVersion("latest-known", undefined);
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.25");
|
||||||
|
expect(mockGetLatestKnownVersion).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetLatestVersion).not.toHaveBeenCalled();
|
||||||
|
expect(mockGetAllVersions).not.toHaveBeenCalled();
|
||||||
|
expect(mockGetFirstMatchingVersion).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops at the first matching version in the default manifest", async () => {
|
||||||
|
mockGetFirstMatchingVersion.mockImplementation(
|
||||||
|
(predicate: (version: string) => boolean) =>
|
||||||
|
["0.9.26", "0.9.25"].find(predicate),
|
||||||
|
);
|
||||||
|
|
||||||
|
const version = await resolveVersion("^0.9.0", undefined);
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.26");
|
||||||
|
expect(mockGetFirstMatchingVersion).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetAllVersions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("streams ranges when the default manifest URL is explicit", async () => {
|
||||||
|
mockGetFirstMatchingVersion.mockImplementation(
|
||||||
|
(predicate: (version: string) => boolean) =>
|
||||||
|
["0.9.26", "0.9.25"].find(predicate),
|
||||||
|
);
|
||||||
|
|
||||||
|
const version = await resolveVersion("^0.9.0", VERSIONS_MANIFEST_URL);
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.26");
|
||||||
|
expect(mockGetFirstMatchingVersion).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetAllVersions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("streams PEP 440 ranges from the default manifest", async () => {
|
||||||
|
mockGetFirstMatchingVersion.mockImplementation(
|
||||||
|
(predicate: (version: string) => boolean) =>
|
||||||
|
["0.9.26", "0.9.25"].find(predicate),
|
||||||
|
);
|
||||||
|
|
||||||
|
const version = await resolveVersion("!=0.9.26", undefined);
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.25");
|
||||||
|
expect(mockGetFirstMatchingVersion).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetAllVersions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats == exact pins as explicit versions", async () => {
|
||||||
|
const version = await resolveVersion("==0.9.26", undefined);
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.26");
|
||||||
|
expect(mockGetAllVersions).not.toHaveBeenCalled();
|
||||||
|
expect(mockGetLatestVersion).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses latest for minimum-only ranges when using the highest strategy", async () => {
|
||||||
|
mockGetLatestVersion.mockResolvedValue("0.9.26");
|
||||||
|
|
||||||
|
const version = await resolveVersion(">=0.9.0", undefined, "highest");
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.26");
|
||||||
|
expect(mockGetLatestVersion).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined);
|
||||||
|
expect(mockGetAllVersions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the lowest compatible version when requested", async () => {
|
||||||
|
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
|
||||||
|
|
||||||
|
const version = await resolveVersion("^0.9.0", undefined, "lowest");
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.25");
|
||||||
|
expect(mockGetAllVersions).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses manifest-file when provided", async () => {
|
||||||
|
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
|
||||||
|
|
||||||
|
const version = await resolveVersion(
|
||||||
|
"^0.9.0",
|
||||||
|
"https://example.com/custom.ndjson",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(version).toBe("0.9.26");
|
||||||
|
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.9.26",
|
||||||
|
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.9.26",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
),
|
||||||
|
).rejects.toThrow(
|
||||||
|
"Could not find artifact for version 0.9.26, arch x86_64, platform unknown-linux-gnu in https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson .",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).not.toHaveBeenCalled();
|
||||||
|
expect(mockValidateChecksum).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the default manifest checksum as a fallback", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "manifest-checksum",
|
||||||
|
downloadUrl: "https://example.com/uv.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.9.26",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockValidateChecksum).toHaveBeenCalledWith(
|
||||||
|
undefined,
|
||||||
|
"/tmp/downloaded",
|
||||||
|
"x86_64",
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"0.9.26",
|
||||||
|
"manifest-checksum",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rewrites GitHub Releases URLs to the Astral mirror", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.9.26",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledWith(
|
||||||
|
"https://releases.astral.sh/github/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not send the token to non-GitHub URLs from the default manifest", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl: "https://example.com/uv.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.9.26",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledWith(
|
||||||
|
"https://example.com/uv.tar.gz",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not send the token to GitHub lookalike hosts", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl: "https://github.com.evil.test/uv.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.9.26",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledWith(
|
||||||
|
"https://github.com.evil.test/uv.tar.gz",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to GitHub Releases when the mirror fails", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDownloadTool
|
||||||
|
.mockRejectedValueOnce(new Error("mirror unavailable"))
|
||||||
|
.mockResolvedValueOnce("/tmp/downloaded");
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.9.26",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockDownloadTool).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
"https://releases.astral.sh/github/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(mockDownloadTool).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-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("does not fall back for non-GitHub URLs", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl: "https://example.com/uv.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
mockDownloadTool.mockRejectedValue(new Error("download failed"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.9.26",
|
||||||
|
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-uv.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.9.26",
|
||||||
|
"",
|
||||||
|
"token",
|
||||||
|
"https://example.com/custom.ndjson",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockValidateChecksum).toHaveBeenCalledWith(
|
||||||
|
"manifest-checksum",
|
||||||
|
"/tmp/downloaded",
|
||||||
|
"x86_64",
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"0.9.26",
|
||||||
|
"manifest-checksum",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers checksum input over manifest-file checksum metadata", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "manifest-checksum",
|
||||||
|
downloadUrl: "https://example.com/custom-uv.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.9.26",
|
||||||
|
"user-checksum",
|
||||||
|
"token",
|
||||||
|
"https://example.com/custom.ndjson",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockValidateChecksum).toHaveBeenCalledWith(
|
||||||
|
"user-checksum",
|
||||||
|
"/tmp/downloaded",
|
||||||
|
"x86_64",
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"0.9.26",
|
||||||
|
"manifest-checksum",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips the Astral mirror when downloadFromAstralMirror is false", async () => {
|
||||||
|
mockGetArtifact.mockResolvedValue({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum: "abc123",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadVersion(
|
||||||
|
"unknown-linux-gnu",
|
||||||
|
"x86_64",
|
||||||
|
"0.9.26",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledWith(
|
||||||
|
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
undefined,
|
||||||
|
"token",
|
||||||
|
);
|
||||||
|
expect(mockDownloadTool).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rewriteToMirror", () => {
|
||||||
|
it("rewrites a GitHub Releases URL to the Astral mirror", () => {
|
||||||
|
expect(
|
||||||
|
rewriteToMirror(
|
||||||
|
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
),
|
||||||
|
).toBe(
|
||||||
|
"https://releases.astral.sh/github/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-gnu.tar.gz",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for non-GitHub URLs", () => {
|
||||||
|
expect(rewriteToMirror("https://example.com/uv.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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
370
__tests__/download/manifest.test.ts
Normal file
370
__tests__/download/manifest.test.ts
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
import {
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
jest,
|
||||||
|
} from "@jest/globals";
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
|
||||||
|
const mockFetch = jest.fn<any>();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
debug: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule("../../src/utils/fetch", () => ({
|
||||||
|
fetch: mockFetch,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
MANIFEST_FETCH_ATTEMPTS,
|
||||||
|
clearManifestCache,
|
||||||
|
fetchManifest,
|
||||||
|
getAllVersions,
|
||||||
|
getArtifact,
|
||||||
|
getFirstMatchingVersion,
|
||||||
|
getLatestVersion,
|
||||||
|
parseManifest,
|
||||||
|
} = await import("../../src/download/manifest");
|
||||||
|
|
||||||
|
const sampleManifestResponse = `{"version":"0.9.26","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-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/uv/releases/download/0.9.26/uv-x86_64-pc-windows-msvc.zip","archive_format":"zip","sha256":"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036"}]}
|
||||||
|
{"version":"0.9.25","artifacts":[{"platform":"aarch64-apple-darwin","variant":"default","url":"https://github.com/astral-sh/uv/releases/download/0.9.25/uv-aarch64-apple-darwin.tar.gz","archive_format":"tar.gz","sha256":"606b3c6949d971709f2526fa0d9f0fd23ccf60e09f117999b406b424af18a6a6"}]}`;
|
||||||
|
|
||||||
|
const multiVariantManifestResponse = `{"version":"0.9.26","artifacts":[{"platform":"aarch64-apple-darwin","variant":"python-managed","url":"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-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/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.zip","archive_format":"zip","sha256":"default-checksum"}]}`;
|
||||||
|
|
||||||
|
function createMockResponse(
|
||||||
|
ok: boolean,
|
||||||
|
status: number,
|
||||||
|
statusText: string,
|
||||||
|
data: string,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
body: null,
|
||||||
|
ok,
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
text: async () => data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStreamingMockResponse(
|
||||||
|
chunks: string[],
|
||||||
|
cancel: () => void | Promise<void> = () => {},
|
||||||
|
close = false,
|
||||||
|
) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
return {
|
||||||
|
body: new ReadableStream<Uint8Array>({
|
||||||
|
cancel,
|
||||||
|
start(controller) {
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
controller.enqueue(encoder.encode(chunk));
|
||||||
|
}
|
||||||
|
if (close) {
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
text: async () => chunks.join(""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("manifest", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clearManifestCache();
|
||||||
|
mockFetch.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
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.9.26");
|
||||||
|
expect(versions[1]?.version).toBe("0.9.25");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retries network failures", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
mockFetch
|
||||||
|
.mockRejectedValueOnce(new Error("request timed out"))
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = fetchManifest();
|
||||||
|
await jest.runAllTimersAsync();
|
||||||
|
|
||||||
|
await expect(result).resolves.toHaveLength(2);
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops after the configured number of network failures", async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
mockFetch.mockRejectedValue(new Error("request timed out"));
|
||||||
|
|
||||||
|
const result = expect(fetchManifest()).rejects.toThrow(
|
||||||
|
"request timed out",
|
||||||
|
);
|
||||||
|
await jest.runAllTimersAsync();
|
||||||
|
|
||||||
|
await result;
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(MANIFEST_FETCH_ATTEMPTS);
|
||||||
|
});
|
||||||
|
|
||||||
|
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.9.26", "0.9.25"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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.9.26");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stops reading the default manifest after the first record", async () => {
|
||||||
|
const [latestVersion] = sampleManifestResponse.split("\n");
|
||||||
|
const cancel = jest.fn();
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createStreamingMockResponse(
|
||||||
|
[
|
||||||
|
latestVersion.slice(0, 100),
|
||||||
|
`${latestVersion.slice(100)}\n`,
|
||||||
|
"invalid trailing data\n",
|
||||||
|
],
|
||||||
|
cancel,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(getLatestVersion()).resolves.toBe("0.9.26");
|
||||||
|
await expect(
|
||||||
|
getArtifact("0.9.26", "aarch64", "apple-darwin"),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
|
||||||
|
expect(cancel).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not fail when canceling the remaining response fails", async () => {
|
||||||
|
const [latestVersion] = sampleManifestResponse.split("\n");
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createStreamingMockResponse([`${latestVersion}\n`], () =>
|
||||||
|
Promise.reject(new Error("cancel failed")),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(getLatestVersion()).resolves.toBe("0.9.26");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getFirstMatchingVersion", () => {
|
||||||
|
it("stops at the first matching record", async () => {
|
||||||
|
const cancel = jest.fn();
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createStreamingMockResponse(
|
||||||
|
[`${sampleManifestResponse}\n`, "invalid trailing data\n"],
|
||||||
|
cancel,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getFirstMatchingVersion((version) => version === "0.9.25"),
|
||||||
|
).resolves.toBe("0.9.25");
|
||||||
|
|
||||||
|
expect(cancel).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caches a fully consumed response stream", async () => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createStreamingMockResponse(
|
||||||
|
[`${sampleManifestResponse}\n`],
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getFirstMatchingVersion((version) => version === "0.0.1"),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
await expect(getLatestVersion()).resolves.toBe("0.9.26");
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getArtifact", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockFetch.mockResolvedValue(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds an artifact by version and platform", async () => {
|
||||||
|
const artifact = await getArtifact("0.9.26", "aarch64", "apple-darwin");
|
||||||
|
|
||||||
|
expect(artifact).toEqual({
|
||||||
|
archiveFormat: "tar.gz",
|
||||||
|
checksum:
|
||||||
|
"fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.tar.gz",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds a windows artifact", async () => {
|
||||||
|
const artifact = await getArtifact("0.9.26", "x86_64", "pc-windows-msvc");
|
||||||
|
|
||||||
|
expect(artifact).toEqual({
|
||||||
|
archiveFormat: "zip",
|
||||||
|
checksum:
|
||||||
|
"eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-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.9.26", "aarch64", "apple-darwin");
|
||||||
|
|
||||||
|
expect(artifact).toEqual({
|
||||||
|
archiveFormat: "zip",
|
||||||
|
checksum: "default-checksum",
|
||||||
|
downloadUrl:
|
||||||
|
"https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.zip",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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.9.26",
|
||||||
|
"aarch64",
|
||||||
|
"unknown-linux-musl",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(artifact).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not cache records from a failed stream read", async () => {
|
||||||
|
const [latestVersion] = sampleManifestResponse.split("\n");
|
||||||
|
mockFetch
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
createStreamingMockResponse([
|
||||||
|
`${latestVersion}\n`,
|
||||||
|
"invalid manifest record\n",
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getArtifact("0.0.1", "aarch64", "apple-darwin"),
|
||||||
|
).rejects.toThrow("Failed to parse manifest data");
|
||||||
|
await expect(getLatestVersion()).resolves.toBe("0.9.26");
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not cache records when the response stream fails", async () => {
|
||||||
|
const [latestVersion] = sampleManifestResponse.split("\n");
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
let sentVersion = false;
|
||||||
|
const body = new ReadableStream<Uint8Array>({
|
||||||
|
pull(controller) {
|
||||||
|
if (!sentVersion) {
|
||||||
|
sentVersion = true;
|
||||||
|
controller.enqueue(encoder.encode(`${latestVersion}\n`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
controller.error(new Error("response stream failed"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockFetch
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
body,
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
createMockResponse(true, 200, "OK", sampleManifestResponse),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getArtifact("0.0.1", "aarch64", "apple-darwin"),
|
||||||
|
).rejects.toThrow("response stream failed");
|
||||||
|
await expect(getLatestVersion()).resolves.toBe("0.9.26");
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
2
__tests__/fixtures/.tool-versions
Normal file
2
__tests__/fixtures/.tool-versions
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
uv 0.5.15
|
||||||
|
python 3.13.1t
|
||||||
16
__tests__/fixtures/cache-dir-defined-project/pyproject.toml
Normal file
16
__tests__/fixtures/cache-dir-defined-project/pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[project]
|
||||||
|
name = "uv-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"ruff>=0.6.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
cache-dir = "/tmp/pyproject-toml-defined-cache-path"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
def hello() -> str:
|
||||||
|
return "Hello from uv-project!"
|
||||||
38
__tests__/fixtures/cache-dir-defined-project/uv.lock
generated
Normal file
38
__tests__/fixtures/cache-dir-defined-project/uv.lock
generated
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
version = 1
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/23/f4/279d044f66b79261fd37df76bf72b64471afab5d3b7906a01499c4451910/ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be", size = 2460281 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/4b/47dd7a69287afb4069fa42c198e899463605460a58120196711bfcf0446b/ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c", size = 9695871 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/c3/8aac62ac4638c14a740ee76a755a925f2d0d04580ab790a9887accb729f6/ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570", size = 9459354 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/cf/77fbd8d4617b9b9c503f9bffb8552c4e3ea1a58dc36975e7a9104ffb0f85/ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158", size = 9163871 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/1c/765192bab32b79efbb498b06f0b9dcb3629112b53b8777ae1d19b8209e09/ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534", size = 10096250 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/d0/86f3cb0f6934c99f759c232984a5204d67a26745cad2d9edff6248adf7d2/ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b", size = 9475376 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/cc/4c8d0e225b559a3fae6092ec310d7150d3b02b4669e9223f783ef64d82c0/ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d", size = 10295634 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/96/d2699cfb1bb5a01c68122af43454c76c31331e1c8a9bd97d653d7c82524b/ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66", size = 11024941 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/a9/6ecd66af8929e0f2a1ed308a4137f3521789f28f0eb97d32c2ca3aa7000c/ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8", size = 10606894 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/73/2ee4cd19f44992fedac1cc6db9e3d825966072f6dcbd4032f21cbd063170/ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1", size = 11552886 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/4c/c0f1cd35ce4a93c54a6bb1ee6934a3a205fa02198dd076678193853ceea1/ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1", size = 10264945 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/89/e45c9359b9cdd4245512ea2b9f2bb128a997feaa5f726fc9e8c7a66afadf/ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23", size = 10100007 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/74/0bd4e0a7ed5f6908df87892f9bf60a2356c0fd74102d8097298bd9b4f346/ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a", size = 9559267 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/03/3dc6dc9419f276f05805bf888c279e3e0b631284abd548d9e87cebb93aec/ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c", size = 9905304 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/5b/d6a72a6a6bbf097c09de468326ef5fa1c9e7aa5e6e45979bc0d984b0dbe7/ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56", size = 10341480 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/a9/0f2f21fe15ba537c46598f96aa9ae4a3d4b9ec64926664617ca6a8c772f4/ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da", size = 7961901 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/80/fff12ffe11853d9f4ea3e5221e6dd2e93640a161c05c9579833e09ad40a7/ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2", size = 8783320 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/91/577cdd64cce5e74d3f8b5ecb93f29566def569c741eb008aed4f331ef821/ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9", size = 8225886 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uv-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "ruff" }]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.11
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello from malformed-pyproject-toml-project!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "malformed-pyproject-toml-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[malformed-toml
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "old-python-constraint-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8,<=3.9"
|
||||||
|
dependencies = [
|
||||||
|
"ruff>=0.6.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.9.22,<0.10.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
def hello() -> str:
|
||||||
|
return "Hello from uv-project!"
|
||||||
40
__tests__/fixtures/old-python-constraint-project/uv.lock
generated
Normal file
40
__tests__/fixtures/old-python-constraint-project/uv.lock
generated
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.8, <=3.9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "old-python-constraint-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "ruff", specifier = ">=0.6.2" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.14.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.11
|
||||||
0
__tests__/fixtures/pyproject-toml-project/README.md
Normal file
0
__tests__/fixtures/pyproject-toml-project/README.md
Normal file
6
__tests__/fixtures/pyproject-toml-project/hello.py
Normal file
6
__tests__/fixtures/pyproject-toml-project/hello.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello from pyproject-toml-project!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
19
__tests__/fixtures/pyproject-toml-project/pyproject.toml
Normal file
19
__tests__/fixtures/pyproject-toml-project/pyproject.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[project]
|
||||||
|
name = "pyproject-toml-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"reuse==5.0.2",
|
||||||
|
{include-group = "lint"},
|
||||||
|
]
|
||||||
|
lint = [
|
||||||
|
"flake8==4.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
required-version = "==0.5.14"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
print("Hello world")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ruff>=0.6.2
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
print("Hello world")
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile --generate-hashes - -o ex-requirements.txt
|
||||||
|
click==8.2.1 \
|
||||||
|
--hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \
|
||||||
|
--hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b
|
||||||
|
# via uvicorn
|
||||||
|
h11==0.16.0 \
|
||||||
|
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
|
||||||
|
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
|
||||||
|
# via uvicorn
|
||||||
|
uv==0.8.3 \
|
||||||
|
--hash=sha256:1121ad1c9389b865d029385031d3fd7d90d343c92a2149a4d4aa20bf469cb27f \
|
||||||
|
--hash=sha256:17bcdb0615e37cc5f985f7d7546f755ac6343c1dc8bbe876c892437f14f8f904 \
|
||||||
|
--hash=sha256:2ccaae4c749126c99f6404d67a0ae1eae29cbafb05603d09094a775061fdf4e5 \
|
||||||
|
--hash=sha256:2e311c029bff2ca07c6ddf877ccc5935cabb78e09b94b53a849542665b6a6fa1 \
|
||||||
|
--hash=sha256:391c97577048a40fd8c85b370055df6420f26e81df7fa906f0e0ce1aa2af3527 \
|
||||||
|
--hash=sha256:3f904f574dc2d7aa1d96ddf2483480ecd121dc9d060108cadd8bff100b754b64 \
|
||||||
|
--hash=sha256:526f2c3bd6f311ce31f6f7b6b7d818b191f41e76bed3aaab671b716220c02d8f \
|
||||||
|
--hash=sha256:5313ee776ad65731ffa8ac585246f987d3a2bf72e6153c12add1fff22ad6e500 \
|
||||||
|
--hash=sha256:5843cc43bafad05cc710d8e31bd347ee37202462a63d32c30746e9df48cfbda2 \
|
||||||
|
--hash=sha256:76de331a07e5ae9b6490e70a9439a072b91b3167a5684510af10c2752c4ece9a \
|
||||||
|
--hash=sha256:8486f7576d15cc73509f93f47b3190f44701ea36839906369301b58c8604d5db \
|
||||||
|
--hash=sha256:8b16f1bddfdf8f7470924ab34a7b55e4c372d5340c7c1e47e7fc84a743dc541f \
|
||||||
|
--hash=sha256:966ec7d7f57521fef0fee685d71e183c9cafb358ddcfe27519dfeaf40550f247 \
|
||||||
|
--hash=sha256:989898caeb6e972979543b57547d1c28ab8af81ff8fc15921fd354c17d432749 \
|
||||||
|
--hash=sha256:9ce7981f4fbeecf93dc5cf0a5a7915e84956fd99ad3ac977c048fe0cfdb1a17e \
|
||||||
|
--hash=sha256:ad13453ab0a1dfa64a221aac8f52199efdcaa52c97134fffd7bcebed794a6f4b \
|
||||||
|
--hash=sha256:ae7efe91dcfc24126fa91e0fb69a1daf6c0e494a781ba192bb0cc62d7ab623ee \
|
||||||
|
--hash=sha256:daa6e0d657a94f20e962d4a03d833ef7af5c8e51b7c8a2d92ba6cf64a4c07ac1 \
|
||||||
|
--hash=sha256:f1eb7c896fc0d80ed534748aaf46697b6ebc8ce401f1c51666ce0b9923c3db9a
|
||||||
|
uvicorn==0.35.0 \
|
||||||
|
--hash=sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a \
|
||||||
|
--hash=sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
print("Hello world")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
uvicorn==0.35.0
|
||||||
|
uv==0.6.17
|
||||||
1
__tests__/fixtures/uv-toml-project/.python-version
Normal file
1
__tests__/fixtures/uv-toml-project/.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.11
|
||||||
0
__tests__/fixtures/uv-toml-project/README.md
Normal file
0
__tests__/fixtures/uv-toml-project/README.md
Normal file
6
__tests__/fixtures/uv-toml-project/hello.py
Normal file
6
__tests__/fixtures/uv-toml-project/hello.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello from uv-toml-project!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
10
__tests__/fixtures/uv-toml-project/pyproject.toml
Normal file
10
__tests__/fixtures/uv-toml-project/pyproject.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[project]
|
||||||
|
name = "uv-toml-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
required-version = "==0.5.14"
|
||||||
1
__tests__/fixtures/uv-toml-project/uv.toml
Normal file
1
__tests__/fixtures/uv-toml-project/uv.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
required-version = "==0.5.15"
|
||||||
37
__tests__/helpers/setup-inputs.ts
Normal file
37
__tests__/helpers/setup-inputs.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { CacheLocalSource, type SetupInputs } from "../../src/utils/inputs";
|
||||||
|
|
||||||
|
export function createSetupInputs(
|
||||||
|
overrides: Partial<SetupInputs> = {},
|
||||||
|
): SetupInputs {
|
||||||
|
return {
|
||||||
|
activateEnvironment: false,
|
||||||
|
addProblemMatchers: false,
|
||||||
|
cacheDependencyGlob: "uv.lock",
|
||||||
|
cacheLocalPath: {
|
||||||
|
path: "/tmp/setup-uv-cache",
|
||||||
|
source: CacheLocalSource.Input,
|
||||||
|
},
|
||||||
|
cachePython: false,
|
||||||
|
cacheSuffix: "",
|
||||||
|
checksum: "",
|
||||||
|
downloadFromAstralMirror: false,
|
||||||
|
enableCache: true,
|
||||||
|
githubToken: "",
|
||||||
|
ignoreEmptyWorkdir: false,
|
||||||
|
ignoreNothingToCache: false,
|
||||||
|
noProject: false,
|
||||||
|
pruneCache: false,
|
||||||
|
pythonArch: "",
|
||||||
|
pythonDir: "/tmp/uv-python-dir",
|
||||||
|
pythonVersion: "",
|
||||||
|
quiet: false,
|
||||||
|
resolutionStrategy: "highest",
|
||||||
|
restoreCache: false,
|
||||||
|
saveCache: true,
|
||||||
|
venvPath: "/workspace/.venv",
|
||||||
|
version: "",
|
||||||
|
versionFile: "",
|
||||||
|
workingDirectory: "/workspace",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
482
__tests__/utils/inputs.test.ts
Normal file
482
__tests__/utils/inputs.test.ts
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
let mockInputs: Record<string, string> = {};
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
const ORIGINAL_GITHUB_EVENT_NAME = process.env.GITHUB_EVENT_NAME;
|
||||||
|
const ORIGINAL_GITHUB_REF = process.env.GITHUB_REF;
|
||||||
|
const ORIGINAL_HOME = process.env.HOME;
|
||||||
|
const ORIGINAL_RUNNER_ENVIRONMENT = process.env.RUNNER_ENVIRONMENT;
|
||||||
|
const ORIGINAL_RUNNER_TEMP = process.env.RUNNER_TEMP;
|
||||||
|
const ORIGINAL_UV_CACHE_DIR = process.env.UV_CACHE_DIR;
|
||||||
|
const ORIGINAL_UV_PYTHON = process.env.UV_PYTHON;
|
||||||
|
const ORIGINAL_UV_PYTHON_ARCH = process.env.UV_PYTHON_ARCH;
|
||||||
|
const ORIGINAL_UV_PYTHON_INSTALL_DIR = process.env.UV_PYTHON_INSTALL_DIR;
|
||||||
|
|
||||||
|
const mockDebug = jest.fn();
|
||||||
|
const mockGetBooleanInput = jest.fn(
|
||||||
|
(name: string) => (mockInputs[name] ?? "") === "true",
|
||||||
|
);
|
||||||
|
const mockGetInput = jest.fn((name: string) => mockInputs[name] ?? "");
|
||||||
|
const mockInfo = jest.fn();
|
||||||
|
const mockWarning = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
debug: mockDebug,
|
||||||
|
getBooleanInput: mockGetBooleanInput,
|
||||||
|
getInput: mockGetInput,
|
||||||
|
info: mockInfo,
|
||||||
|
warning: mockWarning,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { CacheLocalSource, loadInputs, resolvePythonArch } = await import(
|
||||||
|
"../../src/utils/inputs"
|
||||||
|
);
|
||||||
|
|
||||||
|
function createTempProject(files: Record<string, string> = {}): string {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "setup-uv-inputs-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetEnvironment(): void {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockInputs = {};
|
||||||
|
delete process.env.GITHUB_EVENT_NAME;
|
||||||
|
delete process.env.GITHUB_REF;
|
||||||
|
process.env.HOME = "/home/testuser";
|
||||||
|
delete process.env.RUNNER_ENVIRONMENT;
|
||||||
|
delete process.env.RUNNER_TEMP;
|
||||||
|
delete process.env.UV_CACHE_DIR;
|
||||||
|
delete process.env.UV_PYTHON;
|
||||||
|
delete process.env.UV_PYTHON_ARCH;
|
||||||
|
delete process.env.UV_PYTHON_INSTALL_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreEnvironment(): void {
|
||||||
|
for (const dir of tempDirs.splice(0)) {
|
||||||
|
fs.rmSync(dir, { force: true, recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.GITHUB_EVENT_NAME = ORIGINAL_GITHUB_EVENT_NAME;
|
||||||
|
process.env.GITHUB_REF = ORIGINAL_GITHUB_REF;
|
||||||
|
process.env.HOME = ORIGINAL_HOME;
|
||||||
|
process.env.RUNNER_ENVIRONMENT = ORIGINAL_RUNNER_ENVIRONMENT;
|
||||||
|
process.env.RUNNER_TEMP = ORIGINAL_RUNNER_TEMP;
|
||||||
|
process.env.UV_CACHE_DIR = ORIGINAL_UV_CACHE_DIR;
|
||||||
|
process.env.UV_PYTHON = ORIGINAL_UV_PYTHON;
|
||||||
|
if (ORIGINAL_UV_PYTHON_ARCH === undefined) {
|
||||||
|
delete process.env.UV_PYTHON_ARCH;
|
||||||
|
} else {
|
||||||
|
process.env.UV_PYTHON_ARCH = ORIGINAL_UV_PYTHON_ARCH;
|
||||||
|
}
|
||||||
|
process.env.UV_PYTHON_INSTALL_DIR = ORIGINAL_UV_PYTHON_INSTALL_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(resetEnvironment);
|
||||||
|
afterEach(restoreEnvironment);
|
||||||
|
|
||||||
|
describe("loadInputs", () => {
|
||||||
|
it("loads defaults for a github-hosted runner", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["enable-cache"] = "auto";
|
||||||
|
process.env.RUNNER_ENVIRONMENT = "github-hosted";
|
||||||
|
process.env.RUNNER_TEMP = "/runner-temp";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.enableCache).toBe(true);
|
||||||
|
expect(inputs.cacheLocalPath).toEqual({
|
||||||
|
path: "/runner-temp/setup-uv-cache",
|
||||||
|
source: CacheLocalSource.Default,
|
||||||
|
});
|
||||||
|
expect(inputs.pythonDir).toBe("/runner-temp/uv-python-dir");
|
||||||
|
expect(inputs.pythonArch).toBe("");
|
||||||
|
expect(inputs.venvPath).toBe("/workspace/.venv");
|
||||||
|
expect(inputs.manifestFile).toBeUndefined();
|
||||||
|
expect(inputs.resolutionStrategy).toBe("highest");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the Python version from an explicitly selected .tool-versions file", () => {
|
||||||
|
mockInputs["working-directory"] = createTempProject({
|
||||||
|
".tool-versions": "uv 0.12.3\npython 3.13.1t\n",
|
||||||
|
});
|
||||||
|
mockInputs["version-file"] = ".tool-versions";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.pythonVersion).toBe("3.13.1t");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers the python-version input over .tool-versions", () => {
|
||||||
|
mockInputs["working-directory"] = createTempProject({
|
||||||
|
".tool-versions": "uv 0.12.3\npython 3.13\n",
|
||||||
|
});
|
||||||
|
mockInputs["version-file"] = ".tool-versions";
|
||||||
|
mockInputs["python-version"] = "3.12";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.pythonVersion).toBe("3.12");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves UV_PYTHON instead of overriding it from .tool-versions", () => {
|
||||||
|
mockInputs["working-directory"] = createTempProject({
|
||||||
|
".tool-versions": "uv 0.12.3\npython 3.13\n",
|
||||||
|
});
|
||||||
|
mockInputs["version-file"] = ".tool-versions";
|
||||||
|
process.env.UV_PYTHON = "3.11";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.pythonVersion).toBe("");
|
||||||
|
expect(process.env.UV_PYTHON).toBe("3.11");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not discover .tool-versions from the working directory", () => {
|
||||||
|
mockInputs["working-directory"] = createTempProject({
|
||||||
|
".tool-versions": "uv 0.12.3\npython 3.13\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.pythonVersion).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers the python-arch input over UV_PYTHON_ARCH", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["python-arch"] = "x86_64";
|
||||||
|
process.env.UV_PYTHON_ARCH = "aarch64";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
expect(inputs.pythonArch).toBe("x86_64");
|
||||||
|
expect(resolvePythonArch(inputs.pythonArch)).toBe("x86_64");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["aarch64", "x86_64_v3", ""])(
|
||||||
|
"uses UV_PYTHON_ARCH when python-arch is omitted: %s",
|
||||||
|
(pythonArch) => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
process.env.UV_PYTHON_ARCH = pythonArch;
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
expect(inputs.pythonArch).toBe("");
|
||||||
|
expect(resolvePythonArch(inputs.pythonArch)).toBe(pythonArch);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(["pull_request_target", "workflow_run", "release"])(
|
||||||
|
"disables automatic caching for the %s event",
|
||||||
|
(eventName) => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["enable-cache"] = "auto";
|
||||||
|
process.env.RUNNER_ENVIRONMENT = "github-hosted";
|
||||||
|
process.env.RUNNER_TEMP = "/runner-temp";
|
||||||
|
process.env.GITHUB_EVENT_NAME = eventName;
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.enableCache).toBe(false);
|
||||||
|
expect(mockInfo).toHaveBeenCalledWith(
|
||||||
|
`Caching is disabled for the ${eventName} event`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("disables automatic caching for tag pushes", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["enable-cache"] = "auto";
|
||||||
|
process.env.RUNNER_ENVIRONMENT = "github-hosted";
|
||||||
|
process.env.RUNNER_TEMP = "/runner-temp";
|
||||||
|
process.env.GITHUB_EVENT_NAME = "push";
|
||||||
|
process.env.GITHUB_REF = "refs/tags/v1.0.0";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.enableCache).toBe(false);
|
||||||
|
expect(mockInfo).toHaveBeenCalledWith("Caching is disabled for tag pushes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("enables automatic caching for branch pushes", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["enable-cache"] = "auto";
|
||||||
|
process.env.RUNNER_ENVIRONMENT = "github-hosted";
|
||||||
|
process.env.RUNNER_TEMP = "/runner-temp";
|
||||||
|
process.env.GITHUB_EVENT_NAME = "push";
|
||||||
|
process.env.GITHUB_REF = "refs/heads/main";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.enableCache).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("honors explicitly enabled caching for sensitive events", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["enable-cache"] = "true";
|
||||||
|
process.env.RUNNER_ENVIRONMENT = "github-hosted";
|
||||||
|
process.env.RUNNER_TEMP = "/runner-temp";
|
||||||
|
process.env.GITHUB_EVENT_NAME = "release";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.enableCache).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("restores but does not save cache automatically for merge groups", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["enable-cache"] = "auto";
|
||||||
|
mockInputs["restore-cache"] = "true";
|
||||||
|
mockInputs["save-cache"] = "auto";
|
||||||
|
process.env.RUNNER_ENVIRONMENT = "github-hosted";
|
||||||
|
process.env.RUNNER_TEMP = "/runner-temp";
|
||||||
|
process.env.GITHUB_EVENT_NAME = "merge_group";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.enableCache).toBe(true);
|
||||||
|
expect(inputs.restoreCache).toBe(true);
|
||||||
|
expect(inputs.saveCache).toBe(false);
|
||||||
|
expect(mockInfo).toHaveBeenCalledWith(
|
||||||
|
"Cache saving is disabled for the merge_group event",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["true", true],
|
||||||
|
["false", false],
|
||||||
|
])("honors save-cache %s for merge groups", (saveCacheInput, expected) => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["save-cache"] = saveCacheInput;
|
||||||
|
process.env.GITHUB_EVENT_NAME = "merge_group";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.saveCache).toBe(expected);
|
||||||
|
expect(mockInfo).not.toHaveBeenCalledWith(
|
||||||
|
"Cache saving is disabled for the merge_group event",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("automatically saves cache for other events", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["save-cache"] = "auto";
|
||||||
|
process.env.GITHUB_EVENT_NAME = "push";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.saveCache).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses cache-dir from pyproject.toml when present", () => {
|
||||||
|
mockInputs["working-directory"] = createTempProject({
|
||||||
|
"pyproject.toml": `[project]
|
||||||
|
name = "uv-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
cache-dir = "/tmp/pyproject-toml-defined-cache-path"
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.cacheLocalPath).toEqual({
|
||||||
|
path: "/tmp/pyproject-toml-defined-cache-path",
|
||||||
|
source: CacheLocalSource.Config,
|
||||||
|
});
|
||||||
|
expect(mockInfo).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Found cache-dir in"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses UV_CACHE_DIR from the environment", () => {
|
||||||
|
mockInputs["working-directory"] = createTempProject();
|
||||||
|
process.env.UV_CACHE_DIR = "/env/cache-dir";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.cacheLocalPath).toEqual({
|
||||||
|
path: "/env/cache-dir",
|
||||||
|
source: CacheLocalSource.Env,
|
||||||
|
});
|
||||||
|
expect(mockInfo).toHaveBeenCalledWith(
|
||||||
|
"UV_CACHE_DIR is already set to /env/cache-dir",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses UV_PYTHON_INSTALL_DIR from the environment", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
process.env.UV_PYTHON_INSTALL_DIR = "/env/python-dir";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.pythonDir).toBe("/env/python-dir");
|
||||||
|
expect(mockInfo).toHaveBeenCalledWith(
|
||||||
|
"UV_PYTHON_INSTALL_DIR is already set to /env/python-dir",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns when parsing a malformed pyproject.toml for cache-dir", () => {
|
||||||
|
mockInputs["working-directory"] = createTempProject({
|
||||||
|
"pyproject.toml": `[project]
|
||||||
|
name = "malformed-pyproject-toml-project"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[malformed-toml
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.cacheLocalPath).toBeUndefined();
|
||||||
|
expect(mockWarning).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Error while parsing pyproject.toml:"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws for an invalid resolution strategy", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["resolution-strategy"] = "middle";
|
||||||
|
|
||||||
|
expect(() => loadInputs()).toThrow(
|
||||||
|
"Invalid resolution-strategy: middle. Must be 'highest' or 'lowest'.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cacheDependencyGlob", () => {
|
||||||
|
it("returns empty string when input not provided", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.cacheDependencyGlob).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["requirements.txt", "/workspace/requirements.txt"],
|
||||||
|
["./uv.lock", "/workspace/uv.lock"],
|
||||||
|
])("resolves %s to %s", (globInput, expected) => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["cache-dependency-glob"] = globInput;
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.cacheDependencyGlob).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles multiple lines, trimming whitespace, tilde expansion and absolute paths", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["cache-dependency-glob"] =
|
||||||
|
" ~/.cache/file1\n ./rel/file2 \nfile3.txt";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.cacheDependencyGlob).toBe(
|
||||||
|
[
|
||||||
|
"/home/testuser/.cache/file1",
|
||||||
|
"/workspace/rel/file2",
|
||||||
|
"/workspace/file3.txt",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
"/abs/path.lock\nrelative.lock",
|
||||||
|
["/abs/path.lock", "/workspace/relative.lock"].join("\n"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"!/abs/path.lock\n!relative.lock",
|
||||||
|
["!/abs/path.lock", "!/workspace/relative.lock"].join("\n"),
|
||||||
|
],
|
||||||
|
])("normalizes multiline glob %s", (globInput, expected) => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["cache-dependency-glob"] = globInput;
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.cacheDependencyGlob).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("tool directories", () => {
|
||||||
|
it("expands tilde for tool-bin-dir and tool-dir", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["tool-bin-dir"] = "~/tool-bin-dir";
|
||||||
|
mockInputs["tool-dir"] = "~/tool-dir";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.toolBinDir).toBe("/home/testuser/tool-bin-dir");
|
||||||
|
expect(inputs.toolDir).toBe("/home/testuser/tool-dir");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cacheLocalPath", () => {
|
||||||
|
it("expands tilde in cache-local-path", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["cache-local-path"] = "~/uv-cache/cache-local-path";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.cacheLocalPath).toEqual({
|
||||||
|
path: "/home/testuser/uv-cache/cache-local-path",
|
||||||
|
source: CacheLocalSource.Input,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("venvPath", () => {
|
||||||
|
it("defaults to .venv in the working directory", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.venvPath).toBe("/workspace/.venv");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["custom-venv", "/workspace/custom-venv"],
|
||||||
|
["custom-venv/", "/workspace/custom-venv"],
|
||||||
|
["/tmp/custom-venv", "/tmp/custom-venv"],
|
||||||
|
["~/.venv", "/home/testuser/.venv"],
|
||||||
|
])("resolves venv-path %s to %s", (venvPathInput, expected) => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["activate-environment"] = "true";
|
||||||
|
mockInputs["venv-path"] = venvPathInput;
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.venvPath).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("warns when venv-path is set but activate-environment is false", () => {
|
||||||
|
mockInputs["working-directory"] = "/workspace";
|
||||||
|
mockInputs["venv-path"] = "custom-venv";
|
||||||
|
|
||||||
|
const inputs = loadInputs();
|
||||||
|
|
||||||
|
expect(inputs.activateEnvironment).toBe(false);
|
||||||
|
expect(inputs.venvPath).toBe("/workspace/custom-venv");
|
||||||
|
expect(mockWarning).toHaveBeenCalledWith(
|
||||||
|
"venv-path is only used when activate-environment is true",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
152
__tests__/utils/python-arch.test.ts
Normal file
152
__tests__/utils/python-arch.test.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { promisify } from "node:util";
|
||||||
|
import {
|
||||||
|
afterEach,
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
it,
|
||||||
|
jest,
|
||||||
|
} from "@jest/globals";
|
||||||
|
|
||||||
|
const ORIGINAL_UV_PYTHON_ARCH = process.env.UV_PYTHON_ARCH;
|
||||||
|
|
||||||
|
const mockExecFile =
|
||||||
|
jest.fn<
|
||||||
|
(...args: unknown[]) => Promise<{
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}>
|
||||||
|
>();
|
||||||
|
const mockExportVariable = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("node:child_process", () => ({
|
||||||
|
execFile: Object.assign(mockExecFile, { [promisify.custom]: mockExecFile }),
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
exportVariable: mockExportVariable,
|
||||||
|
}));
|
||||||
|
jest.unstable_mockModule("../../src/utils/logging", () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { setupPythonArch } = await import("../../src/utils/python-arch");
|
||||||
|
|
||||||
|
const supportedProbe = Object.assign(new Error("uv exited with code 2"), {
|
||||||
|
code: 2,
|
||||||
|
stderr:
|
||||||
|
"error: Failed to parse environment variable `UV_PYTHON_ARCH` with invalid value `setup-uv-probe`: Unknown architecture: setup-uv-probe\n",
|
||||||
|
});
|
||||||
|
const success = { stderr: "", stdout: "/cache\n" };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env.UV_PYTHON_ARCH;
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockExecFile
|
||||||
|
.mockReset()
|
||||||
|
.mockRejectedValueOnce(supportedProbe)
|
||||||
|
.mockResolvedValueOnce(success);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (ORIGINAL_UV_PYTHON_ARCH === undefined) {
|
||||||
|
delete process.env.UV_PYTHON_ARCH;
|
||||||
|
} else {
|
||||||
|
process.env.UV_PYTHON_ARCH = ORIGINAL_UV_PYTHON_ARCH;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setupPythonArch", () => {
|
||||||
|
it("leaves Python selection alone without an architecture", async () => {
|
||||||
|
await setupPythonArch("/tools/uv", "");
|
||||||
|
|
||||||
|
expect(mockExecFile).not.toHaveBeenCalled();
|
||||||
|
expect(mockExportVariable).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates an inherited architecture without exporting it", async () => {
|
||||||
|
process.env.UV_PYTHON_ARCH = "aarch64";
|
||||||
|
|
||||||
|
await setupPythonArch("/tools/uv", "");
|
||||||
|
|
||||||
|
expect(mockExecFile).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"/tools/uv",
|
||||||
|
["--no-config", "cache", "dir"],
|
||||||
|
expect.objectContaining({
|
||||||
|
env: expect.objectContaining({ UV_PYTHON_ARCH: "aarch64" }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(mockExportVariable).not.toHaveBeenCalled();
|
||||||
|
expect(process.env.UV_PYTHON_ARCH).toBe("aarch64");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["/runner temp/uv", "C:\\runner temp\\uv.exe"])(
|
||||||
|
"validates and exports the architecture using the installed uv: %s",
|
||||||
|
async (uvPath) => {
|
||||||
|
await setupPythonArch(uvPath, "x86_64");
|
||||||
|
|
||||||
|
expect(mockExecFile).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
uvPath,
|
||||||
|
["--no-config", "cache", "dir"],
|
||||||
|
expect.objectContaining({
|
||||||
|
encoding: "utf8",
|
||||||
|
env: expect.objectContaining({ UV_PYTHON_ARCH: "setup-uv-probe" }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(mockExecFile).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
uvPath,
|
||||||
|
["--no-config", "cache", "dir"],
|
||||||
|
expect.objectContaining({
|
||||||
|
env: expect.objectContaining({ UV_PYTHON_ARCH: "x86_64" }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(mockExportVariable).toHaveBeenCalledWith(
|
||||||
|
"UV_PYTHON_ARCH",
|
||||||
|
"x86_64",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("rejects uv versions that ignore UV_PYTHON_ARCH", async () => {
|
||||||
|
mockExecFile.mockReset().mockResolvedValue(success);
|
||||||
|
|
||||||
|
await expect(setupPythonArch("/tools/uv", "x86_64")).rejects.toThrow(
|
||||||
|
"The installed version of uv does not support UV_PYTHON_ARCH",
|
||||||
|
);
|
||||||
|
expect(mockExecFile).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockExportVariable).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports unexpected probe failures", async () => {
|
||||||
|
mockExecFile.mockReset().mockRejectedValue(
|
||||||
|
Object.assign(new Error("uv exited with code 2"), {
|
||||||
|
code: 2,
|
||||||
|
stderr: "error: unrelated configuration error\n",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(setupPythonArch("/tools/uv", "x86_64")).rejects.toThrow(
|
||||||
|
"Failed to check uv's support for UV_PYTHON_ARCH: error: unrelated configuration error",
|
||||||
|
);
|
||||||
|
expect(mockExportVariable).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports invalid architectures before exporting them", async () => {
|
||||||
|
mockExecFile
|
||||||
|
.mockReset()
|
||||||
|
.mockRejectedValueOnce(supportedProbe)
|
||||||
|
.mockRejectedValueOnce(
|
||||||
|
Object.assign(new Error("uv exited with code 2"), {
|
||||||
|
code: 2,
|
||||||
|
stderr: "error: Unknown architecture: invalid\n",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(setupPythonArch("/tools/uv", "invalid")).rejects.toThrow(
|
||||||
|
"Failed to set Python architecture to invalid: error: Unknown architecture: invalid",
|
||||||
|
);
|
||||||
|
expect(mockExportVariable).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
86
__tests__/utils/python-runtime.test.ts
Normal file
86
__tests__/utils/python-runtime.test.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { promisify } from "node:util";
|
||||||
|
import { beforeEach, expect, it, jest } from "@jest/globals";
|
||||||
|
import { createSetupInputs } from "../helpers/setup-inputs";
|
||||||
|
|
||||||
|
const mockExecFile =
|
||||||
|
jest.fn<
|
||||||
|
(...args: unknown[]) => Promise<{ stdout: string; stderr: string }>
|
||||||
|
>();
|
||||||
|
const inputs = createSetupInputs({
|
||||||
|
activateEnvironment: true,
|
||||||
|
pythonVersion: "3.15t",
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.unstable_mockModule("node:child_process", () => ({
|
||||||
|
// execFile's custom promisifier returns both stdout and stderr.
|
||||||
|
execFile: Object.assign(mockExecFile, { [promisify.custom]: mockExecFile }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { getPythonRuntimeId } = await import("../../src/utils/python-runtime");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockExecFile.mockReset();
|
||||||
|
mockExecFile.mockResolvedValue({
|
||||||
|
stderr: "",
|
||||||
|
stdout: '[{"key":"cpython-3.13.1-linux-x86_64-gnu"}]\r\n',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not query uv without environment activation", async () => {
|
||||||
|
expect(
|
||||||
|
await getPythonRuntimeId({ ...inputs, activateEnvironment: false }),
|
||||||
|
).toBe("");
|
||||||
|
expect(mockExecFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"cpython-3.13.1-linux-x86_64-gnu",
|
||||||
|
"cpython-3.15.0rc1+freethreaded-macos-aarch64-none",
|
||||||
|
"cpython-3.15.0rc2+freethreaded-windows-x86_64-none",
|
||||||
|
"pypy-3.11.15-linux-x86_64-gnu",
|
||||||
|
])("returns uv's opaque runtime key unchanged: %s", async (key) => {
|
||||||
|
mockExecFile.mockResolvedValue({
|
||||||
|
stderr: "",
|
||||||
|
stdout: `${JSON.stringify([{ key }])}\r\n`,
|
||||||
|
});
|
||||||
|
expect(await getPythonRuntimeId(inputs)).toBe(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['/runner temp/a "quoted" venv', "C:\\runner temp\\custom venv"])(
|
||||||
|
"queries the exact venv directory: %s",
|
||||||
|
async (venvPath) => {
|
||||||
|
await getPythonRuntimeId({ ...inputs, venvPath });
|
||||||
|
expect(mockExecFile).toHaveBeenCalledWith(
|
||||||
|
"uv",
|
||||||
|
[
|
||||||
|
"python",
|
||||||
|
"list",
|
||||||
|
venvPath,
|
||||||
|
"--only-installed",
|
||||||
|
"--output-format",
|
||||||
|
"json",
|
||||||
|
],
|
||||||
|
{ encoding: "utf8" },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
new Error("uv failed"),
|
||||||
|
"not JSON",
|
||||||
|
"null",
|
||||||
|
"{}",
|
||||||
|
"[]",
|
||||||
|
'[{"key":""}]',
|
||||||
|
'[{"key":123}]',
|
||||||
|
'[{"key":"first"},{"key":"second"}]',
|
||||||
|
])("rejects uv failure or invalid results: %s", async (result) => {
|
||||||
|
if (result instanceof Error) {
|
||||||
|
mockExecFile.mockRejectedValue(result);
|
||||||
|
} else {
|
||||||
|
mockExecFile.mockResolvedValue({ stderr: "", stdout: result });
|
||||||
|
}
|
||||||
|
await expect(getPythonRuntimeId(inputs)).rejects.toThrow(
|
||||||
|
"Failed to identify the activated environment's Python runtime:",
|
||||||
|
);
|
||||||
|
});
|
||||||
37
__tests__/version/requirements-file.test.ts
Normal file
37
__tests__/version/requirements-file.test.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { expect, test } from "@jest/globals";
|
||||||
|
import { getUvVersionFromFile } from "../../src/version/file-parser";
|
||||||
|
import {
|
||||||
|
getUvVersionFromPyprojectContent,
|
||||||
|
getUvVersionFromRequirementsText,
|
||||||
|
} from "../../src/version/requirements-file";
|
||||||
|
|
||||||
|
test("ignores dependencies starting with uv", async () => {
|
||||||
|
const parsedVersion = getUvVersionFromFile(
|
||||||
|
"__tests__/fixtures/uv-in-requirements-txt-project/requirements.txt",
|
||||||
|
);
|
||||||
|
expect(parsedVersion).toBe("0.6.17");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
["without space before marker", "uv==0.11.20; sys_platform != 'emscripten'"],
|
||||||
|
["with space before marker", "uv==0.11.20 ; sys_platform != 'emscripten'"],
|
||||||
|
])(
|
||||||
|
"strips PEP 508 markers from pyproject dependency groups %s",
|
||||||
|
(_, dependency) => {
|
||||||
|
const parsedVersion = getUvVersionFromPyprojectContent(`[dependency-groups]
|
||||||
|
test = [
|
||||||
|
"${dependency}",
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(parsedVersion).toBe("==0.11.20");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("strips PEP 508 markers from requirements dependencies", () => {
|
||||||
|
const parsedVersion = getUvVersionFromRequirementsText(
|
||||||
|
"uv==0.11.20; sys_platform != 'emscripten'",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parsedVersion).toBe("==0.11.20");
|
||||||
|
});
|
||||||
9
__tests__/version/requirements-hashes-file.test.ts
Normal file
9
__tests__/version/requirements-hashes-file.test.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { expect, test } from "@jest/globals";
|
||||||
|
import { getUvVersionFromFile } from "../../src/version/file-parser";
|
||||||
|
|
||||||
|
test("ignores dependencies starting with uv", async () => {
|
||||||
|
const parsedVersion = getUvVersionFromFile(
|
||||||
|
"__tests__/fixtures/uv-in-requirements-hash-txt-project/requirements.txt",
|
||||||
|
);
|
||||||
|
expect(parsedVersion).toBe("0.8.3");
|
||||||
|
});
|
||||||
211
__tests__/version/tool-versions-file.test.ts
Normal file
211
__tests__/version/tool-versions-file.test.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||||
|
|
||||||
|
const mockReadFileSync = jest.fn();
|
||||||
|
const mockWarning = jest.fn();
|
||||||
|
|
||||||
|
jest.unstable_mockModule("node:fs", () => ({
|
||||||
|
default: {
|
||||||
|
readFileSync: mockReadFileSync,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.unstable_mockModule("@actions/core", () => ({
|
||||||
|
warning: mockWarning,
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function getVersionFromToolVersions(filePath: string) {
|
||||||
|
const { getUvVersionFromToolVersions } = await import(
|
||||||
|
"../../src/version/tool-versions-file"
|
||||||
|
);
|
||||||
|
|
||||||
|
return getUvVersionFromToolVersions(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPythonVersionFromToolVersions(filePath: string) {
|
||||||
|
const module = await import("../../src/version/tool-versions-file");
|
||||||
|
return module.getPythonVersionFromToolVersions(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getUvVersionFromToolVersions", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for non-.tool-versions files", async () => {
|
||||||
|
const result = await getVersionFromToolVersions("package.json");
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(mockReadFileSync).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return version for valid uv entry", async () => {
|
||||||
|
const fileContent = "python 3.11.0\nuv 0.1.0\nnodejs 18.0.0";
|
||||||
|
mockReadFileSync.mockReturnValue(fileContent);
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBe("0.1.0");
|
||||||
|
expect(mockReadFileSync).toHaveBeenCalledWith(".tool-versions", "utf8");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return version for uv entry with v prefix", async () => {
|
||||||
|
const fileContent = "uv v0.2.0";
|
||||||
|
mockReadFileSync.mockReturnValue(fileContent);
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBe("0.2.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle whitespace around uv entry", async () => {
|
||||||
|
const fileContent = " uv 0.3.0 ";
|
||||||
|
mockReadFileSync.mockReturnValue(fileContent);
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBe("0.3.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip comments", async () => {
|
||||||
|
const fileContent = "# uv 0.1.0\npython 3.11.0\nuv 0.2.0 # inline comment";
|
||||||
|
mockReadFileSync.mockReturnValue(fileContent);
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBe("0.2.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return first matching uv version", async () => {
|
||||||
|
const fileContent = "uv 0.1.0\npython 3.11.0\nuv 0.2.0";
|
||||||
|
mockReadFileSync.mockReturnValue(fileContent);
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBe("0.1.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when no uv entry found", async () => {
|
||||||
|
const fileContent = "python 3.11.0\nnodejs 18.0.0";
|
||||||
|
mockReadFileSync.mockReturnValue(fileContent);
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for empty file", async () => {
|
||||||
|
mockReadFileSync.mockReturnValue("");
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn and return undefined for ref syntax", async () => {
|
||||||
|
const fileContent = "uv ref:main";
|
||||||
|
mockReadFileSync.mockReturnValue(fileContent);
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(mockWarning).toHaveBeenCalledWith(
|
||||||
|
"The ref syntax of .tool-versions is not supported. Please use a released version instead.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["/my/python/exploit", "my/exploited/uv", "C:\\exploited\\uv"])(
|
||||||
|
"should warn and return undefined for path %s",
|
||||||
|
async (version) => {
|
||||||
|
mockReadFileSync.mockReturnValue(`uv ${version}`);
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(mockWarning).toHaveBeenCalledWith(
|
||||||
|
`The uv version ${version} in .tool-versions is not supported. Paths are not allowed.`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should handle file path with .tool-versions extension", async () => {
|
||||||
|
const fileContent = "uv 0.1.0";
|
||||||
|
mockReadFileSync.mockReturnValue(fileContent);
|
||||||
|
|
||||||
|
const result = await getVersionFromToolVersions("path/to/.tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBe("0.1.0");
|
||||||
|
expect(mockReadFileSync).toHaveBeenCalledWith(
|
||||||
|
"path/to/.tool-versions",
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPythonVersionFromToolVersions", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return version for a valid Python entry", async () => {
|
||||||
|
mockReadFileSync.mockReturnValue(
|
||||||
|
"nodejs 24.0.0\r\npython v3.13.1t # use free-threaded Python\r\nuv 0.12.3",
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await getPythonVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBe("3.13.1t");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the first matching Python version", async () => {
|
||||||
|
mockReadFileSync.mockReturnValue("python 3.12\npython 3.13");
|
||||||
|
|
||||||
|
const result = await getPythonVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBe("3.12");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when no Python entry is found", async () => {
|
||||||
|
mockReadFileSync.mockReturnValue("uv 0.12.3\nnodejs 24.0.0");
|
||||||
|
|
||||||
|
const result = await getPythonVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn and return undefined for multiple Python versions", async () => {
|
||||||
|
mockReadFileSync.mockReturnValue("python 3.13 3.12 system");
|
||||||
|
|
||||||
|
const result = await getPythonVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(mockWarning).toHaveBeenCalledWith(
|
||||||
|
"Multiple Python versions in .tool-versions are not supported. The Python entry will be ignored.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"ref:main",
|
||||||
|
"path:~/src/python",
|
||||||
|
"system",
|
||||||
|
"/my/python/exploit",
|
||||||
|
"my/exploited/python",
|
||||||
|
"C:\\exploited\\python",
|
||||||
|
])("should warn and return undefined for %s", async (version) => {
|
||||||
|
mockReadFileSync.mockReturnValue(`python ${version}`);
|
||||||
|
|
||||||
|
const result = await getPythonVersionFromToolVersions(".tool-versions");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(mockWarning).toHaveBeenCalledWith(
|
||||||
|
`The Python version ${version} in .tool-versions is not supported. The Python entry will be ignored.`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for non-.tool-versions files", async () => {
|
||||||
|
const result = await getPythonVersionFromToolVersions(".python-version");
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(mockReadFileSync).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
36
__tests__/version/uv-lock-file.test.ts
Normal file
36
__tests__/version/uv-lock-file.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from "@jest/globals";
|
||||||
|
import { getUvVersionFromUvLockContent } from "../../src/version/uv-lock-file";
|
||||||
|
|
||||||
|
const UV_LOCK = `version = 1
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uv"
|
||||||
|
version = "0.8.17"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
`;
|
||||||
|
|
||||||
|
describe("getUvVersionFromUvLockContent", () => {
|
||||||
|
it("returns the exact uv version locked in uv.lock", () => {
|
||||||
|
expect(getUvVersionFromUvLockContent(UV_LOCK)).toBe("0.8.17");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when uv is not a locked package", () => {
|
||||||
|
const content = `version = 1
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.6.0"
|
||||||
|
`;
|
||||||
|
expect(getUvVersionFromUvLockContent(content)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when there are no packages", () => {
|
||||||
|
expect(getUvVersionFromUvLockContent("version = 1\n")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
143
__tests__/version/version-request-resolver.test.ts
Normal file
143
__tests__/version/version-request-resolver.test.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, describe, expect, it } from "@jest/globals";
|
||||||
|
import { resolveVersionRequest } from "../../src/version/version-request-resolver";
|
||||||
|
|
||||||
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
function createTempProject(files: Record<string, string> = {}): string {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "setup-uv-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", () => {
|
||||||
|
it("prefers explicit input over version-file and workspace config", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
".tool-versions": "uv 0.4.0\n",
|
||||||
|
"pyproject.toml": `[tool.uv]\nrequired-version = "==0.5.14"\n`,
|
||||||
|
"uv.toml": `required-version = "==0.5.15"\n`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({
|
||||||
|
version: "==0.6.0",
|
||||||
|
versionFile: path.join(workingDirectory, ".tool-versions"),
|
||||||
|
workingDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
source: "input",
|
||||||
|
specifier: "0.6.0",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses .tool-versions when it is passed via version-file", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
".tool-versions": "uv 0.5.15\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({
|
||||||
|
versionFile: path.join(workingDirectory, ".tool-versions"),
|
||||||
|
workingDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
format: ".tool-versions",
|
||||||
|
source: "version-file",
|
||||||
|
sourcePath: path.join(workingDirectory, ".tool-versions"),
|
||||||
|
specifier: "0.5.15",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the exact uv version locked in uv.lock when it is passed via version-file", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
"uv.lock": `version = 1\n\n[[package]]\nname = "uv"\nversion = "0.8.17"\nsource = { registry = "https://pypi.org/simple" }\n`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({
|
||||||
|
versionFile: path.join(workingDirectory, "uv.lock"),
|
||||||
|
workingDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
format: "uv.lock",
|
||||||
|
source: "version-file",
|
||||||
|
sourcePath: path.join(workingDirectory, "uv.lock"),
|
||||||
|
specifier: "0.8.17",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses requirements.txt when it is passed via version-file", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
"requirements.txt": "uv==0.6.17\nuvicorn==0.35.0\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({
|
||||||
|
versionFile: path.join(workingDirectory, "requirements.txt"),
|
||||||
|
workingDirectory,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
format: "requirements",
|
||||||
|
source: "version-file",
|
||||||
|
sourcePath: path.join(workingDirectory, "requirements.txt"),
|
||||||
|
specifier: "0.6.17",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers uv.toml over pyproject.toml during workspace discovery", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
"pyproject.toml": `[tool.uv]\nrequired-version = "==0.5.14"\n`,
|
||||||
|
"uv.toml": `required-version = "==0.5.15"\n`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({ workingDirectory });
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
format: "uv.toml",
|
||||||
|
source: "uv.toml",
|
||||||
|
sourcePath: path.join(workingDirectory, "uv.toml"),
|
||||||
|
specifier: "0.5.15",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to latest when no version source is found", () => {
|
||||||
|
const workingDirectory = createTempProject({});
|
||||||
|
|
||||||
|
const request = resolveVersionRequest({ workingDirectory });
|
||||||
|
|
||||||
|
expect(request).toEqual({
|
||||||
|
source: "default",
|
||||||
|
specifier: "latest",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when version-file does not resolve a version", () => {
|
||||||
|
const workingDirectory = createTempProject({
|
||||||
|
"requirements.txt": "uvicorn==0.35.0\n",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
resolveVersionRequest({
|
||||||
|
versionFile: path.join(workingDirectory, "requirements.txt"),
|
||||||
|
workingDirectory,
|
||||||
|
}),
|
||||||
|
).toThrow(
|
||||||
|
`Could not determine uv version from file: ${path.join(workingDirectory, "requirements.txt")}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
42
__tests__/workflows/workflow-run.yml
Normal file
42
__tests__/workflows/workflow-run.yml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: "test workflow_run caching"
|
||||||
|
|
||||||
|
on: # zizmor: ignore[dangerous-triggers] this workflow is a test fixture executed by act only
|
||||||
|
workflow_run:
|
||||||
|
workflows:
|
||||||
|
- test
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-cache-disabled:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Setup uv with automatic caching
|
||||||
|
id: setup-uv
|
||||||
|
uses: ./
|
||||||
|
- name: Verify automatic caching is disabled
|
||||||
|
env:
|
||||||
|
CACHE_KEY: ${{ steps.setup-uv.outputs.cache-key }}
|
||||||
|
run: |
|
||||||
|
if [ "$GITHUB_EVENT_NAME" != "workflow_run" ]; then
|
||||||
|
echo "Expected workflow_run event, got: $GITHUB_EVENT_NAME"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$RUNNER_ENVIRONMENT" != "github-hosted" ]; then
|
||||||
|
echo "Expected a simulated GitHub-hosted runner, got: $RUNNER_ENVIRONMENT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -n "$CACHE_KEY" ]; then
|
||||||
|
echo "Cache key should not be set for a workflow_run event: $CACHE_KEY"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -n "$UV_CACHE_DIR" ]; then
|
||||||
|
echo "UV_CACHE_DIR should not be set for a workflow_run event: $UV_CACHE_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
91
action-types.yml
Normal file
91
action-types.yml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# See https://github.com/typesafegithub/github-actions-typing
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
version-file:
|
||||||
|
type: string
|
||||||
|
python-version:
|
||||||
|
type: string
|
||||||
|
python-arch:
|
||||||
|
type: string
|
||||||
|
activate-environment:
|
||||||
|
type: boolean
|
||||||
|
venv-path:
|
||||||
|
type: string
|
||||||
|
no-project:
|
||||||
|
type: boolean
|
||||||
|
working-directory:
|
||||||
|
type: string
|
||||||
|
checksum:
|
||||||
|
type: string
|
||||||
|
github-token:
|
||||||
|
type: string
|
||||||
|
enable-cache:
|
||||||
|
type: enum
|
||||||
|
allowed-values:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
- auto
|
||||||
|
cache-dependency-glob:
|
||||||
|
type: list
|
||||||
|
separator: "\n"
|
||||||
|
list-item:
|
||||||
|
type: string
|
||||||
|
restore-cache:
|
||||||
|
type: boolean
|
||||||
|
save-cache:
|
||||||
|
type: enum
|
||||||
|
allowed-values:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
- auto
|
||||||
|
cache-suffix:
|
||||||
|
type: string
|
||||||
|
cache-local-path:
|
||||||
|
type: string
|
||||||
|
prune-cache:
|
||||||
|
type: boolean
|
||||||
|
cache-python:
|
||||||
|
type: boolean
|
||||||
|
ignore-nothing-to-cache:
|
||||||
|
type: boolean
|
||||||
|
ignore-empty-workdir:
|
||||||
|
type: boolean
|
||||||
|
tool-dir:
|
||||||
|
type: string
|
||||||
|
tool-bin-dir:
|
||||||
|
type: string
|
||||||
|
manifest-file:
|
||||||
|
type: string
|
||||||
|
download-from-astral-mirror:
|
||||||
|
type: boolean
|
||||||
|
add-problem-matchers:
|
||||||
|
type: boolean
|
||||||
|
quiet:
|
||||||
|
type: boolean
|
||||||
|
resolution-strategy:
|
||||||
|
type: enum
|
||||||
|
allowed-values:
|
||||||
|
- highest
|
||||||
|
- lowest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
uv-version:
|
||||||
|
type: string
|
||||||
|
uv-path:
|
||||||
|
type: string
|
||||||
|
uvx-path:
|
||||||
|
type: string
|
||||||
|
cache-hit:
|
||||||
|
type: boolean
|
||||||
|
cache-key:
|
||||||
|
type: string
|
||||||
|
venv:
|
||||||
|
type: string
|
||||||
|
python-version:
|
||||||
|
type: string
|
||||||
|
python-runtime-id:
|
||||||
|
type: string
|
||||||
|
python-cache-hit:
|
||||||
|
type: boolean
|
||||||
126
action.yml
126
action.yml
@@ -1,38 +1,124 @@
|
|||||||
name: 'Python setup uv'
|
name: "astral-sh/setup-uv"
|
||||||
description: 'Set up your GitHub Actions workflow with a specific version of uv'
|
description:
|
||||||
author: 'eifinger'
|
"Set up your GitHub Actions workflow with a specific version of uv."
|
||||||
|
author: "astral-sh"
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'The version of uv to install'
|
description: "The version of uv to install, e.g., `0.5.0`, `latest`, or `latest-known`. Defaults to the version in pyproject.toml or `latest`."
|
||||||
default: '0.3.3'
|
default: ""
|
||||||
|
version-file:
|
||||||
|
description: "Path to a file containing the version of uv to install, e.g., uv.toml, pyproject.toml, .tool-versions, requirements.txt or uv.lock. A selected .tool-versions file can also provide the Python version. Defaults to searching for uv.toml and if not found pyproject.toml."
|
||||||
|
default: ""
|
||||||
|
python-version:
|
||||||
|
description: "The version of Python to set UV_PYTHON to. Overrides the Python version from .tool-versions."
|
||||||
|
required: false
|
||||||
|
python-arch:
|
||||||
|
description: "The Python architecture to set UV_PYTHON_ARCH to, e.g., x86_64 or aarch64. Overrides UV_PYTHON_ARCH."
|
||||||
|
required: false
|
||||||
|
activate-environment:
|
||||||
|
description: "Use uv venv to activate a venv ready to be used by later steps. "
|
||||||
|
default: "false"
|
||||||
|
venv-path:
|
||||||
|
description: "Custom path for the virtual environment when using activate-environment. Defaults to '.venv' in the working directory."
|
||||||
|
default: ""
|
||||||
|
no-project:
|
||||||
|
description: "Pass --no-project when creating the venv with activate-environment."
|
||||||
|
default: "false"
|
||||||
|
working-directory:
|
||||||
|
description: "The directory to execute all commands in and look for files such as pyproject.toml"
|
||||||
|
default: ${{ github.workspace }}
|
||||||
checksum:
|
checksum:
|
||||||
description: 'The checksum of the uv version to install'
|
description: "The checksum of the uv version to install"
|
||||||
required: false
|
required: false
|
||||||
github-token:
|
github-token:
|
||||||
description: 'Used to increase the rate limit when retrieving versions and downloading uv.'
|
description:
|
||||||
|
"Used when downloading uv from GitHub releases."
|
||||||
required: false
|
required: false
|
||||||
|
default: ${{ github.token }}
|
||||||
enable-cache:
|
enable-cache:
|
||||||
description: 'Enable caching of the uv cache'
|
description: "Enable the GitHub Actions cache for uv. 'auto' enables caching on GitHub-hosted runners except for release, tag push, pull_request_target, and workflow_run events."
|
||||||
default: 'false'
|
default: "auto"
|
||||||
cache-dependency-glob:
|
cache-dependency-glob:
|
||||||
description: 'Glob pattern to match files relative to the repository root to control the cache. e.g. "uv.lock"'
|
description:
|
||||||
required: false
|
"Glob pattern to match files relative to the working directory to control
|
||||||
|
the cache."
|
||||||
|
default: |
|
||||||
|
**/*requirements*.txt
|
||||||
|
**/*requirements*.in
|
||||||
|
**/*constraints*.txt
|
||||||
|
**/*constraints*.in
|
||||||
|
**/pyproject.toml
|
||||||
|
**/uv.lock
|
||||||
|
**/*.py.lock
|
||||||
|
restore-cache:
|
||||||
|
description: "Whether to restore the cache if found."
|
||||||
|
default: "true"
|
||||||
|
save-cache:
|
||||||
|
description: "Whether to save the cache after the run. 'auto' disables saving for merge_group events."
|
||||||
|
default: "auto"
|
||||||
cache-suffix:
|
cache-suffix:
|
||||||
description: 'Suffix for the cache key'
|
description: "Suffix for the cache key"
|
||||||
required: false
|
required: false
|
||||||
cache-local-path:
|
cache-local-path:
|
||||||
description: 'Local path to store the cache.'
|
description: "Local path to store the cache."
|
||||||
default: '/tmp/setup-uv-cache'
|
default: ""
|
||||||
|
prune-cache:
|
||||||
|
description: "Prune cache before saving."
|
||||||
|
default: "false"
|
||||||
|
cache-python:
|
||||||
|
description: "Upload managed Python installations to the Github Actions cache."
|
||||||
|
default: "false"
|
||||||
|
ignore-nothing-to-cache:
|
||||||
|
description: "Ignore when nothing is found to cache."
|
||||||
|
default: "false"
|
||||||
|
ignore-empty-workdir:
|
||||||
|
description: "Ignore when the working directory is empty."
|
||||||
|
default: "false"
|
||||||
|
tool-dir:
|
||||||
|
description: "Custom path to set UV_TOOL_DIR to."
|
||||||
|
required: false
|
||||||
|
tool-bin-dir:
|
||||||
|
description: "Custom path to set UV_TOOL_BIN_DIR to."
|
||||||
|
required: false
|
||||||
|
manifest-file:
|
||||||
|
description: "URL to a custom manifest file in the astral-sh/versions format."
|
||||||
|
required: false
|
||||||
|
download-from-astral-mirror:
|
||||||
|
description: "Download uv from the Astral mirror instead of directly from GitHub Releases."
|
||||||
|
default: "true"
|
||||||
|
add-problem-matchers:
|
||||||
|
description: "Add problem matchers."
|
||||||
|
default: "true"
|
||||||
|
quiet:
|
||||||
|
description: "Suppress info-level log output. Only warnings and errors are shown."
|
||||||
|
default: "false"
|
||||||
|
resolution-strategy:
|
||||||
|
description: "Resolution strategy to use when resolving version ranges. 'highest' uses the latest compatible version, 'lowest' uses the oldest compatible version."
|
||||||
|
default: "highest"
|
||||||
outputs:
|
outputs:
|
||||||
uv-version:
|
uv-version:
|
||||||
description: "The installed uv version. Useful when using latest."
|
description: "The installed uv version. Useful when using latest."
|
||||||
|
uv-path:
|
||||||
|
description: "The path to the installed uv binary."
|
||||||
|
uvx-path:
|
||||||
|
description: "The path to the installed uvx binary."
|
||||||
cache-hit:
|
cache-hit:
|
||||||
description: "A boolean value to indicate a cache entry was found"
|
description: "A boolean value to indicate a cache entry was found"
|
||||||
|
cache-key:
|
||||||
|
description: "The cache key used for storing/restoring the cache"
|
||||||
|
venv:
|
||||||
|
description: "Path to the activated venv if activate-environment is true"
|
||||||
|
python-version:
|
||||||
|
description: "The Python version that was set."
|
||||||
|
python-runtime-id:
|
||||||
|
description: "An opaque identifier reported by uv for the activated venv's Python runtime. Empty when activate-environment is false."
|
||||||
|
python-cache-hit:
|
||||||
|
description: "A boolean value to indicate the Python cache entry was found"
|
||||||
runs:
|
runs:
|
||||||
using: 'node20'
|
using: "node24"
|
||||||
main: 'dist/setup/index.js'
|
main: "dist/setup/index.cjs"
|
||||||
post: 'dist/save-cache/index.js'
|
post: "dist/save-cache/index.cjs"
|
||||||
post-if: success()
|
post-if: success()
|
||||||
branding:
|
branding:
|
||||||
icon: 'package'
|
icon: "package"
|
||||||
color: 'blue'
|
color: "black"
|
||||||
|
|||||||
45
biome.json
Normal file
45
biome.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.5.9/schema.json",
|
||||||
|
"assist": {
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"organizeImports": "on",
|
||||||
|
"useSortedAttributes": "on",
|
||||||
|
"useSortedKeys": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false,
|
||||||
|
"includes": [
|
||||||
|
"**",
|
||||||
|
"!**/dist",
|
||||||
|
"!**/lib",
|
||||||
|
"!**/node_modules",
|
||||||
|
"!**/package*.json",
|
||||||
|
"!**/known-checksums.*"
|
||||||
|
],
|
||||||
|
"maxSize": 2097152
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "space"
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "double",
|
||||||
|
"trailingCommas": "all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"preset": "recommended"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vcs": {
|
||||||
|
"clientKind": "git",
|
||||||
|
"enabled": true,
|
||||||
|
"useIgnoreFile": false
|
||||||
|
}
|
||||||
|
}
|
||||||
64912
dist/save-cache/index.cjs
generated
vendored
Normal file
64912
dist/save-cache/index.cjs
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
84957
dist/save-cache/index.js
generated
vendored
84957
dist/save-cache/index.js
generated
vendored
File diff suppressed because one or more lines are too long
1
dist/save-cache/index.js.map
generated
vendored
1
dist/save-cache/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
1
dist/save-cache/sourcemap-register.js
generated
vendored
1
dist/save-cache/sourcemap-register.js
generated
vendored
File diff suppressed because one or more lines are too long
453
dist/setup/37.index.js
generated
vendored
453
dist/setup/37.index.js
generated
vendored
@@ -1,453 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
exports.id = 37;
|
|
||||||
exports.ids = [37];
|
|
||||||
exports.modules = {
|
|
||||||
|
|
||||||
/***/ 4037:
|
|
||||||
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
||||||
|
|
||||||
__webpack_require__.r(__webpack_exports__);
|
|
||||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
||||||
/* harmony export */ "toFormData": () => (/* binding */ toFormData)
|
|
||||||
/* harmony export */ });
|
|
||||||
/* harmony import */ var fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2777);
|
|
||||||
/* harmony import */ var formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8010);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let s = 0;
|
|
||||||
const S = {
|
|
||||||
START_BOUNDARY: s++,
|
|
||||||
HEADER_FIELD_START: s++,
|
|
||||||
HEADER_FIELD: s++,
|
|
||||||
HEADER_VALUE_START: s++,
|
|
||||||
HEADER_VALUE: s++,
|
|
||||||
HEADER_VALUE_ALMOST_DONE: s++,
|
|
||||||
HEADERS_ALMOST_DONE: s++,
|
|
||||||
PART_DATA_START: s++,
|
|
||||||
PART_DATA: s++,
|
|
||||||
END: s++
|
|
||||||
};
|
|
||||||
|
|
||||||
let f = 1;
|
|
||||||
const F = {
|
|
||||||
PART_BOUNDARY: f,
|
|
||||||
LAST_BOUNDARY: f *= 2
|
|
||||||
};
|
|
||||||
|
|
||||||
const LF = 10;
|
|
||||||
const CR = 13;
|
|
||||||
const SPACE = 32;
|
|
||||||
const HYPHEN = 45;
|
|
||||||
const COLON = 58;
|
|
||||||
const A = 97;
|
|
||||||
const Z = 122;
|
|
||||||
|
|
||||||
const lower = c => c | 0x20;
|
|
||||||
|
|
||||||
const noop = () => {};
|
|
||||||
|
|
||||||
class MultipartParser {
|
|
||||||
/**
|
|
||||||
* @param {string} boundary
|
|
||||||
*/
|
|
||||||
constructor(boundary) {
|
|
||||||
this.index = 0;
|
|
||||||
this.flags = 0;
|
|
||||||
|
|
||||||
this.onHeaderEnd = noop;
|
|
||||||
this.onHeaderField = noop;
|
|
||||||
this.onHeadersEnd = noop;
|
|
||||||
this.onHeaderValue = noop;
|
|
||||||
this.onPartBegin = noop;
|
|
||||||
this.onPartData = noop;
|
|
||||||
this.onPartEnd = noop;
|
|
||||||
|
|
||||||
this.boundaryChars = {};
|
|
||||||
|
|
||||||
boundary = '\r\n--' + boundary;
|
|
||||||
const ui8a = new Uint8Array(boundary.length);
|
|
||||||
for (let i = 0; i < boundary.length; i++) {
|
|
||||||
ui8a[i] = boundary.charCodeAt(i);
|
|
||||||
this.boundaryChars[ui8a[i]] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.boundary = ui8a;
|
|
||||||
this.lookbehind = new Uint8Array(this.boundary.length + 8);
|
|
||||||
this.state = S.START_BOUNDARY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Uint8Array} data
|
|
||||||
*/
|
|
||||||
write(data) {
|
|
||||||
let i = 0;
|
|
||||||
const length_ = data.length;
|
|
||||||
let previousIndex = this.index;
|
|
||||||
let {lookbehind, boundary, boundaryChars, index, state, flags} = this;
|
|
||||||
const boundaryLength = this.boundary.length;
|
|
||||||
const boundaryEnd = boundaryLength - 1;
|
|
||||||
const bufferLength = data.length;
|
|
||||||
let c;
|
|
||||||
let cl;
|
|
||||||
|
|
||||||
const mark = name => {
|
|
||||||
this[name + 'Mark'] = i;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clear = name => {
|
|
||||||
delete this[name + 'Mark'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const callback = (callbackSymbol, start, end, ui8a) => {
|
|
||||||
if (start === undefined || start !== end) {
|
|
||||||
this[callbackSymbol](ui8a && ui8a.subarray(start, end));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataCallback = (name, clear) => {
|
|
||||||
const markSymbol = name + 'Mark';
|
|
||||||
if (!(markSymbol in this)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clear) {
|
|
||||||
callback(name, this[markSymbol], i, data);
|
|
||||||
delete this[markSymbol];
|
|
||||||
} else {
|
|
||||||
callback(name, this[markSymbol], data.length, data);
|
|
||||||
this[markSymbol] = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (i = 0; i < length_; i++) {
|
|
||||||
c = data[i];
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case S.START_BOUNDARY:
|
|
||||||
if (index === boundary.length - 2) {
|
|
||||||
if (c === HYPHEN) {
|
|
||||||
flags |= F.LAST_BOUNDARY;
|
|
||||||
} else if (c !== CR) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
break;
|
|
||||||
} else if (index - 1 === boundary.length - 2) {
|
|
||||||
if (flags & F.LAST_BOUNDARY && c === HYPHEN) {
|
|
||||||
state = S.END;
|
|
||||||
flags = 0;
|
|
||||||
} else if (!(flags & F.LAST_BOUNDARY) && c === LF) {
|
|
||||||
index = 0;
|
|
||||||
callback('onPartBegin');
|
|
||||||
state = S.HEADER_FIELD_START;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c !== boundary[index + 2]) {
|
|
||||||
index = -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c === boundary[index + 2]) {
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case S.HEADER_FIELD_START:
|
|
||||||
state = S.HEADER_FIELD;
|
|
||||||
mark('onHeaderField');
|
|
||||||
index = 0;
|
|
||||||
// falls through
|
|
||||||
case S.HEADER_FIELD:
|
|
||||||
if (c === CR) {
|
|
||||||
clear('onHeaderField');
|
|
||||||
state = S.HEADERS_ALMOST_DONE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
if (c === HYPHEN) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c === COLON) {
|
|
||||||
if (index === 1) {
|
|
||||||
// empty header field
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dataCallback('onHeaderField', true);
|
|
||||||
state = S.HEADER_VALUE_START;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
cl = lower(c);
|
|
||||||
if (cl < A || cl > Z) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case S.HEADER_VALUE_START:
|
|
||||||
if (c === SPACE) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
mark('onHeaderValue');
|
|
||||||
state = S.HEADER_VALUE;
|
|
||||||
// falls through
|
|
||||||
case S.HEADER_VALUE:
|
|
||||||
if (c === CR) {
|
|
||||||
dataCallback('onHeaderValue', true);
|
|
||||||
callback('onHeaderEnd');
|
|
||||||
state = S.HEADER_VALUE_ALMOST_DONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case S.HEADER_VALUE_ALMOST_DONE:
|
|
||||||
if (c !== LF) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = S.HEADER_FIELD_START;
|
|
||||||
break;
|
|
||||||
case S.HEADERS_ALMOST_DONE:
|
|
||||||
if (c !== LF) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback('onHeadersEnd');
|
|
||||||
state = S.PART_DATA_START;
|
|
||||||
break;
|
|
||||||
case S.PART_DATA_START:
|
|
||||||
state = S.PART_DATA;
|
|
||||||
mark('onPartData');
|
|
||||||
// falls through
|
|
||||||
case S.PART_DATA:
|
|
||||||
previousIndex = index;
|
|
||||||
|
|
||||||
if (index === 0) {
|
|
||||||
// boyer-moore derrived algorithm to safely skip non-boundary data
|
|
||||||
i += boundaryEnd;
|
|
||||||
while (i < bufferLength && !(data[i] in boundaryChars)) {
|
|
||||||
i += boundaryLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
i -= boundaryEnd;
|
|
||||||
c = data[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < boundary.length) {
|
|
||||||
if (boundary[index] === c) {
|
|
||||||
if (index === 0) {
|
|
||||||
dataCallback('onPartData', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
} else {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
} else if (index === boundary.length) {
|
|
||||||
index++;
|
|
||||||
if (c === CR) {
|
|
||||||
// CR = part boundary
|
|
||||||
flags |= F.PART_BOUNDARY;
|
|
||||||
} else if (c === HYPHEN) {
|
|
||||||
// HYPHEN = end boundary
|
|
||||||
flags |= F.LAST_BOUNDARY;
|
|
||||||
} else {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
} else if (index - 1 === boundary.length) {
|
|
||||||
if (flags & F.PART_BOUNDARY) {
|
|
||||||
index = 0;
|
|
||||||
if (c === LF) {
|
|
||||||
// unset the PART_BOUNDARY flag
|
|
||||||
flags &= ~F.PART_BOUNDARY;
|
|
||||||
callback('onPartEnd');
|
|
||||||
callback('onPartBegin');
|
|
||||||
state = S.HEADER_FIELD_START;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (flags & F.LAST_BOUNDARY) {
|
|
||||||
if (c === HYPHEN) {
|
|
||||||
callback('onPartEnd');
|
|
||||||
state = S.END;
|
|
||||||
flags = 0;
|
|
||||||
} else {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index > 0) {
|
|
||||||
// when matching a possible boundary, keep a lookbehind reference
|
|
||||||
// in case it turns out to be a false lead
|
|
||||||
lookbehind[index - 1] = c;
|
|
||||||
} else if (previousIndex > 0) {
|
|
||||||
// if our boundary turned out to be rubbish, the captured lookbehind
|
|
||||||
// belongs to partData
|
|
||||||
const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength);
|
|
||||||
callback('onPartData', 0, previousIndex, _lookbehind);
|
|
||||||
previousIndex = 0;
|
|
||||||
mark('onPartData');
|
|
||||||
|
|
||||||
// reconsider the current character even so it interrupted the sequence
|
|
||||||
// it could be the beginning of a new sequence
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case S.END:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unexpected state entered: ${state}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataCallback('onHeaderField');
|
|
||||||
dataCallback('onHeaderValue');
|
|
||||||
dataCallback('onPartData');
|
|
||||||
|
|
||||||
// Update properties for the next call
|
|
||||||
this.index = index;
|
|
||||||
this.state = state;
|
|
||||||
this.flags = flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
end() {
|
|
||||||
if ((this.state === S.HEADER_FIELD_START && this.index === 0) ||
|
|
||||||
(this.state === S.PART_DATA && this.index === this.boundary.length)) {
|
|
||||||
this.onPartEnd();
|
|
||||||
} else if (this.state !== S.END) {
|
|
||||||
throw new Error('MultipartParser.end(): stream ended unexpectedly');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _fileName(headerValue) {
|
|
||||||
// matches either a quoted-string or a token (RFC 2616 section 19.5.1)
|
|
||||||
const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i);
|
|
||||||
if (!m) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = m[2] || m[3] || '';
|
|
||||||
let filename = match.slice(match.lastIndexOf('\\') + 1);
|
|
||||||
filename = filename.replace(/%22/g, '"');
|
|
||||||
filename = filename.replace(/&#(\d{4});/g, (m, code) => {
|
|
||||||
return String.fromCharCode(code);
|
|
||||||
});
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toFormData(Body, ct) {
|
|
||||||
if (!/multipart/i.test(ct)) {
|
|
||||||
throw new TypeError('Failed to fetch');
|
|
||||||
}
|
|
||||||
|
|
||||||
const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
|
||||||
|
|
||||||
if (!m) {
|
|
||||||
throw new TypeError('no or bad content-type header, no multipart boundary');
|
|
||||||
}
|
|
||||||
|
|
||||||
const parser = new MultipartParser(m[1] || m[2]);
|
|
||||||
|
|
||||||
let headerField;
|
|
||||||
let headerValue;
|
|
||||||
let entryValue;
|
|
||||||
let entryName;
|
|
||||||
let contentType;
|
|
||||||
let filename;
|
|
||||||
const entryChunks = [];
|
|
||||||
const formData = new formdata_polyfill_esm_min_js__WEBPACK_IMPORTED_MODULE_1__/* .FormData */ .Ct();
|
|
||||||
|
|
||||||
const onPartData = ui8a => {
|
|
||||||
entryValue += decoder.decode(ui8a, {stream: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
const appendToFile = ui8a => {
|
|
||||||
entryChunks.push(ui8a);
|
|
||||||
};
|
|
||||||
|
|
||||||
const appendFileToFormData = () => {
|
|
||||||
const file = new fetch_blob_from_js__WEBPACK_IMPORTED_MODULE_0__/* .File */ .$B(entryChunks, filename, {type: contentType});
|
|
||||||
formData.append(entryName, file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const appendEntryToFormData = () => {
|
|
||||||
formData.append(entryName, entryValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
decoder.decode();
|
|
||||||
|
|
||||||
parser.onPartBegin = function () {
|
|
||||||
parser.onPartData = onPartData;
|
|
||||||
parser.onPartEnd = appendEntryToFormData;
|
|
||||||
|
|
||||||
headerField = '';
|
|
||||||
headerValue = '';
|
|
||||||
entryValue = '';
|
|
||||||
entryName = '';
|
|
||||||
contentType = '';
|
|
||||||
filename = null;
|
|
||||||
entryChunks.length = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
parser.onHeaderField = function (ui8a) {
|
|
||||||
headerField += decoder.decode(ui8a, {stream: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
parser.onHeaderValue = function (ui8a) {
|
|
||||||
headerValue += decoder.decode(ui8a, {stream: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
parser.onHeaderEnd = function () {
|
|
||||||
headerValue += decoder.decode();
|
|
||||||
headerField = headerField.toLowerCase();
|
|
||||||
|
|
||||||
if (headerField === 'content-disposition') {
|
|
||||||
// matches either a quoted-string or a token (RFC 2616 section 19.5.1)
|
|
||||||
const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i);
|
|
||||||
|
|
||||||
if (m) {
|
|
||||||
entryName = m[2] || m[3] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
filename = _fileName(headerValue);
|
|
||||||
|
|
||||||
if (filename) {
|
|
||||||
parser.onPartData = appendToFile;
|
|
||||||
parser.onPartEnd = appendFileToFormData;
|
|
||||||
}
|
|
||||||
} else if (headerField === 'content-type') {
|
|
||||||
contentType = headerValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
headerValue = '';
|
|
||||||
headerField = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
for await (const chunk of Body) {
|
|
||||||
parser.write(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
parser.end();
|
|
||||||
|
|
||||||
return formData;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/***/ })
|
|
||||||
|
|
||||||
};
|
|
||||||
;
|
|
||||||
//# sourceMappingURL=37.index.js.map
|
|
||||||
1
dist/setup/37.index.js.map
generated
vendored
1
dist/setup/37.index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
102609
dist/setup/index.cjs
generated
vendored
Normal file
102609
dist/setup/index.cjs
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
87697
dist/setup/index.js
generated
vendored
87697
dist/setup/index.js
generated
vendored
File diff suppressed because one or more lines are too long
1
dist/setup/index.js.map
generated
vendored
1
dist/setup/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
1056
dist/setup/licenses.txt
generated
vendored
1056
dist/setup/licenses.txt
generated
vendored
File diff suppressed because it is too large
Load Diff
1
dist/setup/sourcemap-register.js
generated
vendored
1
dist/setup/sourcemap-register.js
generated
vendored
File diff suppressed because one or more lines are too long
35031
dist/update-checksums/index.js
generated
vendored
35031
dist/update-checksums/index.js
generated
vendored
File diff suppressed because one or more lines are too long
1
dist/update-checksums/index.js.map
generated
vendored
1
dist/update-checksums/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
1
dist/update-checksums/sourcemap-register.js
generated
vendored
1
dist/update-checksums/sourcemap-register.js
generated
vendored
File diff suppressed because one or more lines are too long
34791
dist/update-default-version/index.js
generated
vendored
34791
dist/update-default-version/index.js
generated
vendored
File diff suppressed because one or more lines are too long
1
dist/update-default-version/index.js.map
generated
vendored
1
dist/update-default-version/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
1
dist/update-default-version/sourcemap-register.js
generated
vendored
1
dist/update-default-version/sourcemap-register.js
generated
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user