Compare commits
117 Commits
server-202
...
server-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a20f813df | ||
|
|
3b465533fd | ||
|
|
df719ea2db | ||
|
|
9ee8ee8cbf | ||
|
|
def3007602 | ||
|
|
7d7f70b4b5 | ||
|
|
2ded4aa7b6 | ||
|
|
da2745d523 | ||
|
|
1338bf3192 | ||
|
|
e8e253d21e | ||
|
|
60aa530966 | ||
|
|
9447077c08 | ||
|
|
b60d738999 | ||
|
|
85fb206c8b | ||
|
|
d1210e2311 | ||
|
|
85dd5a599f | ||
|
|
e108e40930 | ||
|
|
eaa4317039 | ||
|
|
5cdef88bcc | ||
|
|
4132ca2e7e | ||
|
|
e1541acc11 | ||
|
|
f5dd749ae0 | ||
|
|
ef17850f7e | ||
|
|
5bde4266c8 | ||
|
|
e57edb42bd | ||
|
|
69f9251e1c | ||
|
|
39d3fd332b | ||
|
|
58f5b99fea | ||
|
|
20959b15db | ||
|
|
cbb7ab5e8b | ||
|
|
2bd926e65f | ||
|
|
04638ab0ee | ||
|
|
4d203e1937 | ||
|
|
6219c6da82 | ||
|
|
a16cf24b52 | ||
|
|
d3a1ef2ff7 | ||
|
|
32b1e341d7 | ||
|
|
2d5b72b207 | ||
|
|
0c4fed4dc6 | ||
|
|
8af909d118 | ||
|
|
00d72da97e | ||
|
|
4ec62fa445 | ||
|
|
57520a974f | ||
|
|
8c7872a666 | ||
|
|
ad82f7647a | ||
|
|
43940aeeae | ||
|
|
d05172557d | ||
|
|
4aa9aa339e | ||
|
|
91e4f4a78a | ||
|
|
383812e160 | ||
|
|
20ab5255cd | ||
|
|
20ae47e5ff | ||
|
|
62430392f8 | ||
|
|
cc90c190f2 | ||
|
|
48e25771ee | ||
|
|
fe2d2fbb44 | ||
|
|
d87673f6a8 | ||
|
|
ffee2e103c | ||
|
|
bbc8c2035f | ||
|
|
caa3ffef39 | ||
|
|
d37d6c8aab | ||
|
|
51de256952 | ||
|
|
3c7f38861a | ||
|
|
85e4ee6b2b | ||
|
|
5e40080099 | ||
|
|
28bee8681a | ||
|
|
17a50b81e4 | ||
|
|
d186f7cfb5 | ||
|
|
8ba3a74aa2 | ||
|
|
9d22508df1 | ||
|
|
d79879cd75 | ||
|
|
4e4e3f82c6 | ||
|
|
04f4fbd156 | ||
|
|
66631524c5 | ||
|
|
e7d76b117e | ||
|
|
868643ee6c | ||
|
|
0a57af28bd | ||
|
|
1d2bf19100 | ||
|
|
d502e7031f | ||
|
|
a8a1e77433 | ||
|
|
d00c4de4a3 | ||
|
|
9ab1a906b2 | ||
|
|
5580b1cd5c | ||
|
|
f28578f4e8 | ||
|
|
68e5eb6b8b | ||
|
|
58ea5aead3 | ||
|
|
ba6a24e84c | ||
|
|
2170a022f0 | ||
|
|
7d0fd4a07a | ||
|
|
64da4d07c2 | ||
|
|
4c31202935 | ||
|
|
8ed3dc8db4 | ||
|
|
f767fabf43 | ||
|
|
e3808c1738 | ||
|
|
f3e0cc06f4 | ||
|
|
b85cfc7c1e | ||
|
|
b8abface20 | ||
|
|
c095e5c855 | ||
|
|
b0fa13ba2d | ||
|
|
814311e488 | ||
|
|
ae8414a6f0 | ||
|
|
ee86905883 | ||
|
|
f8404935e9 | ||
|
|
114ebe0bb1 | ||
|
|
2515cf9b27 | ||
|
|
da6002099f | ||
|
|
0f14eed035 | ||
|
|
2e6b1b8909 | ||
|
|
808c6774ca | ||
|
|
5902dfc1cf | ||
|
|
fdce66f470 | ||
|
|
d6df7f8edb | ||
|
|
6eff6a7110 | ||
|
|
a92c2f1e64 | ||
|
|
a9ecbb064e | ||
|
|
b9955143db | ||
|
|
d07dd65dfe |
33
.github/actions/docusaurus-swizzled-warning/package-lock.json
generated
vendored
33
.github/actions/docusaurus-swizzled-warning/package-lock.json
generated
vendored
@@ -9,17 +9,25 @@
|
||||
"version": "0.0.0",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/core": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz",
|
||||
"integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==",
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
|
||||
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
|
||||
"dependencies": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/http-client": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/exec": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
|
||||
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
|
||||
"dependencies": {
|
||||
"@actions/io": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/github": {
|
||||
@@ -42,6 +50,11 @@
|
||||
"undici": "^5.25.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@actions/io": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
|
||||
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="
|
||||
},
|
||||
"node_modules/@fastify/busboy": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz",
|
||||
@@ -220,14 +233,6 @@
|
||||
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
|
||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"author": "jNullj",
|
||||
"license": "CC0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -4,8 +4,42 @@ Note: this changelog is for the shields.io server. The changelog for the badge-m
|
||||
|
||||
---
|
||||
|
||||
## server-2024-12-01
|
||||
|
||||
- add [WingetVersion] Badge [#10245](https://github.com/badges/shields/issues/10245)
|
||||
- Fix broken URL for pingpong.one [#10655](https://github.com/badges/shields/issues/10655)
|
||||
- [npm] - Last update badge added [#10641](https://github.com/badges/shields/issues/10641)
|
||||
- reduce overhead of NPM Last Update badge; test [npm] [#10666](https://github.com/badges/shields/issues/10666)
|
||||
- Add YouTube-specific privacy notes [#10646](https://github.com/badges/shields/issues/10646)
|
||||
- Dependency updates
|
||||
|
||||
## server-2024-11-02
|
||||
|
||||
- cleanly handle null or undefined result from jsonpath-plus [#10645](https://github.com/badges/shields/issues/10645)
|
||||
- add content security policy header to SVG responses [#10642](https://github.com/badges/shields/issues/10642)
|
||||
- [Scoop] Added scoop-license badge. [#10627](https://github.com/badges/shields/issues/10627)
|
||||
- [Chromewebstore] Extension size & last updated [#10613](https://github.com/badges/shields/issues/10613)
|
||||
- Deprecate HackageDeps service [#10618](https://github.com/badges/shields/issues/10618)
|
||||
- Add [CratesUserDownloads] service and tester [#10619](https://github.com/badges/shields/issues/10619)
|
||||
- [Snapcraft] - Added snapcraft last update badge [#10610](https://github.com/badges/shields/issues/10610)
|
||||
- [GitHubHacktoberfest] 2024 support [#10612](https://github.com/badges/shields/issues/10612)
|
||||
- add [homebrew] cask download badge [#10595](https://github.com/badges/shields/issues/10595)
|
||||
- remove prefix v for commit hash version [#10597](https://github.com/badges/shields/issues/10597)
|
||||
- [Maven] Added badge for Maven-Cenral last-update (#10301) [#10585](https://github.com/badges/shields/issues/10585)
|
||||
- [DynamicXml] parse doc as html if served with text/html content type [#10607](https://github.com/badges/shields/issues/10607)
|
||||
- Revert "Use old.stats.jenkins.io for JSON data (#10522)" [#10537](https://github.com/badges/shields/issues/10537)
|
||||
- catch queries that cause TypeError [#10556](https://github.com/badges/shields/issues/10556)
|
||||
- Dependency updates
|
||||
|
||||
## server-2024-09-25
|
||||
|
||||
This release includes an important security fix. See
|
||||
|
||||
- https://github.com/badges/shields/security/advisories/GHSA-rxvx-x284-4445
|
||||
- https://github.com/badges/shields/issues/10553
|
||||
|
||||
for more details
|
||||
|
||||
- [dynamicjson dynamicyaml dynamictoml] switch to jsonpath-plus [#10551](https://github.com/badges/shields/issues/10551)
|
||||
- [Snapcraft] license [#10520](https://github.com/badges/shields/issues/10520)
|
||||
- deprecate [wheelmap] service [#10538](https://github.com/badges/shields/issues/10538)
|
||||
|
||||
14
README.md
14
README.md
@@ -198,25 +198,19 @@ You can read more about [the project's inception][thread],
|
||||
|
||||
Maintainers:
|
||||
|
||||
- [calebcartwright](https://github.com/calebcartwright) (core team)
|
||||
- [chris48s](https://github.com/chris48s) (core team)
|
||||
- [Daniel15](https://github.com/Daniel15) (core team)
|
||||
- [paulmelnikow](https://github.com/paulmelnikow) (core team)
|
||||
- [platan](https://github.com/platan) (core team)
|
||||
- [PyvesB](https://github.com/PyvesB) (core team)
|
||||
- [RedSparr0w](https://github.com/RedSparr0w) (core team)
|
||||
|
||||
Operations:
|
||||
|
||||
- [calebcartwright](https://github.com/calebcartwright)
|
||||
- [chris48s](https://github.com/chris48s)
|
||||
- [jNullj](https://github.com/jnullj)
|
||||
- [paulmelnikow](https://github.com/paulmelnikow)
|
||||
- [PyvesB](https://github.com/PyvesB)
|
||||
|
||||
Alumni:
|
||||
|
||||
- [Daniel15](https://github.com/Daniel15)
|
||||
- [espadrine](https://github.com/espadrine)
|
||||
- [olivierlacan](https://github.com/olivierlacan)
|
||||
- [platan](https://github.com/platan)
|
||||
- [RedSparr0w](https://github.com/RedSparr0w)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -2229,3 +2229,325 @@ exports['The badge generator badges with logos should always produce the same ba
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator "flat" template badge generation should match snapshots: message with custom suffix 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="107"
|
||||
height="20"
|
||||
role="img"
|
||||
aria-label="cactus: grown"
|
||||
>
|
||||
<title>cactus: grown</title>
|
||||
<linearGradient id="s1" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<clipPath id="r1">
|
||||
<rect width="107" height="20" rx="3" fill="#fff" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#r1)">
|
||||
<rect width="62" height="20" fill="#0f0" />
|
||||
<rect x="62" width="45" height="20" fill="#b3e" />
|
||||
<rect width="107" height="20" fill="url(#s1)" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="110"
|
||||
>
|
||||
<image
|
||||
x="5"
|
||||
y="3"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="405"
|
||||
y="150"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="350"
|
||||
>
|
||||
cactus
|
||||
</text>
|
||||
<text x="405" y="140" transform="scale(.1)" fill="#fff" textLength="350">
|
||||
cactus
|
||||
</text>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="835"
|
||||
y="150"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="350"
|
||||
>
|
||||
grown
|
||||
</text>
|
||||
<text x="835" y="140" transform="scale(.1)" fill="#fff" textLength="350">
|
||||
grown
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator "flat-square" template badge generation should match snapshots: message with custom suffix 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="107"
|
||||
height="20"
|
||||
role="img"
|
||||
aria-label="cactus: grown"
|
||||
>
|
||||
<title>cactus: grown</title>
|
||||
<g shape-rendering="crispEdges">
|
||||
<rect width="62" height="20" fill="#0f0" />
|
||||
<rect x="62" width="45" height="20" fill="#b3e" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="110"
|
||||
>
|
||||
<image
|
||||
x="5"
|
||||
y="3"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
<text x="405" y="140" transform="scale(.1)" fill="#fff" textLength="350">
|
||||
cactus
|
||||
</text>
|
||||
<text x="835" y="140" transform="scale(.1)" fill="#fff" textLength="350">
|
||||
grown
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator "plastic" template badge generation should match snapshots: message with custom suffix 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="107"
|
||||
height="18"
|
||||
role="img"
|
||||
aria-label="cactus: grown"
|
||||
>
|
||||
<title>cactus: grown</title>
|
||||
<linearGradient id="s1" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".7" />
|
||||
<stop offset=".1" stop-color="#aaa" stop-opacity=".1" />
|
||||
<stop offset=".9" stop-color="#000" stop-opacity=".3" />
|
||||
<stop offset="1" stop-color="#000" stop-opacity=".5" />
|
||||
</linearGradient>
|
||||
<clipPath id="r1">
|
||||
<rect width="107" height="18" rx="4" fill="#fff" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#r1)">
|
||||
<rect width="62" height="18" fill="#0f0" />
|
||||
<rect x="62" width="45" height="18" fill="#b3e" />
|
||||
<rect width="107" height="18" fill="url(#s1)" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="110"
|
||||
>
|
||||
<image
|
||||
x="5"
|
||||
y="2"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="405"
|
||||
y="140"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="350"
|
||||
>
|
||||
cactus
|
||||
</text>
|
||||
<text x="405" y="130" transform="scale(.1)" fill="#fff" textLength="350">
|
||||
cactus
|
||||
</text>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="835"
|
||||
y="140"
|
||||
fill="#010101"
|
||||
fill-opacity=".3"
|
||||
transform="scale(.1)"
|
||||
textLength="350"
|
||||
>
|
||||
grown
|
||||
</text>
|
||||
<text x="835" y="130" transform="scale(.1)" fill="#fff" textLength="350">
|
||||
grown
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator "for-the-badge" template badge generation should match snapshots: message with custom suffix 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="163.75"
|
||||
height="28"
|
||||
role="img"
|
||||
aria-label="CACTUS: GROWN"
|
||||
>
|
||||
<title>CACTUS: GROWN</title>
|
||||
<g shape-rendering="crispEdges">
|
||||
<rect width="89.5" height="28" fill="#0f0" />
|
||||
<rect x="89.5" width="74.25" height="28" fill="#b3e" />
|
||||
</g>
|
||||
<g
|
||||
fill="#fff"
|
||||
text-anchor="middle"
|
||||
font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-size="100"
|
||||
>
|
||||
<image
|
||||
x="9"
|
||||
y="7"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
<text transform="scale(.1)" x="532.5" y="175" textLength="485" fill="#fff">
|
||||
CACTUS
|
||||
</text>
|
||||
<text
|
||||
transform="scale(.1)"
|
||||
x="1266.25"
|
||||
y="175"
|
||||
textLength="502.5"
|
||||
fill="#fff"
|
||||
font-weight="bold"
|
||||
>
|
||||
GROWN
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
exports['The badge generator "social" template badge generation should match snapshots: message with custom suffix 1'] = `
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="112"
|
||||
height="20"
|
||||
role="img"
|
||||
aria-label="Cactus: grown"
|
||||
>
|
||||
<title>Cactus: grown</title>
|
||||
<style>
|
||||
a:hover #llink1 {
|
||||
fill: url(#b1);
|
||||
stroke: #ccc;
|
||||
}
|
||||
a:hover #rlink1 {
|
||||
fill: #4183c4;
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="a1" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#fcfcfc" stop-opacity="0" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="b1" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#ccc" stop-opacity=".1" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<g stroke="#d5d5d5">
|
||||
<rect
|
||||
stroke="none"
|
||||
fill="#fcfcfc"
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="64"
|
||||
height="19"
|
||||
rx="2"
|
||||
/>
|
||||
<rect x="70.5" y="0.5" width="41" height="19" rx="2" fill="#fafafa" />
|
||||
<rect x="70" y="7.5" width="0.5" height="5" stroke="#fafafa" />
|
||||
<path d="M70.5 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa" />
|
||||
</g>
|
||||
<image
|
||||
x="5"
|
||||
y="3"
|
||||
width="14"
|
||||
height="14"
|
||||
xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxu"
|
||||
/>
|
||||
<g
|
||||
aria-hidden="true"
|
||||
fill="#333"
|
||||
text-anchor="middle"
|
||||
font-family="Helvetica Neue,Helvetica,Arial,sans-serif"
|
||||
text-rendering="geometricPrecision"
|
||||
font-weight="700"
|
||||
font-size="110px"
|
||||
line-height="14px"
|
||||
>
|
||||
<rect
|
||||
id="llink1"
|
||||
stroke="#d5d5d5"
|
||||
fill="url(#a1)"
|
||||
x=".5"
|
||||
y=".5"
|
||||
width="64"
|
||||
height="19"
|
||||
rx="2"
|
||||
/>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="405"
|
||||
y="150"
|
||||
fill="#fff"
|
||||
transform="scale(.1)"
|
||||
textLength="370"
|
||||
>
|
||||
Cactus
|
||||
</text>
|
||||
<text x="405" y="140" transform="scale(.1)" textLength="370">Cactus</text>
|
||||
<text
|
||||
aria-hidden="true"
|
||||
x="905"
|
||||
y="150"
|
||||
fill="#fff"
|
||||
transform="scale(.1)"
|
||||
textLength="330"
|
||||
>
|
||||
grown
|
||||
</text>
|
||||
<text id="rlink1" x="905" y="140" transform="scale(.1)" textLength="330">
|
||||
grown
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
`
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 4.1.0
|
||||
|
||||
### Features
|
||||
|
||||
- Add `idSuffix` param. This can be used to ensure every element id within the SVG is unique
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -73,6 +73,11 @@ The format is the following:
|
||||
// (Optional) One of: 'plastic', 'flat', 'flat-square', 'for-the-badge' or 'social'
|
||||
// Each offers a different visual design.
|
||||
style: 'flat',
|
||||
|
||||
// (Optional) A string with only letters, numbers, -, and _. This can be used
|
||||
// to ensure every element id within the SVG is unique and prevent CSS
|
||||
// cross-contamination when the SVG badge is rendered inline in HTML pages.
|
||||
idSuffix: 'dd'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
1
badge-maker/index.d.ts
vendored
1
badge-maker/index.d.ts
vendored
@@ -6,6 +6,7 @@ interface Format {
|
||||
style?: 'plastic' | 'flat' | 'flat-square' | 'for-the-badge' | 'social'
|
||||
logoBase64?: string
|
||||
links?: Array<string>
|
||||
idSuffix?: string
|
||||
}
|
||||
|
||||
export declare class ValidationError extends Error {}
|
||||
|
||||
@@ -128,6 +128,7 @@ class Badge {
|
||||
logoPadding,
|
||||
color = '#4c1',
|
||||
labelColor,
|
||||
idSuffix = '',
|
||||
}) {
|
||||
const horizPadding = 5
|
||||
const hasLogo = !!logo
|
||||
@@ -178,6 +179,7 @@ class Badge {
|
||||
this.label = label
|
||||
this.message = message
|
||||
this.accessibleText = accessibleText
|
||||
this.idSuffix = idSuffix
|
||||
|
||||
this.logoElement = getLogoElement({
|
||||
logo,
|
||||
@@ -286,7 +288,7 @@ class Badge {
|
||||
},
|
||||
}),
|
||||
],
|
||||
attrs: { id: 'r' },
|
||||
attrs: { id: `r${this.idSuffix}` },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -313,7 +315,7 @@ class Badge {
|
||||
attrs: {
|
||||
width: this.width,
|
||||
height: this.constructor.height,
|
||||
fill: 'url(#s)',
|
||||
fill: `url(#s${this.idSuffix})`,
|
||||
},
|
||||
})
|
||||
const content = withGradient
|
||||
@@ -379,14 +381,14 @@ class Plastic extends Badge {
|
||||
attrs: { offset: 1, 'stop-color': '#000', 'stop-opacity': '.5' },
|
||||
}),
|
||||
],
|
||||
attrs: { id: 's', x2: 0, y2: '100%' },
|
||||
attrs: { id: `s${this.idSuffix}`, x2: 0, y2: '100%' },
|
||||
})
|
||||
|
||||
const clipPath = this.getClipPathElement(4)
|
||||
|
||||
const backgroundGroup = this.getBackgroundGroupElement({
|
||||
withGradient: true,
|
||||
attrs: { 'clip-path': 'url(#r)' },
|
||||
attrs: { 'clip-path': `url(#r${this.idSuffix})` },
|
||||
})
|
||||
|
||||
return renderBadge(
|
||||
@@ -428,14 +430,14 @@ class Flat extends Badge {
|
||||
attrs: { offset: 1, 'stop-opacity': '.1' },
|
||||
}),
|
||||
],
|
||||
attrs: { id: 's', x2: 0, y2: '100%' },
|
||||
attrs: { id: `s${this.idSuffix}`, x2: 0, y2: '100%' },
|
||||
})
|
||||
|
||||
const clipPath = this.getClipPathElement(3)
|
||||
|
||||
const backgroundGroup = this.getBackgroundGroupElement({
|
||||
withGradient: true,
|
||||
attrs: { 'clip-path': 'url(#r)' },
|
||||
attrs: { 'clip-path': `url(#r${this.idSuffix})` },
|
||||
})
|
||||
|
||||
return renderBadge(
|
||||
@@ -492,6 +494,7 @@ function social({
|
||||
logoPadding,
|
||||
color = '#4c1',
|
||||
labelColor = '#555',
|
||||
idSuffix = '',
|
||||
}) {
|
||||
// Social label is styled with a leading capital. Convert to caps here so
|
||||
// width can be measured using the correct characters.
|
||||
@@ -565,9 +568,9 @@ function social({
|
||||
const rect = new XmlElement({
|
||||
name: 'rect',
|
||||
attrs: {
|
||||
id: 'llink',
|
||||
id: `llink${idSuffix}`,
|
||||
stroke: '#d5d5d5',
|
||||
fill: 'url(#a)',
|
||||
fill: `url(#a${idSuffix})`,
|
||||
x: '.5',
|
||||
y: '.5',
|
||||
width: labelRectWidth,
|
||||
@@ -640,7 +643,7 @@ function social({
|
||||
name: 'text',
|
||||
content: [message],
|
||||
attrs: {
|
||||
id: 'rlink',
|
||||
id: `rlink${idSuffix}`,
|
||||
x: messageTextX,
|
||||
y: 140,
|
||||
transform: FONT_SCALE_DOWN_VALUE,
|
||||
@@ -660,7 +663,7 @@ function social({
|
||||
const style = new XmlElement({
|
||||
name: 'style',
|
||||
content: [
|
||||
'a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{fill:#4183c4}',
|
||||
`a:hover #llink${idSuffix}{fill:url(#b${idSuffix});stroke:#ccc}a:hover #rlink${idSuffix}{fill:#4183c4}`,
|
||||
],
|
||||
})
|
||||
const gradients = new ElementList({
|
||||
@@ -681,7 +684,7 @@ function social({
|
||||
attrs: { offset: 1, 'stop-opacity': '.1' },
|
||||
}),
|
||||
],
|
||||
attrs: { id: 'a', x2: 0, y2: '100%' },
|
||||
attrs: { id: `a${idSuffix}`, x2: 0, y2: '100%' },
|
||||
}),
|
||||
new XmlElement({
|
||||
name: 'linearGradient',
|
||||
@@ -695,7 +698,7 @@ function social({
|
||||
attrs: { offset: 1, 'stop-opacity': '.1' },
|
||||
}),
|
||||
],
|
||||
attrs: { id: 'b', x2: 0, y2: '100%' },
|
||||
attrs: { id: `b${idSuffix}`, x2: 0, y2: '100%' },
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -52,6 +52,11 @@ function _validate(format) {
|
||||
`Field \`style\` must be one of (${styleValues.toString()})`,
|
||||
)
|
||||
}
|
||||
if ('idSuffix' in format && !/^[a-zA-Z0-9\-_]*$/.test(format.idSuffix)) {
|
||||
throw new ValidationError(
|
||||
'Field `idSuffix` must contain only numbers, letters, -, and _',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function _clean(format) {
|
||||
@@ -63,6 +68,7 @@ function _clean(format) {
|
||||
'style',
|
||||
'logoBase64',
|
||||
'links',
|
||||
'idSuffix',
|
||||
]
|
||||
|
||||
const cleaned = {}
|
||||
@@ -95,6 +101,7 @@ function _clean(format) {
|
||||
* @param {string} format.style (Optional) Visual style (e.g: 'flat')
|
||||
* @param {string} format.logoBase64 (Optional) Logo data URL
|
||||
* @param {Array} format.links (Optional) Links array (e.g: ['https://example.com', 'https://example.com'])
|
||||
* @param {string} format.idSuffix (Optional) Suffix for IDs, e.g. 1, 2, and 3 for three invocations that will be used on the same page.
|
||||
* @returns {string} Badge in SVG format
|
||||
* @see https://github.com/badges/shields/tree/master/badge-maker/README.md
|
||||
*/
|
||||
|
||||
@@ -101,5 +101,11 @@ describe('makeBadge function', function () {
|
||||
ValidationError,
|
||||
'Field `style` must be one of (plastic,flat,flat-square,for-the-badge,social)',
|
||||
)
|
||||
expect(() =>
|
||||
makeBadge({ label: 'build', message: 'passed', idSuffix: '\\' }),
|
||||
).to.throw(
|
||||
ValidationError,
|
||||
'Field `idSuffix` must contain only numbers, letters, -, and _',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ module.exports = function makeBadge({
|
||||
logoSize,
|
||||
logoWidth,
|
||||
links = ['', ''],
|
||||
idSuffix,
|
||||
}) {
|
||||
// String coercion and whitespace removal.
|
||||
label = `${label}`.trim()
|
||||
@@ -38,6 +39,7 @@ module.exports = function makeBadge({
|
||||
link: links,
|
||||
name: label,
|
||||
value: message,
|
||||
idSuffix,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ module.exports = function makeBadge({
|
||||
logoPadding: logo && label.length ? 3 : 0,
|
||||
color: toSvgColor(color),
|
||||
labelColor: toSvgColor(labelColor),
|
||||
idSuffix,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -167,6 +167,18 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message with custom suffix', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
idSuffix: '1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
@@ -259,6 +271,19 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message with custom suffix', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'flat-square',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
idSuffix: '1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
@@ -351,6 +376,19 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message with custom suffix', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'plastic',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
idSuffix: '1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
@@ -470,6 +508,19 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message with custom suffix', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'for-the-badge',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
idSuffix: '1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
@@ -589,6 +640,19 @@ describe('The badge generator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message with custom suffix', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: 'cactus',
|
||||
message: 'grown',
|
||||
format: 'svg',
|
||||
style: 'social',
|
||||
color: '#b3e',
|
||||
labelColor: '#0f0',
|
||||
logo: 'data:image/svg+xml;base64,PHN2ZyB4bWxu',
|
||||
idSuffix: '1',
|
||||
})
|
||||
})
|
||||
|
||||
it('should match snapshots: message only, no logo', async function () {
|
||||
await expectBadgeToMatchSnapshot({
|
||||
label: '',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "badge-maker",
|
||||
"version": "4.0.0",
|
||||
"version": "4.1.0",
|
||||
"description": "Shields.io badge library",
|
||||
"keywords": [
|
||||
"GitHub",
|
||||
|
||||
@@ -11,6 +11,7 @@ function streamFromString(str) {
|
||||
|
||||
function sendSVG(res, askres, end) {
|
||||
askres.setHeader('Content-Type', 'image/svg+xml;charset=utf-8')
|
||||
askres.setHeader('Content-Security-Policy', "script-src 'none';")
|
||||
askres.setHeader('Content-Length', Buffer.byteLength(res, 'utf8'))
|
||||
end(null, { template: streamFromString(res) })
|
||||
}
|
||||
|
||||
@@ -139,6 +139,27 @@ describe('The server', function () {
|
||||
expect(() => JSON.parse(body)).not.to.throw()
|
||||
})
|
||||
|
||||
describe('Content Security Policy', function () {
|
||||
it('should disable javascript when serving SVG content (no extension)', async function () {
|
||||
const { headers } = await got(`${baseUrl}:fruit-apple-green`)
|
||||
expect(headers['content-security-policy']).to.equal(
|
||||
"script-src 'none';",
|
||||
)
|
||||
})
|
||||
|
||||
it('should disable javascript when serving SVG content (with extension)', async function () {
|
||||
const { headers } = await got(`${baseUrl}:fruit-apple-green.svg`)
|
||||
expect(headers['content-security-policy']).to.equal(
|
||||
"script-src 'none';",
|
||||
)
|
||||
})
|
||||
|
||||
it('should not send content security headers when serving JSON content', async function () {
|
||||
const { headers } = await got(`${baseUrl}:fruit-apple-green.json`)
|
||||
expect(headers).not.to.have.property('content-security-policy')
|
||||
})
|
||||
})
|
||||
|
||||
it('should preserve label case', async function () {
|
||||
const { statusCode, body } = await got(`${baseUrl}:fRuiT-apple-green.svg`)
|
||||
expect(statusCode).to.equal(200)
|
||||
|
||||
4
fly.toml
4
fly.toml
@@ -45,3 +45,7 @@ processes = []
|
||||
interval = "15s"
|
||||
restart_limit = 0
|
||||
timeout = "2s"
|
||||
|
||||
[[vm]]
|
||||
size = "shared-cpu-1x"
|
||||
memory = "256mb"
|
||||
|
||||
661
frontend/blog/2024-09-25-rce.md
Normal file
661
frontend/blog/2024-09-25-rce.md
Normal file
@@ -0,0 +1,661 @@
|
||||
---
|
||||
slug: GHSA-rxvx-x284-4445
|
||||
title: Our response to RCE Security Advisory
|
||||
authors:
|
||||
name: chris48s
|
||||
title: Shields.io Core Team
|
||||
url: https://github.com/chris48s
|
||||
image_url: https://avatars.githubusercontent.com/u/6025893
|
||||
tags: []
|
||||
---
|
||||
|
||||
We've just published a critical security advisory relating to a Remote Code Execution vulnerability in Dynamic JSON/TOML/YAML badges: https://github.com/badges/shields/security/advisories/GHSA-rxvx-x284-4445 Thanks to [@nickcopi](https://github.com/nickcopi) for his help with this.
|
||||
|
||||
If you self-host your own instance of Shields you should upgrade to [server-2024-09-25](https://hub.docker.com/layers/shieldsio/shields/server-2024-09-25/images/sha256-28aaea75049e325c9f1d63c8a8b477fc387d3d3fe35b933d6581487843cd610f?context=explore) or later as soon as possible to protect your instance.
|
||||
|
||||
This is primarily a concern for self-hosting users. However this does also have a couple of knock-on implications for some users of shields.io itself.
|
||||
|
||||
## 1. Users who have authorized the Shields.io GitHub OAuth app
|
||||
|
||||
While we have taken steps to close this vulnerability quickly after becoming aware of it, this attack vector has existed in our application for some time. We aren't aware of it having been actively exploited on shields.io. We also can't prove that it has not been exploited.
|
||||
|
||||
We don't log or track our users, so a breach offers a very limited attack surface against end users of shields.io. This is by design. One of the (few) information assets shields.io does hold is our GitHub token pool. This allows users to share a token with us by authorizing our OAuth app. Doing this gives us access to a token with read-only access to public data which we can use to increase our rate limit when making calls to the GitHub API.
|
||||
|
||||
The tokens we hold are not of high value to an attacker because they have read-only access to public data, but we can't say for sure they haven't been exfiltrated. If you've donated a token in the past and want to revoke it, you can revoke the Shields.io OAuth app at https://github.com/settings/applications which will de-activate the token you have shared with us.
|
||||
|
||||
## 2. Users of Dynamic JSON/TOML/YAML badges
|
||||
|
||||
Up until now, we have been using https://github.com/dchester/jsonpath as our library querying documents using JSONPath expressions. [@nickcopi](https://github.com/nickcopi) responsibly reported to us how a prototype pollution vulnerability in this library could be exploited to construct a JSONPath expression allowing an attacker to perform remote code execution. This vulnerability was reported on the package's issue tracker but not flagged by security scanning tools. It seems extremely unlikely that this will be fixed in the upstream package despite being widely used. It also seems unlikely this package will receive any further maintenance in future, even in response to critical security issues. In order to resolve this issue, we needed to switch to a different JSONPath library. We've decided to switch https://github.com/JSONPath-Plus/JSONPath using the `eval: false` option to disable script expressions.
|
||||
|
||||
This is an important security improvement and we have to make a change to protect the security of shields.io and users hosting their own instance of the application. However, this does come with some tradeoffs from a backwards-compatibility perspective.
|
||||
|
||||
### Using `eval: false`
|
||||
|
||||
Using JSONPath-Plus with `eval: false` does disable some query syntax which relies on evaluating javascript expressions.
|
||||
|
||||
For example, it would previously have been possible to use a JSONPath query like `$..keywords[(@.length-1)]` against the document https://github.com/badges/shields/raw/master/package.json to select the last element from the keywords array https://github.com/badges/shields/blob/e237e40ab88b8ad4808caad4f3dae653822aa79a/package.json#L6-L12
|
||||
|
||||
This is now not a supported query.
|
||||
|
||||
In this particular case, you could rewrite that query to `$..keywords[-1:]` and obtain the same result, but that may not be possible in all cases. We do realise that this removes some functionality that previously worked but closing this remote code execution vulnerability is the top priority, especially since there will be workarounds in many cases.
|
||||
|
||||
### Implementation Quirks
|
||||
|
||||
Historically, every JSONPath implementation has had its own idiosyncrasies. While most simple and common queries will behave the same way across different implementations, switching to another library will mean that some subset of queries may not work or produce different results.
|
||||
|
||||
One interesting thing that has happened in the world of JSONPath lately is RFC 9535 https://www.rfc-editor.org/rfc/rfc9535 which is an attempt to standardise JSONPath. As part of this mitigation, we did look at whether it would be possible to migrate to something that is RFC9535-compliant. However it is our assessment that the JavaScript community does not yet have a sufficiently mature/supported RFC9535-compliant JSONPath implementation. This means we are switching from one quirky implementation to another implementation with different quirks.
|
||||
|
||||
Again, this represents an unfortunate break in backwards-compatibility. However, it was necessary to prioritise closing off this remote code execution vulnerability over backwards-compatibility.
|
||||
|
||||
Although we can not provide a precise migration guide, here is a table of query types where https://github.com/dchester/jsonpath and https://github.com/JSONPath-Plus/JSONPath are known to diverge from the consensus implementation. This is sourced from the excellent https://cburgmer.github.io/json-path-comparison/
|
||||
While this is a long list, many of these inputs represent edge cases or pathological inputs rather than common usage.
|
||||
|
||||
<details>
|
||||
<summary>Table</summary>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Query Type</th>
|
||||
<th>Example Query</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Array slice with large number for end and negative step</td>
|
||||
<td><code>$[2:-113667776004:-1]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Array slice with large number for start end negative step</td>
|
||||
<td><code>$[113667776004:2:-1]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Array slice with negative step</td>
|
||||
<td><code>$[3:0:-2]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Array slice with negative step on partially overlapping array</td>
|
||||
<td><code>$[7:3:-1]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Array slice with negative step only</td>
|
||||
<td><code>$[::-2]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Array slice with open end and negative step</td>
|
||||
<td><code>$[3::-1]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Array slice with open start and negative step</td>
|
||||
<td><code>$[:2:-1]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Array slice with range of 0</td>
|
||||
<td><code>$[0:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Array slice with step 0</td>
|
||||
<td><code>$[0:3:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Array slice with step and leading zeros</td>
|
||||
<td><code>$[010:024:010]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with empty path</td>
|
||||
<td><code>$[]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with number on object</td>
|
||||
<td><code>$[0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with number on string</td>
|
||||
<td><code>$[0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with number -1</td>
|
||||
<td><code>$[-1]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted array slice literal</td>
|
||||
<td><code>$[':']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted closing bracket literal</td>
|
||||
<td><code>$[']']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted current object literal</td>
|
||||
<td><code>$['@']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted escaped backslash</td>
|
||||
<td><code>$['\\']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted escaped single quote</td>
|
||||
<td><code>$['\'']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted root literal</td>
|
||||
<td><code>$['$']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted special characters combined</td>
|
||||
<td><code>$[':@."$,*\'\\']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted string and unescaped single quote</td>
|
||||
<td><code>$['single'quote']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted union literal</td>
|
||||
<td><code>$[',']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted wildcard literal ?</td>
|
||||
<td><code>$['*']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with quoted wildcard literal on object without key</td>
|
||||
<td><code>$['*']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with spaces</td>
|
||||
<td><code>$[ 'a' ]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with two literals separated by dot</td>
|
||||
<td><code>$['two'.'some']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation with two literals separated by dot without quotes</td>
|
||||
<td><code>$[two.some]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bracket notation without quotes</td>
|
||||
<td><code>$[key]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Current with dot notation</td>
|
||||
<td><code>@.a</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot bracket notation</td>
|
||||
<td><code>$.['key']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot bracket notation with double quotes</td>
|
||||
<td><code>$.["key"]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot bracket notation without quotes</td>
|
||||
<td><code>$.[key]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation after recursive descent with extra dot ?</td>
|
||||
<td><code>$...key</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation after union with keys</td>
|
||||
<td><code>$['one','three'].key</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with dash</td>
|
||||
<td><code>$.key-dash</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with double quotes</td>
|
||||
<td><code>$."key"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with double quotes after recursive descent ?</td>
|
||||
<td><code>$.."key"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with empty path</td>
|
||||
<td><code>$.</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with key named length on array</td>
|
||||
<td><code>$.length</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with key root literal</td>
|
||||
<td><code>$.$</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with non ASCII key</td>
|
||||
<td><code>$.??</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with number</td>
|
||||
<td><code>$.2</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with number -1</td>
|
||||
<td><code>$.-1</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with single quotes</td>
|
||||
<td><code>$.'key'</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with single quotes after recursive descent ?</td>
|
||||
<td><code>$..'key'</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with single quotes and dot</td>
|
||||
<td><code>$.'some.key'</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with space padded key</td>
|
||||
<td><code>$. a</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation with wildcard after recursive descent on scalar ?</td>
|
||||
<td><code>$..*</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation without dot</td>
|
||||
<td><code>$a</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation without root</td>
|
||||
<td><code>.key</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dot notation without root and dot</td>
|
||||
<td><code>key</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Empty</td>
|
||||
<td><code>n/a</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression on object</td>
|
||||
<td><code>$[?(@.key)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression after dot notation with wildcard after recursive descent ?</td>
|
||||
<td><code>$..*[?(@.id>2)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression after recursive descent ?</td>
|
||||
<td><code>$..[?(@.id==2)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with addition</td>
|
||||
<td><code>$[?(@.key+50==100)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with boolean and operator and value false</td>
|
||||
<td><code>$[?(@.key>0 && false)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with boolean and operator and value true</td>
|
||||
<td><code>$[?(@.key>0 && true)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with boolean or operator and value false</td>
|
||||
<td><code>$[?(@.key>0 &#124;&#124; false)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with boolean or operator and value true</td>
|
||||
<td><code>$[?(@.key>0 &#124;&#124; true)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with bracket notation with -1</td>
|
||||
<td><code>$[?(@[-1]==2)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with bracket notation with number on object</td>
|
||||
<td><code>$[?(@[1]=='b')]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with current object</td>
|
||||
<td><code>$[?(@)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with different ungrouped operators</td>
|
||||
<td><code>$[?(@.a && @.b &#124;&#124; @.c)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with division</td>
|
||||
<td><code>$[?(@.key/10==5)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with dot notation with dash</td>
|
||||
<td><code>$[?(@.key-dash == 'value')]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with dot notation with number</td>
|
||||
<td><code>$[?(@.2 == 'second')]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with dot notation with number on array</td>
|
||||
<td><code>$[?(@.2 == 'third')]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with empty expression</td>
|
||||
<td><code>$[?()]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals</td>
|
||||
<td><code>$[?(@.key==42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals on array of numbers</td>
|
||||
<td><code>$[?(@==42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals on object</td>
|
||||
<td><code>$[?(@.key==42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals array</td>
|
||||
<td><code>$[?(@.d==["v1","v2"])]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals array for array slice with range 1</td>
|
||||
<td><code>$[?(@[0:1]==[1])]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals array for dot notation with star</td>
|
||||
<td><code>$[?(@.*==[1,2])]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals array or equals true</td>
|
||||
<td><code>$[?(@.d==["v1","v2"] &#124;&#124; (@.d == true))]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals array with single quotes</td>
|
||||
<td><code>$[?(@.d==['v1','v2'])]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals boolean expression value</td>
|
||||
<td><code>$[?((@.key<44)==false)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals false</td>
|
||||
<td><code>$[?(@.key==false)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals null</td>
|
||||
<td><code>$[?(@.key==null)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals number for array slice with range 1</td>
|
||||
<td><code>$[?(@[0:1]==1)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals number for bracket notation with star</td>
|
||||
<td><code>$[?(@[*]==2)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals number for dot notation with star</td>
|
||||
<td><code>$[?(@.*==2)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals number with fraction</td>
|
||||
<td><code>$[?(@.key==-0.123e2)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals number with leading zeros</td>
|
||||
<td><code>$[?(@.key==010)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals object</td>
|
||||
<td><code>$[?(@.d=={"k":"v"})]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals string</td>
|
||||
<td><code>$[?(@.key=="value")]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals string with unicode character escape</td>
|
||||
<td><code>$[?(@.key=="Mot\u00f6rhead")]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals true</td>
|
||||
<td><code>$[?(@.key==true)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals with path and path</td>
|
||||
<td><code>$[?(@.key1==@.key2)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with equals with root reference</td>
|
||||
<td><code>$.items[?(@.key==$.value)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with greater than</td>
|
||||
<td><code>$[?(@.key>42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with greater than or equal</td>
|
||||
<td><code>$[?(@.key>=42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with in array of values</td>
|
||||
<td><code>$[?(@.d in [2, 3])]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with in current object</td>
|
||||
<td><code>$[?(2 in @.d)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with length free function</td>
|
||||
<td><code>$[?(length(@) == 4)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with length function</td>
|
||||
<td><code>$[?(@.length() == 4)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with length property</td>
|
||||
<td><code>$[?(@.length == 4)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with less than</td>
|
||||
<td><code>$[?(@.key<42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with less than or equal</td>
|
||||
<td><code>$[?(@.key<=42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with local dot key and null in data</td>
|
||||
<td><code>$[?(@.key='value')]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with multiplication</td>
|
||||
<td><code>$[?(@.key*2==100)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with negation and equals</td>
|
||||
<td><code>$[?(!(@.key==42))]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with negation and equals array or equals true</td>
|
||||
<td><code>$[?(!(@.d==["v1","v2"]) &#124;&#124; (@.d == true))]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with negation and less than</td>
|
||||
<td><code>$[?(!(@.key<42))]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with negation and without value</td>
|
||||
<td><code>$[?(!@.key)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with non singular existence test</td>
|
||||
<td><code>$[?(@.a.*)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with not equals</td>
|
||||
<td><code>$[?(@.key!=42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with not equals array or equals true</td>
|
||||
<td><code>$[?((@.d!=["v1","v2"]) &#124;&#124; (@.d == true))]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with parent axis operator</td>
|
||||
<td><code>$[*].bookmarks[?(@.page == 45)]^^^</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with regular expression</td>
|
||||
<td><code>$[?(@.name=~/hello.*/)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with regular expression from member</td>
|
||||
<td><code>$[?(@.name=~/@.pattern/)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with set wise comparison to scalar</td>
|
||||
<td><code>$[?(@[*]>=4)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with set wise comparison to set</td>
|
||||
<td><code>$.x[?(@[*]>=$.y[*])]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with single equal</td>
|
||||
<td><code>$[?(@.key=42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with subfilter</td>
|
||||
<td><code>$[?(@.a[?(@.price>10)])]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with subpaths deeply nested</td>
|
||||
<td><code>$[?(@.a.b.c==3)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with subtraction</td>
|
||||
<td><code>$[?(@.key-50==-100)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with triple equal</td>
|
||||
<td><code>$[?(@.key===42)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with value</td>
|
||||
<td><code>$[?(@.key)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with value after recursive descent ?</td>
|
||||
<td><code>$..[?(@.id)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with value false</td>
|
||||
<td><code>$[?(false)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with value from recursive descent</td>
|
||||
<td><code>$[?(@..child)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with value null</td>
|
||||
<td><code>$[?(null)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression with value true</td>
|
||||
<td><code>$[?(true)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression without parens</td>
|
||||
<td><code>$[?@.key==42]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filter expression without value</td>
|
||||
<td><code>$[?(@.key)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Function sum</td>
|
||||
<td><code>$.data.sum()</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Parens notation</td>
|
||||
<td><code>$(key,more)</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Recursive descent ?</td>
|
||||
<td><code>$..</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Recursive descent after dot notation ?</td>
|
||||
<td><code>$.key..</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Root on scalar</td>
|
||||
<td><code>$</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Root on scalar false</td>
|
||||
<td><code>$</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Root on scalar true</td>
|
||||
<td><code>$</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Script expression</td>
|
||||
<td><code>$[(@.length-1)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with duplication from array</td>
|
||||
<td><code>$[0,0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with duplication from object</td>
|
||||
<td><code>$['a','a']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with filter</td>
|
||||
<td><code>$[?(@.key<3),?(@.key>6)]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with keys</td>
|
||||
<td><code>$['key','another']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with keys on object without key</td>
|
||||
<td><code>$['missing','key']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with keys after array slice</td>
|
||||
<td><code>$[:]['c','d']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with keys after bracket notation</td>
|
||||
<td><code>$[0]['c','d']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with keys after dot notation with wildcard</td>
|
||||
<td><code>$.*['c','d']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with keys after recursive descent ?</td>
|
||||
<td><code>$..['c','d']</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with repeated matches after dot notation with wildcard</td>
|
||||
<td><code>$.*[0,:5]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with slice and number</td>
|
||||
<td><code>$[1:3,4]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with spaces</td>
|
||||
<td><code>$[ 0 , 1 ]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Union with wildcard and number</td>
|
||||
<td><code>$[*,1]</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
28
frontend/blog/2024-11-14-token-pool.md
Normal file
28
frontend/blog/2024-11-14-token-pool.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
slug: token-pool
|
||||
title: How shields.io uses the GitHub API
|
||||
authors:
|
||||
name: chris48s
|
||||
title: Shields.io Core Team
|
||||
url: https://github.com/chris48s
|
||||
image_url: https://avatars.githubusercontent.com/u/6025893
|
||||
tags: []
|
||||
---
|
||||
|
||||
We serve a lot of badges which display information fetched from the GitHub API. When I say a lot, this varies a bit but in a typical hour we make hundreds of thousands of calls to the GitHub API.
|
||||
|
||||
But hang on. GitHub's API has rate limits.
|
||||
|
||||
Specifically, users can make up to [5,000 requests per hour](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-authenticated-users) to GitHub's v3/REST API. The v4/GraphQL also applies rate limits, but it is based on a slightly more complicated [points-based system](https://docs.github.com/en/graphql/overview/rate-limits-and-node-limits-for-the-graphql-api#primary-rate-limit).
|
||||
|
||||
In any case, we are clearly making many times more requests to GitHub's API than would be allowed with a single token.
|
||||
|
||||
So how are we doing that? Well, we have lots of tokens. To elaborate on that slightly, as a user of shields.io you can choose to share a token with us to help increase our rate limit. Here's how it works:
|
||||
|
||||
- Authorize our [OAuth Application](https://img.shields.io/github-auth).
|
||||
- This shares with us a GitHub token which has read-only access to public data. We only ask for the minimum permissions necessary. Authorizing the OAuth app doesn't allow us access to your private data or allow us to perform any actions on your behalf.
|
||||
- Your token is added to a pool of tokens shared by other users like you.
|
||||
- When we need to make a request to the GitHub API, we pick one of the tokens from our pool. We only make a handful of requests with each token before picking another from the pool.
|
||||
- If you ever decide you would not like to continue sharing a token with us, you can revoke the Shields.io OAuth app at https://github.com/settings/applications. You can do this at any time. This will de-activate the token you have shared with us and we'll remove it from the pool.
|
||||
|
||||
This method allows us (with your help) to make hundreds of thousands of request per hour to the GitHub API. Because we have thousands of tokens in the pool and we only make a few requests with each one before picking another token from the pool, most users don't notice any meaningful impact on their available rate limit as a result of authorizing our app.
|
||||
15
migrations/1727809177709_add-created.cjs
Normal file
15
migrations/1727809177709_add-created.cjs
Normal file
@@ -0,0 +1,15 @@
|
||||
exports.shorthands = undefined
|
||||
|
||||
exports.up = pgm => {
|
||||
pgm.addColumn('github_user_tokens', {
|
||||
created: {
|
||||
type: 'TIMESTAMP',
|
||||
notNull: true,
|
||||
default: pgm.func('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = pgm => {
|
||||
pgm.dropColumn('github_user_tokens', 'created')
|
||||
}
|
||||
1187
package-lock.json
generated
1187
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
@@ -21,11 +21,11 @@
|
||||
"url": "https://github.com/badges/shields"
|
||||
},
|
||||
"dependencies": {
|
||||
"@renovatebot/pep440": "^3.0.20",
|
||||
"@renovatebot/ruby-semver": "^3.0.23",
|
||||
"@sentry/node": "^8.30.0",
|
||||
"@renovatebot/pep440": "^4.0.1",
|
||||
"@renovatebot/ruby-semver": "^4.0.0",
|
||||
"@sentry/node": "^8.41.0",
|
||||
"@shields_io/camp": "^18.1.2",
|
||||
"@xmldom/xmldom": "0.9.2",
|
||||
"@xmldom/xmldom": "0.9.5",
|
||||
"badge-maker": "file:badge-maker",
|
||||
"bytes": "^3.1.2",
|
||||
"camelcase": "^8.0.0",
|
||||
@@ -41,33 +41,33 @@
|
||||
"fast-xml-parser": "^4.5.0",
|
||||
"glob": "^11.0.0",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^14.4.2",
|
||||
"got": "^14.4.5",
|
||||
"graphql": "16.9.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"joi": "17.13.3",
|
||||
"joi-extension-semver": "5.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath-plus": "^9.0.0",
|
||||
"jsonpath-plus": "^10.2.0",
|
||||
"lodash.countby": "^4.6.0",
|
||||
"lodash.groupby": "^4.6.0",
|
||||
"lodash.times": "^4.3.2",
|
||||
"matcher": "^5.0.0",
|
||||
"node-env-flag": "^0.1.0",
|
||||
"node-pg-migrate": "^7.6.1",
|
||||
"node-pg-migrate": "^7.8.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"path-to-regexp": "^6.3.0",
|
||||
"pg": "^8.13.0",
|
||||
"pg": "^8.13.1",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"priorityqueuejs": "^2.0.0",
|
||||
"prom-client": "^15.1.3",
|
||||
"qs": "^6.13.0",
|
||||
"query-string": "^9.1.0",
|
||||
"qs": "^6.13.1",
|
||||
"query-string": "^9.1.1",
|
||||
"semver": "~7.6.3",
|
||||
"simple-icons": "13.10.0",
|
||||
"smol-toml": "1.3.0",
|
||||
"simple-icons": "13.18.0",
|
||||
"smol-toml": "1.3.1",
|
||||
"svg-path-bbox": "^2.1.0",
|
||||
"svgpath": "^2.6.0",
|
||||
"webextension-store-meta": "^1.2.3",
|
||||
"webextension-store-meta": "^1.2.4",
|
||||
"xpath": "~0.0.34"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -148,19 +148,19 @@
|
||||
"devDependencies": {
|
||||
"@docusaurus/core": "^3.5.2",
|
||||
"@docusaurus/preset-classic": "^3.5.2",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.44.5",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@typescript-eslint/parser": "^8.6.0",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.46.1",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@typescript-eslint/parser": "^8.16.0",
|
||||
"c8": "^10.1.2",
|
||||
"caller": "^1.1.0",
|
||||
"chai": "^4.5.0",
|
||||
"chai-as-promised": "^8.0.0",
|
||||
"chai-as-promised": "^8.0.1",
|
||||
"chai-datetime": "^1.8.1",
|
||||
"chai-string": "^1.4.0",
|
||||
"child-process-promise": "^2.2.1",
|
||||
"clsx": "^2.1.1",
|
||||
"concurrently": "^9.0.1",
|
||||
"cypress": "^13.14.2",
|
||||
"concurrently": "^9.1.0",
|
||||
"cypress": "^13.16.0",
|
||||
"cypress-wait-for-stable-dom": "^0.1.0",
|
||||
"danger": "^12.3.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
@@ -171,36 +171,36 @@
|
||||
"eslint-config-standard-jsx": "11.0.0",
|
||||
"eslint-config-standard-react": "13.0.0",
|
||||
"eslint-plugin-chai-friendly": "^1.0.1",
|
||||
"eslint-plugin-cypress": "^3.5.0",
|
||||
"eslint-plugin-cypress": "^3.6.0",
|
||||
"eslint-plugin-icedfrisby": "^0.1.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-jsdoc": "^50.2.4",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsdoc": "^50.6.0",
|
||||
"eslint-plugin-mocha": "^10.5.0",
|
||||
"eslint-plugin-no-extension-in-require": "^0.2.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "6.6.0",
|
||||
"eslint-plugin-react": "^7.36.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-sort-class-members": "^1.20.0",
|
||||
"form-data": "^4.0.0",
|
||||
"eslint-plugin-sort-class-members": "^1.21.0",
|
||||
"form-data": "^4.0.1",
|
||||
"icedfrisby": "4.0.0",
|
||||
"icedfrisby-nock": "^2.1.0",
|
||||
"is-svg": "^5.1.0",
|
||||
"jsdoc": "^4.0.3",
|
||||
"jsdoc": "^4.0.4",
|
||||
"lint-staged": "^15.2.10",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"minimist": "^1.2.8",
|
||||
"mocha": "^10.7.3",
|
||||
"mocha": "^10.8.2",
|
||||
"mocha-env-reporter": "^4.0.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-yaml-loader": "^1.0.3",
|
||||
"nock": "13.5.5",
|
||||
"node-mocks-http": "^1.16.0",
|
||||
"nodemon": "^3.1.6",
|
||||
"nock": "13.5.6",
|
||||
"node-mocks-http": "^1.16.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"open-cli": "^8.0.0",
|
||||
"portfinder": "^1.0.32",
|
||||
"prettier": "3.3.3",
|
||||
"prettier": "3.4.1",
|
||||
"prism-react-renderer": "^2.4.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Arch Linux package (valid)')
|
||||
.get('/core/x86_64/pacman.json')
|
||||
.get('/core/x86_64/iptables.json')
|
||||
.expectBadge({
|
||||
label: 'arch linux',
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffixAndEpoch,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import {
|
||||
floorCount as floorCountColor,
|
||||
age as ageColor,
|
||||
} from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
import { renderLicenseBadge } from '../licenses.js'
|
||||
import { addv, metric, formatDate } from '../text-formatters.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import {
|
||||
BaseJsonService,
|
||||
@@ -12,6 +10,7 @@ import {
|
||||
InvalidResponse,
|
||||
pathParams,
|
||||
} from '../index.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
|
||||
const aurSchema = Joi.object({
|
||||
resultcount: nonNegativeInteger,
|
||||
@@ -170,7 +169,7 @@ class AurVersion extends BaseAurService {
|
||||
|
||||
static render({ version, outOfDate }) {
|
||||
const color = outOfDate === null ? 'blue' : 'orange'
|
||||
return { message: addv(version), color }
|
||||
return renderVersionBadge({ version, versionFormatter: () => color })
|
||||
}
|
||||
|
||||
async handle({ packageName }) {
|
||||
@@ -242,16 +241,10 @@ class AurLastModified extends BaseAurService {
|
||||
|
||||
static defaultBadgeData = { label: 'last modified' }
|
||||
|
||||
static render({ date }) {
|
||||
const color = ageColor(date)
|
||||
const message = formatDate(date)
|
||||
return { color, message }
|
||||
}
|
||||
|
||||
async handle({ packageName }) {
|
||||
const json = await this.fetch({ packageName })
|
||||
const date = 1000 * parseInt(json.results[0].LastModified)
|
||||
return this.constructor.render({ date })
|
||||
return renderDateBadge(date)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ describe('AurVersion', function () {
|
||||
given({ version: '1:1.1.42.622-1', outOfDate: 1 }).expect({
|
||||
color: 'orange',
|
||||
message: 'v1:1.1.42.622-1',
|
||||
label: undefined,
|
||||
})
|
||||
|
||||
given({ version: '7', outOfDate: null }).expect({
|
||||
color: 'blue',
|
||||
message: 'v7',
|
||||
label: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { relativeUri } from '../validators.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
@@ -43,13 +42,6 @@ export default class BitbucketLastCommit extends BaseJsonService {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ user, repo, branch, path }) {
|
||||
// https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#api-repositories-workspace-repo-slug-commits-get
|
||||
return this._requestJson({
|
||||
@@ -76,6 +68,6 @@ export default class BitbucketLastCommit extends BaseJsonService {
|
||||
|
||||
if (!commit) throw new NotFound({ prettyMessage: 'no commits found' })
|
||||
|
||||
return this.constructor.render({ commitDate: commit.date })
|
||||
return renderDateBadge(commit.date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { NotFound, pathParams } from '../index.js'
|
||||
import BaseChromeWebStoreService from './chrome-web-store-base.js'
|
||||
|
||||
export default class ChromeWebStoreLastUpdated extends BaseChromeWebStoreService {
|
||||
static category = 'activity'
|
||||
static route = { base: 'chrome-web-store/last-updated', pattern: ':storeId' }
|
||||
|
||||
static openApi = {
|
||||
'/chrome-web-store/last-updated/{storeId}': {
|
||||
get: {
|
||||
summary: 'Chrome Web Store Last Updated',
|
||||
parameters: pathParams({
|
||||
name: 'storeId',
|
||||
example: 'nccfelhkfpbnefflolffkclhenplhiab',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'last updated',
|
||||
}
|
||||
|
||||
async handle({ storeId }) {
|
||||
const chromeWebStore = await this.fetch({ storeId })
|
||||
const lastUpdated = chromeWebStore.lastUpdated()
|
||||
|
||||
if (lastUpdated == null) {
|
||||
throw new NotFound({ prettyMessage: 'not found' })
|
||||
}
|
||||
|
||||
return renderDateBadge(lastUpdated)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { isFormattedDate } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('Last updated')
|
||||
.get('/nccfelhkfpbnefflolffkclhenplhiab.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('Last updated (not found)')
|
||||
.get('/invalid-name-of-addon.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'not found',
|
||||
})
|
||||
35
services/chrome-web-store/chrome-web-store-size.service.js
Normal file
35
services/chrome-web-store/chrome-web-store-size.service.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NotFound, pathParams } from '../index.js'
|
||||
import BaseChromeWebStoreService from './chrome-web-store-base.js'
|
||||
|
||||
export default class ChromeWebStoreSize extends BaseChromeWebStoreService {
|
||||
static category = 'size'
|
||||
static route = { base: 'chrome-web-store/size', pattern: ':storeId' }
|
||||
|
||||
static openApi = {
|
||||
'/chrome-web-store/size/{storeId}': {
|
||||
get: {
|
||||
summary: 'Chrome Web Store Size',
|
||||
parameters: pathParams({
|
||||
name: 'storeId',
|
||||
example: 'nccfelhkfpbnefflolffkclhenplhiab',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'extension size',
|
||||
color: 'blue',
|
||||
}
|
||||
|
||||
async handle({ storeId }) {
|
||||
const chromeWebStore = await this.fetch({ storeId })
|
||||
const size = chromeWebStore.size()
|
||||
|
||||
if (size == null) {
|
||||
throw new NotFound({ prettyMessage: 'not found' })
|
||||
}
|
||||
|
||||
return { message: size }
|
||||
}
|
||||
}
|
||||
13
services/chrome-web-store/chrome-web-store-size.tester.js
Normal file
13
services/chrome-web-store/chrome-web-store-size.tester.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
const isFileSize = /^\d+(\.\d+)?(MiB|KiB)$/
|
||||
|
||||
t.create('Size').get('/nccfelhkfpbnefflolffkclhenplhiab.json').expectBadge({
|
||||
label: 'extension size',
|
||||
message: isFileSize,
|
||||
})
|
||||
|
||||
t.create('Size (not found)')
|
||||
.get('/invalid-name-of-addon.json')
|
||||
.expectBadge({ label: 'extension size', message: 'not found' })
|
||||
@@ -34,6 +34,10 @@ class ClojarsVersionService extends BaseClojarsService {
|
||||
static defaultBadgeData = { label: 'clojars' }
|
||||
|
||||
static render({ clojar, version }) {
|
||||
// clojars format is non standard to fit community style
|
||||
// dont use renderVersionBadge
|
||||
// see also https://github.com/badges/shields/pull/431
|
||||
// commit d0414c9
|
||||
return {
|
||||
message: `[${clojar} "${version}"]`,
|
||||
color: versionColor(version),
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import pep440 from '@renovatebot/pep440'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge based on version.
|
||||
@@ -23,7 +22,10 @@ function version(version) {
|
||||
if (first === 'v') {
|
||||
first = version[1]
|
||||
}
|
||||
if (first === '0' || /alpha|beta|snapshot|dev|pre|rc/i.test(version)) {
|
||||
if (
|
||||
first === '0' ||
|
||||
/alpha|beta|snapshot|dev|pre|rc|scm|cvs/i.test(version)
|
||||
) {
|
||||
return 'orange'
|
||||
} else {
|
||||
return 'blue'
|
||||
@@ -172,24 +174,7 @@ function colorScale(steps, colors, reversed) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge according to the age.
|
||||
* Age is calculated as days elapsed till current date.
|
||||
* The color varies from bright green to red as the age increases
|
||||
* or the other way around if `reverse` is given `true`.
|
||||
*
|
||||
* @param {string} date Date string
|
||||
* @param {boolean} reversed Reverse the color scale a.k.a. the older, the better
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function age(date, reversed = false) {
|
||||
const colorByAge = colorScale([7, 30, 180, 365, 730], undefined, !reversed)
|
||||
const daysElapsed = dayjs().diff(dayjs(date), 'days')
|
||||
return colorByAge(daysElapsed)
|
||||
}
|
||||
|
||||
export {
|
||||
age,
|
||||
colorScale,
|
||||
coveragePercentage,
|
||||
downloadCount,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { forCases, given, test } from 'sazerac'
|
||||
import {
|
||||
age,
|
||||
colorScale,
|
||||
coveragePercentage,
|
||||
letterScore,
|
||||
@@ -53,46 +52,6 @@ describe('Color formatters', function () {
|
||||
given('Z').expect('red')
|
||||
})
|
||||
|
||||
const monthsAgo = months => {
|
||||
const result = new Date()
|
||||
// This looks wack but it works.
|
||||
result.setMonth(result.getMonth() - months)
|
||||
return result
|
||||
}
|
||||
test(age, () => {
|
||||
given(Date.now())
|
||||
.describe('when given the current timestamp')
|
||||
.expect('brightgreen')
|
||||
given(new Date())
|
||||
.describe('when given the current Date')
|
||||
.expect('brightgreen')
|
||||
given(new Date(2001, 1, 1))
|
||||
.describe('when given a Date many years ago')
|
||||
.expect('red')
|
||||
given(monthsAgo(2))
|
||||
.describe('when given a Date two months ago')
|
||||
.expect('yellowgreen')
|
||||
given(monthsAgo(15))
|
||||
.describe('when given a Date 15 months ago')
|
||||
.expect('orange')
|
||||
// --- reversed --- //
|
||||
given(Date.now(), true)
|
||||
.describe('when given the current timestamp and reversed')
|
||||
.expect('red')
|
||||
given(new Date(), true)
|
||||
.describe('when given the current Date and reversed')
|
||||
.expect('red')
|
||||
given(new Date(2001, 1, 1), true)
|
||||
.describe('when given a Date many years ago and reversed')
|
||||
.expect('brightgreen')
|
||||
given(monthsAgo(2), true)
|
||||
.describe('when given a Date two months ago and reversed')
|
||||
.expect('yellow')
|
||||
given(monthsAgo(15), true)
|
||||
.describe('when given a Date 15 months ago and reversed')
|
||||
.expect('green')
|
||||
})
|
||||
|
||||
test(version, () => {
|
||||
forCases([given('1.0'), given(9), given(1.0)]).expect('blue')
|
||||
|
||||
@@ -105,6 +64,8 @@ describe('Color formatters', function () {
|
||||
given('1.0.1-dev'),
|
||||
given('2.1.6-prerelease'),
|
||||
given('2.1.6-RC1'),
|
||||
given('cvs-1'),
|
||||
given('scm-2'),
|
||||
]).expect('orange')
|
||||
|
||||
expect(() => version(null)).to.throw(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { pathParams } from '../index.js'
|
||||
import { addv as versionText } from '../text-formatters.js'
|
||||
import { version as versionColor } from '../color-formatters.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import BaseCondaService from './conda-base.js'
|
||||
|
||||
export default class CondaVersion extends BaseCondaService {
|
||||
@@ -33,20 +32,12 @@ export default class CondaVersion extends BaseCondaService {
|
||||
},
|
||||
}
|
||||
|
||||
static render({ variant, channel, version }) {
|
||||
return {
|
||||
label: variant === 'vn' ? channel : `conda | ${channel}`,
|
||||
message: versionText(version),
|
||||
color: versionColor(version),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ variant, channel, packageName }) {
|
||||
const json = await this.fetch({ channel, packageName })
|
||||
return this.constructor.render({
|
||||
variant,
|
||||
channel,
|
||||
const defaultLabel = variant === 'vn' ? channel : `conda | ${channel}`
|
||||
return renderVersionBadge({
|
||||
version: json.latest_version,
|
||||
defaultLabel,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import { BaseJsonService, InvalidResponse } from '../index.js'
|
||||
|
||||
const versionSchema = Joi.object({
|
||||
downloads: nonNegativeInteger,
|
||||
// Crate size is not available for all versions.
|
||||
crate_size: nonNegativeInteger.allow(null),
|
||||
crate_size: nonNegativeInteger,
|
||||
num: Joi.string().required(),
|
||||
license: Joi.string().required().allow(null),
|
||||
rust_version: Joi.string().allow(null),
|
||||
@@ -24,9 +23,21 @@ const versionResponseSchema = Joi.object({
|
||||
version: versionSchema.required(),
|
||||
}).required()
|
||||
|
||||
const userStatsSchema = Joi.object({
|
||||
total_downloads: nonNegativeInteger.required(),
|
||||
}).required()
|
||||
|
||||
class BaseCratesService extends BaseJsonService {
|
||||
static defaultBadgeData = { label: 'crates.io' }
|
||||
|
||||
/**
|
||||
* Fetches data from the crates.io API.
|
||||
*
|
||||
* @param {object} options - The options for the request
|
||||
* @param {string} options.crate - The crate name.
|
||||
* @param {string} [options.version] - The crate version number (optional).
|
||||
* @returns {Promise<object>} the JSON response from the API.
|
||||
*/
|
||||
async fetch({ crate, version }) {
|
||||
const url = version
|
||||
? `https://crates.io/api/v1/crates/${crate}/${version}`
|
||||
@@ -54,7 +65,23 @@ class BaseCratesService extends BaseJsonService {
|
||||
}
|
||||
}
|
||||
|
||||
class BaseCratesUserService extends BaseJsonService {
|
||||
static defaultBadgeData = { label: 'crates.io' }
|
||||
|
||||
/**
|
||||
* Fetches data from the crates.io API.
|
||||
*
|
||||
* @param {object} options - The options for the request
|
||||
* @param {string} options.userId - The user ID.
|
||||
* @returns {Promise<object>} the JSON response from the API.
|
||||
*/
|
||||
async fetch({ userId }) {
|
||||
const url = `https://crates.io/api/v1/users/${userId}/stats`
|
||||
return this._requestJson({ schema: userStatsSchema, url })
|
||||
}
|
||||
}
|
||||
|
||||
const description =
|
||||
'[Crates.io](https://crates.io/) is a package registry for Rust.'
|
||||
|
||||
export { BaseCratesService, description }
|
||||
export { BaseCratesService, BaseCratesUserService, description }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import { InvalidResponse, pathParams } from '../index.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import { BaseCratesService, description } from './crates-base.js'
|
||||
|
||||
export default class CratesSize extends BaseCratesService {
|
||||
@@ -49,11 +49,6 @@ export default class CratesSize extends BaseCratesService {
|
||||
async handle({ crate, version }) {
|
||||
const json = await this.fetch({ crate, version })
|
||||
const size = this.constructor.getVersionObj(json).crate_size
|
||||
|
||||
if (size == null) {
|
||||
throw new InvalidResponse({ prettyMessage: 'unknown' })
|
||||
}
|
||||
|
||||
return this.render({ size })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,6 @@ t.create('size (with version)')
|
||||
.get('/tokio/1.32.0.json')
|
||||
.expectBadge({ label: 'size', message: '725 kB' })
|
||||
|
||||
t.create('size (with version where version doesnt have size)')
|
||||
.get('/tokio/0.1.6.json')
|
||||
.expectBadge({ label: 'crates.io', message: 'unknown' })
|
||||
|
||||
t.create('size (not found)')
|
||||
.get('/not-a-crate.json')
|
||||
.expectBadge({ label: 'crates.io', message: 'not found' })
|
||||
|
||||
32
services/crates/crates-user-downloads.service.js
Normal file
32
services/crates/crates-user-downloads.service.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import { BaseCratesUserService, description } from './crates-base.js'
|
||||
|
||||
export default class CratesUserDownloads extends BaseCratesUserService {
|
||||
static category = 'downloads'
|
||||
static route = {
|
||||
base: 'crates',
|
||||
pattern: 'udt/:userId',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/crates/udt/{userId}': {
|
||||
get: {
|
||||
summary: 'Crates.io User Total Downloads',
|
||||
description,
|
||||
parameters: pathParams({
|
||||
name: 'userId',
|
||||
example: '3027',
|
||||
description:
|
||||
'The user ID can be found using `https://crates.io/api/v1/users/{username}`',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async handle({ userId }) {
|
||||
const json = await this.fetch({ userId })
|
||||
const { total_downloads: downloads } = json
|
||||
return renderDownloadsBadge({ downloads, labelOverride: 'downloads' })
|
||||
}
|
||||
}
|
||||
16
services/crates/crates-user-downloads.tester.js
Normal file
16
services/crates/crates-user-downloads.tester.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { isMetric } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('total user downloads')
|
||||
.get('/udt/3027.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetric })
|
||||
|
||||
// non-existent user returns 0 downloads with 200 OK status code rather than 404.
|
||||
t.create('total user downloads (user not found)')
|
||||
.get('/udt/2147483647.json') // 2147483647 is the maximum valid user id as API uses i32
|
||||
.expectBadge({ label: 'downloads', message: '0' })
|
||||
|
||||
t.create('total user downloads (invalid)')
|
||||
.get('/udt/999999999999999999999999.json')
|
||||
.expectBadge({ label: 'crates.io', message: 'invalid' })
|
||||
108
services/date.js
Normal file
108
services/date.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Commonly-used functions for rendering badges containing a date
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import calendar from 'dayjs/plugin/calendar.js'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
|
||||
import { colorScale } from './color-formatters.js'
|
||||
import { InvalidResponse } from './index.js'
|
||||
|
||||
dayjs.extend(calendar)
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
/**
|
||||
* Parse and validate a string date into a dayjs object. Use this helper
|
||||
* in preference to invoking dayjs directly when parsing a date from string.
|
||||
*
|
||||
* @param {...any} args - Variadic: Arguments to pass through to dayjs
|
||||
* @returns {dayjs} - Parsed object
|
||||
* @throws {InvalidResponse} - Error if validation fails
|
||||
* @see https://day.js.org/docs/en/parse/string
|
||||
* @see https://day.js.org/docs/en/parse/string-format
|
||||
* @see https://day.js.org/docs/en/parse/is-valid
|
||||
* @example
|
||||
* parseDate('2024-01-01')
|
||||
* parseDate('31/01/2024', 'DD/MM/YYYY')
|
||||
* parseDate('2018 Enero 15', 'YYYY MMMM DD', 'es')
|
||||
*/
|
||||
function parseDate(...args) {
|
||||
let date
|
||||
if (args.length >= 2) {
|
||||
// always use strict mode if format arg is supplied
|
||||
date = dayjs(...args, true)
|
||||
} else {
|
||||
date = dayjs(...args)
|
||||
}
|
||||
if (!date.isValid()) {
|
||||
throw new InvalidResponse({ prettyMessage: 'invalid date' })
|
||||
}
|
||||
return date
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted date string without the year based on the value of input date param d.
|
||||
*
|
||||
* @param {Date | string | number | dayjs } d JS Date object, string, unix timestamp or dayjs object
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
function formatDate(d) {
|
||||
const date = parseDate(d)
|
||||
const dateString = date.calendar(null, {
|
||||
lastDay: '[yesterday]',
|
||||
sameDay: '[today]',
|
||||
lastWeek: '[last] dddd',
|
||||
sameElse: 'MMMM YYYY',
|
||||
})
|
||||
// Trim current year from date string
|
||||
return dateString.replace(` ${dayjs().year()}`, '').toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the color used for a badge according to the age.
|
||||
* Age is calculated as days elapsed till current date.
|
||||
* The color varies from bright green to red as the age increases
|
||||
* or the other way around if `reverse` is given `true`.
|
||||
*
|
||||
* @param {Date | string | number | dayjs } date JS Date object, string, unix timestamp or dayjs object
|
||||
* @param {boolean} reversed Reverse the color scale (the older, the better)
|
||||
* @returns {string} Badge color
|
||||
*/
|
||||
function age(date, reversed = false) {
|
||||
const colorByAge = colorScale([7, 30, 180, 365, 730], undefined, !reversed)
|
||||
const daysElapsed = dayjs().diff(parseDate(date), 'days')
|
||||
return colorByAge(daysElapsed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a badge object that displays a date
|
||||
*
|
||||
* @param {Date | string | number | dayjs } date JS Date object, string, unix timestamp or dayjs object
|
||||
* @param {boolean} reversed Reverse the color scale (the older, the better)
|
||||
* @returns {object} A badge object that has two properties: message, and color
|
||||
*/
|
||||
function renderDateBadge(date, reversed = false) {
|
||||
const d = parseDate(date)
|
||||
const color = age(d, reversed)
|
||||
const message = formatDate(d)
|
||||
return { message, color }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a relative date from the input timestamp.
|
||||
* For example, day after tomorrow's timestamp will return 'in 2 days'.
|
||||
*
|
||||
* @param {number | string} timestamp - Unix timestamp
|
||||
* @returns {string} Relative date from the unix timestamp
|
||||
*/
|
||||
function formatRelativeDate(timestamp) {
|
||||
const parsedDate = dayjs.unix(parseInt(timestamp, 10))
|
||||
if (!parsedDate.isValid()) {
|
||||
return 'invalid date'
|
||||
}
|
||||
return dayjs().to(parsedDate).toLowerCase()
|
||||
}
|
||||
|
||||
export { parseDate, renderDateBadge, formatDate, formatRelativeDate, age }
|
||||
132
services/date.spec.js
Normal file
132
services/date.spec.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import { expect } from 'chai'
|
||||
import { test, given } from 'sazerac'
|
||||
import sinon from 'sinon'
|
||||
import { parseDate, formatDate, formatRelativeDate, age } from './date.js'
|
||||
import { InvalidResponse } from './index.js'
|
||||
|
||||
describe('parseDate', function () {
|
||||
it('parses valid inputs', function () {
|
||||
expect(parseDate('2024-01-01').valueOf()).to.equal(
|
||||
new Date('2024-01-01').valueOf(),
|
||||
)
|
||||
expect(parseDate('Jan 01 01:00:00 2024 GMT').valueOf()).to.equal(
|
||||
new Date('2024-01-01T01:00:00.000Z').valueOf(),
|
||||
)
|
||||
expect(parseDate('31/01/2024', 'DD/MM/YYYY').valueOf()).to.equal(
|
||||
new Date('2024-01-31T00:00:00.000Z').valueOf(),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws when given invalid inputs', function () {
|
||||
// not a date
|
||||
expect(() => parseDate('foo')).to.throw(InvalidResponse)
|
||||
expect(() => parseDate([])).to.throw(InvalidResponse)
|
||||
expect(() => parseDate(null)).to.throw(InvalidResponse)
|
||||
|
||||
// invalid dates (only works with format string)
|
||||
expect(() => parseDate('2024-02-31', 'YYYY-MM-DD')).to.throw(
|
||||
InvalidResponse,
|
||||
)
|
||||
expect(() => parseDate('2024-12-32', 'YYYY-MM-DD')).to.throw(
|
||||
InvalidResponse,
|
||||
)
|
||||
|
||||
// non-standard format with no format string
|
||||
expect(() => parseDate('31/01/2024')).to.throw(InvalidResponse)
|
||||
|
||||
// parse format doesn't match date
|
||||
expect(() => parseDate('2024-01-01', 'YYYYMMDDHHmmss')).to.throw(
|
||||
InvalidResponse,
|
||||
)
|
||||
})
|
||||
|
||||
test(formatDate, () => {
|
||||
given(1465513200000)
|
||||
.describe('when given a timestamp in june 2016')
|
||||
.expect('june 2016')
|
||||
})
|
||||
|
||||
context('in october', function () {
|
||||
let clock
|
||||
beforeEach(function () {
|
||||
clock = sinon.useFakeTimers(new Date(2017, 9, 15).getTime())
|
||||
})
|
||||
afterEach(function () {
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
test(formatDate, () => {
|
||||
given(new Date(2017, 0, 1).getTime())
|
||||
.describe('when given the beginning of this year')
|
||||
.expect('january')
|
||||
})
|
||||
})
|
||||
|
||||
context('in october', function () {
|
||||
let clock
|
||||
beforeEach(function () {
|
||||
clock = sinon.useFakeTimers(new Date(2018, 9, 29).getTime())
|
||||
})
|
||||
afterEach(function () {
|
||||
clock.restore()
|
||||
})
|
||||
|
||||
test(formatRelativeDate, () => {
|
||||
given(new Date(2018, 9, 31).getTime() / 1000)
|
||||
.describe('when given the end of october')
|
||||
.expect('in 2 days')
|
||||
})
|
||||
|
||||
test(formatRelativeDate, () => {
|
||||
given(new Date(2018, 9, 1).getTime() / 1000)
|
||||
.describe('when given the beginning of october')
|
||||
.expect('a month ago')
|
||||
})
|
||||
|
||||
test(formatRelativeDate, () => {
|
||||
given(9999999999999)
|
||||
.describe('when given invalid date')
|
||||
.expect('invalid date')
|
||||
})
|
||||
})
|
||||
|
||||
const monthsAgo = months => {
|
||||
const result = new Date()
|
||||
// This looks wack but it works.
|
||||
result.setMonth(result.getMonth() - months)
|
||||
return result
|
||||
}
|
||||
test(age, () => {
|
||||
given(Date.now())
|
||||
.describe('when given the current timestamp')
|
||||
.expect('brightgreen')
|
||||
given(new Date())
|
||||
.describe('when given the current Date')
|
||||
.expect('brightgreen')
|
||||
given(new Date(2001, 1, 1))
|
||||
.describe('when given a Date many years ago')
|
||||
.expect('red')
|
||||
given(monthsAgo(2))
|
||||
.describe('when given a Date two months ago')
|
||||
.expect('yellowgreen')
|
||||
given(monthsAgo(15))
|
||||
.describe('when given a Date 15 months ago')
|
||||
.expect('orange')
|
||||
// --- reversed --- //
|
||||
given(Date.now(), true)
|
||||
.describe('when given the current timestamp and reversed')
|
||||
.expect('red')
|
||||
given(new Date(), true)
|
||||
.describe('when given the current Date and reversed')
|
||||
.expect('red')
|
||||
given(new Date(2001, 1, 1), true)
|
||||
.describe('when given a Date many years ago and reversed')
|
||||
.expect('brightgreen')
|
||||
given(monthsAgo(2), true)
|
||||
.describe('when given a Date two months ago and reversed')
|
||||
.expect('yellow')
|
||||
given(monthsAgo(15), true)
|
||||
.describe('when given a Date 15 months ago and reversed')
|
||||
.expect('green')
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatRelativeDate } from '../text-formatters.js'
|
||||
import { formatRelativeDate } from '../date.js'
|
||||
import { BaseService, pathParams } from '../index.js'
|
||||
|
||||
const description = `
|
||||
|
||||
@@ -3,10 +3,10 @@ import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('docker version (valid, library)')
|
||||
.get('/_/memcached.json')
|
||||
.get('/docker/example-voting-app-vote.json')
|
||||
.expectBadge({
|
||||
label: 'version',
|
||||
message: isSemver,
|
||||
message: 'latest',
|
||||
})
|
||||
|
||||
t.create('docker version (valid, library with tag)')
|
||||
|
||||
@@ -160,12 +160,25 @@ t.create('query with parse error')
|
||||
})
|
||||
|
||||
// Example from https://stackoverflow.com/q/11670384/893113
|
||||
const badQuery =
|
||||
const invalidTokenQuery =
|
||||
"$[?(en|**|(@.object.property.one=='other') && (@.object.property.two=='something(abc/def)'))]"
|
||||
t.create('query with invalid token')
|
||||
.get(
|
||||
`.json?url=https://github.com/badges/shields/raw/master/package.json&query=${encodeURIComponent(
|
||||
badQuery,
|
||||
invalidTokenQuery,
|
||||
)}`,
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'custom badge',
|
||||
message: 'query not supported',
|
||||
color: 'red',
|
||||
})
|
||||
|
||||
const invalidEscapequery = "$.definitions.common.properties.['@id'].format"
|
||||
t.create('query with invalid escape')
|
||||
.get(
|
||||
`.json?url=https://raw.githubusercontent.com/json-ld/json-ld.org/refs/heads/main/schemas/jsonld-schema.json&query=${encodeURIComponent(
|
||||
invalidEscapequery,
|
||||
)}`,
|
||||
)
|
||||
.expectBadge({
|
||||
|
||||
@@ -70,7 +70,7 @@ export default class DynamicXml extends BaseService {
|
||||
|
||||
static defaultBadgeData = { label: 'custom badge' }
|
||||
|
||||
transform({ pathExpression, buffer }) {
|
||||
transform({ pathExpression, buffer, contentType = 'text/xml' }) {
|
||||
// e.g. //book[2]/@id
|
||||
const pathIsAttr = (
|
||||
pathExpression.split('/').slice(-1)[0] || ''
|
||||
@@ -78,14 +78,20 @@ export default class DynamicXml extends BaseService {
|
||||
|
||||
let parsed
|
||||
try {
|
||||
parsed = new DOMParser().parseFromString(buffer, 'text/xml')
|
||||
parsed = new DOMParser().parseFromString(buffer, contentType)
|
||||
} catch (e) {
|
||||
throw new InvalidResponse({ prettyMessage: e.message })
|
||||
}
|
||||
|
||||
let values
|
||||
try {
|
||||
values = xpath.select(pathExpression, parsed)
|
||||
if (contentType === 'text/html') {
|
||||
values = xpath
|
||||
.parse(pathExpression)
|
||||
.select({ node: parsed, isHtml: true })
|
||||
} else {
|
||||
values = xpath.select(pathExpression, parsed)
|
||||
}
|
||||
} catch (e) {
|
||||
throw new InvalidParameter({ prettyMessage: e.message })
|
||||
}
|
||||
@@ -122,16 +128,25 @@ export default class DynamicXml extends BaseService {
|
||||
}
|
||||
|
||||
async handle(_namedParams, { url, query: pathExpression, prefix, suffix }) {
|
||||
const { buffer } = await this._request({
|
||||
const { buffer, res } = await this._request({
|
||||
url,
|
||||
options: { headers: { Accept: 'application/xml, text/xml' } },
|
||||
httpErrors,
|
||||
logErrors: [],
|
||||
})
|
||||
|
||||
let contentType = 'text/xml'
|
||||
if (
|
||||
res.headers['content-type'] &&
|
||||
res.headers['content-type'].includes('text/html')
|
||||
) {
|
||||
contentType = 'text/html'
|
||||
}
|
||||
|
||||
const { values: value } = this.transform({
|
||||
pathExpression,
|
||||
buffer,
|
||||
contentType,
|
||||
})
|
||||
|
||||
return renderDynamicBadge({ value, prefix, suffix })
|
||||
|
||||
@@ -20,6 +20,29 @@ const exampleXml = `<?xml version="1.0"?>
|
||||
</catalog>
|
||||
`
|
||||
|
||||
const exampleHtml = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Herman Melville - Moby-Dick</h1>
|
||||
<div>
|
||||
<p>
|
||||
Availing himself of the mild, summer-cool weather that now reigned in these
|
||||
latitudes, and in preparation for the peculiarly active pursuits shortly to
|
||||
be anticipated, Perth, the begrimed, blistered old blacksmith, had not
|
||||
removed his portable forge to the hold again, after concluding his
|
||||
contributory work for Ahab's leg, but still retained it on deck, fast lashed
|
||||
to ringbolts by the foremast; being now almost incessantly invoked by the
|
||||
headsmen, and harpooneers, and bowsmen to do some little job for them;
|
||||
altering, or repairing, or new shaping their various weapons and boat
|
||||
furniture.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
describe('DynamicXml', function () {
|
||||
describe('transform()', function () {
|
||||
beforeEach(function () {
|
||||
@@ -126,5 +149,12 @@ describe('DynamicXml', function () {
|
||||
}).expect({
|
||||
values: ["XML Developer's Guide", '44.95'],
|
||||
})
|
||||
given({
|
||||
pathExpression: '//h1[1]',
|
||||
buffer: exampleHtml,
|
||||
contentType: 'text/html',
|
||||
}).expect({
|
||||
values: ['Herman Melville - Moby-Dick'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -215,3 +215,16 @@ t.create('query with type conversion to number')
|
||||
message: '44.95',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('query HTML document')
|
||||
.get(
|
||||
`.json?${queryString.stringify({
|
||||
url: 'https://httpbin.org/html',
|
||||
query: '//h1[1]',
|
||||
})}`,
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'custom badge',
|
||||
message: 'Herman Melville - Moby-Dick',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
@@ -48,7 +48,10 @@ export default superclass =>
|
||||
values = jp({ json: data, path: pathExpression, eval: false })
|
||||
} catch (e) {
|
||||
const { message } = e
|
||||
if (message.includes('prevented in JSONPath expression')) {
|
||||
if (
|
||||
message.includes('prevented in JSONPath expression') ||
|
||||
e instanceof TypeError
|
||||
) {
|
||||
throw new InvalidParameter({
|
||||
prettyMessage: 'query not supported',
|
||||
})
|
||||
@@ -57,7 +60,7 @@ export default superclass =>
|
||||
}
|
||||
}
|
||||
|
||||
if (!values.length) {
|
||||
if (!values || !values.length) {
|
||||
throw new InvalidResponse({ prettyMessage: 'no result' })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { pathParams } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import EclipseMarketplaceBase from './eclipse-marketplace-base.js'
|
||||
|
||||
@@ -34,19 +33,12 @@ export default class EclipseMarketplaceUpdate extends EclipseMarketplaceBase {
|
||||
|
||||
static defaultBadgeData = { label: 'updated' }
|
||||
|
||||
static render({ date }) {
|
||||
return {
|
||||
message: formatDate(date),
|
||||
color: ageColor(date),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ name }) {
|
||||
const { marketplace } = await this.fetch({
|
||||
name,
|
||||
schema: updateResponseSchema,
|
||||
})
|
||||
const date = 1000 * parseInt(marketplace.node.changed)
|
||||
return this.constructor.render({ date })
|
||||
return renderDateBadge(date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ import {
|
||||
optionalNonNegativeInteger,
|
||||
nonNegativeInteger,
|
||||
} from '../validators.js'
|
||||
import { addv } from '../text-formatters.js'
|
||||
import { version as versionColor } from '../color-formatters.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
@@ -46,13 +45,6 @@ export default class FDroid extends BaseJsonService {
|
||||
|
||||
static defaultBadgeData = { label: 'f-droid' }
|
||||
|
||||
static render({ version }) {
|
||||
return {
|
||||
message: addv(version),
|
||||
color: versionColor(version),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ appId }) {
|
||||
const url = `https://f-droid.org/api/v1/packages/${appId}`
|
||||
return this._requestJson({
|
||||
@@ -83,6 +75,6 @@ export default class FDroid extends BaseJsonService {
|
||||
const json = await this.fetch({ appId })
|
||||
const suggested = includePre === undefined
|
||||
const { version } = this.transform({ json, suggested })
|
||||
return this.constructor.render({ version })
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService, pathParams } from '../index.js'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
@@ -131,18 +130,9 @@ class FactorioModPortalLastUpdated extends BaseFactorioModPortalService {
|
||||
|
||||
static defaultBadgeData = { label: 'last updated' }
|
||||
|
||||
static render({ lastUpdated }) {
|
||||
return {
|
||||
message: formatDate(lastUpdated),
|
||||
color: age(lastUpdated),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ modName }) {
|
||||
const resp = await this.fetch({ modName })
|
||||
return this.constructor.render({
|
||||
lastUpdated: resp.latest_release.released_at,
|
||||
})
|
||||
return renderDateBadge(resp.latest_release.released_at)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService, NotFound, pathParams } from '../index.js'
|
||||
import {
|
||||
renderVersionBadge,
|
||||
searchServiceUrl,
|
||||
stripBuildMetadata,
|
||||
selectVersion,
|
||||
} from '../nuget/nuget-helpers.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
|
||||
const singlePageSchema = Joi.object({
|
||||
'@id': Joi.string().required(),
|
||||
@@ -64,10 +64,6 @@ class FeedzVersionService extends BaseJsonService {
|
||||
label: 'feedz',
|
||||
}
|
||||
|
||||
static render(props) {
|
||||
return renderVersionBadge(props)
|
||||
}
|
||||
|
||||
apiUrl({ organization, repository }) {
|
||||
return `https://f.feedz.io/${organization}/${repository}/nuget`
|
||||
}
|
||||
@@ -122,9 +118,9 @@ class FeedzVersionService extends BaseJsonService {
|
||||
const json = await this.fetch({ baseUrl, packageName })
|
||||
const fetchedJson = await this.fetchItems({ json })
|
||||
const version = this.transform({ json: fetchedJson, includePrereleases })
|
||||
return this.constructor.render({
|
||||
return renderVersionBadge({
|
||||
version,
|
||||
feed: FeedzVersionService.defaultBadgeData.label,
|
||||
defaultLabel: FeedzVersionService.defaultBadgeData.label,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,6 @@ t.create('version (valid)')
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('version (yellow badge)')
|
||||
.get('/feedz/v/shieldstests/public/Shields.TestPreOnly.json')
|
||||
.expectBadge({
|
||||
label: 'feedz',
|
||||
message: 'v0.1.0-pre',
|
||||
color: 'yellow',
|
||||
})
|
||||
|
||||
t.create('version (orange badge)')
|
||||
.get('/feedz/v/shieldstests/public/Shields.NoV1.json')
|
||||
.expectBadge({
|
||||
@@ -77,14 +69,6 @@ t.create('version (pre) (valid)')
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('version (pre) (yellow badge)')
|
||||
.get('/feedz/vpre/shieldstests/public/Shields.TestPreOnly.json')
|
||||
.expectBadge({
|
||||
label: 'feedz',
|
||||
message: 'v0.1.0-pre',
|
||||
color: 'yellow',
|
||||
})
|
||||
|
||||
t.create('version (pre) (orange badge)')
|
||||
.get('/feedz/vpre/shieldstests/public/Shields.NoV1.json')
|
||||
.expectBadge({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { pathParams } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import BaseGalaxyToolshedService from './galaxytoolshed-base.js'
|
||||
|
||||
export default class GalaxyToolshedCreatedDate extends BaseGalaxyToolshedService {
|
||||
@@ -29,11 +29,6 @@ export default class GalaxyToolshedCreatedDate extends BaseGalaxyToolshedService
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'created date',
|
||||
color: 'blue',
|
||||
}
|
||||
|
||||
static render({ date }) {
|
||||
return { message: formatDate(date) }
|
||||
}
|
||||
|
||||
async handle({ repository, owner }) {
|
||||
@@ -42,6 +37,6 @@ export default class GalaxyToolshedCreatedDate extends BaseGalaxyToolshedService
|
||||
owner,
|
||||
})
|
||||
const { create_time: date } = response[0]
|
||||
return this.constructor.render({ date })
|
||||
return renderDateBadge(date, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { pathParam, queryParam } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { optionalUrl, relativeUri } from '../validators.js'
|
||||
import GiteaBase from './gitea-base.js'
|
||||
import { description, httpErrorsFor } from './gitea-helper.js'
|
||||
@@ -114,13 +113,6 @@ export default class GiteaLastCommit extends GiteaBase {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ user, repo, branch, baseUrl, path }) {
|
||||
// https://gitea.com/api/swagger#/repository
|
||||
return super.fetch({
|
||||
@@ -146,8 +138,6 @@ export default class GiteaLastCommit extends GiteaBase {
|
||||
baseUrl,
|
||||
path,
|
||||
})
|
||||
return this.constructor.render({
|
||||
commitDate: body[0].commit[displayTimestamp].date,
|
||||
})
|
||||
return renderDateBadge(body[0].commit[displayTimestamp].date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { pathParams } from '../../index.js'
|
||||
import { formatDate } from '../../text-formatters.js'
|
||||
import { age as ageColor } from '../../color-formatters.js'
|
||||
import { renderDateBadge } from '../../date.js'
|
||||
import { GithubAuthV3Service } from '../github-auth-service.js'
|
||||
import { documentation, httpErrorsFor } from '../github-helpers.js'
|
||||
|
||||
@@ -27,13 +26,6 @@ export default class GistLastCommit extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ gistId }) {
|
||||
return this._requestJson({
|
||||
url: `/gists/${gistId}`,
|
||||
@@ -44,6 +36,6 @@ export default class GistLastCommit extends GithubAuthV3Service {
|
||||
|
||||
async handle({ gistId }) {
|
||||
const { updated_at: commitDate } = await this.fetch({ gistId })
|
||||
return this.constructor.render({ commitDate })
|
||||
return renderDateBadge(commitDate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Joi from 'joi'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { pathParams } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import { documentation, httpErrorsFor } from './github-helpers.js'
|
||||
|
||||
@@ -34,14 +32,6 @@ export default class GithubCreatedAt extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'created at' }
|
||||
|
||||
static render({ createdAt }) {
|
||||
const date = dayjs(createdAt)
|
||||
return {
|
||||
message: formatDate(date),
|
||||
color: age(date, true),
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ user, repo }) {
|
||||
const { created_at: createdAt } = await this._requestJson({
|
||||
schema,
|
||||
@@ -49,6 +39,6 @@ export default class GithubCreatedAt extends GithubAuthV3Service {
|
||||
httpErrors: httpErrorsFor('repo not found'),
|
||||
})
|
||||
|
||||
return this.constructor.render({ createdAt })
|
||||
return renderDateBadge(createdAt, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import gql from 'graphql-tag'
|
||||
import Joi from 'joi'
|
||||
import dayjs from 'dayjs'
|
||||
import { pathParam, queryParam } from '../index.js'
|
||||
import { parseDate } from '../date.js'
|
||||
import { metric, maybePluralize } from '../text-formatters.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { GithubAuthV4Service } from './github-auth-service.js'
|
||||
@@ -52,7 +53,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
static category = 'issue-tracking'
|
||||
static route = {
|
||||
base: 'github/hacktoberfest',
|
||||
pattern: ':year(2019|2020|2021|2022|2023)/:user/:repo',
|
||||
pattern: ':year(2019|2020|2021|2022|2023|2024)/:user/:repo',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'year',
|
||||
example: '2023',
|
||||
example: '2024',
|
||||
schema: { type: 'string', enum: this.getEnum('year') },
|
||||
}),
|
||||
pathParam({ name: 'user', example: 'tmrowco' }),
|
||||
@@ -97,7 +98,7 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
// The global cutoff time is 11/1 noon UTC.
|
||||
// https://github.com/badges/shields/pull/4109#discussion_r330782093
|
||||
// We want to show "1 day left" on the last day so we add 1.
|
||||
daysLeft = dayjs(`${year}-11-01 12:00:00 Z`).diff(dayjs(), 'days') + 1
|
||||
daysLeft = parseDate(`${year}-11-01 12:00:00 Z`).diff(dayjs(), 'days') + 1
|
||||
}
|
||||
if (daysLeft < 0) {
|
||||
return {
|
||||
@@ -181,7 +182,10 @@ export default class GithubHacktoberfestCombinedStatus extends GithubAuthV4Servi
|
||||
}
|
||||
|
||||
static getCalendarPosition(year) {
|
||||
const daysToStart = dayjs(`${year}-10-01 00:00:00 Z`).diff(dayjs(), 'days')
|
||||
const daysToStart = parseDate(`${year}-10-01 00:00:00 Z`).diff(
|
||||
dayjs(),
|
||||
'days',
|
||||
)
|
||||
const isBefore = daysToStart > 0
|
||||
return { daysToStart, isBefore }
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ import { colorScale } from '../color-formatters.js'
|
||||
import { InvalidResponse, NotFound } from '../index.js'
|
||||
|
||||
const documentation = `
|
||||
If your GitHub badge errors, it might be because you hit GitHub's rate limits.
|
||||
You can increase Shields.io's rate limit by
|
||||
[adding the Shields GitHub application](https://img.shields.io/github-auth)
|
||||
using your GitHub account.
|
||||
You can help increase Shields.io's rate limit by
|
||||
[authorizing the Shields.io GitHub application](https://img.shields.io/github-auth).
|
||||
Read more about [how it works](/blog/token-pool).
|
||||
`
|
||||
|
||||
function issueStateColor(s) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { formatDate, metric } from '../text-formatters.js'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { InvalidResponse, pathParams } from '../index.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import {
|
||||
@@ -133,11 +133,13 @@ const ageUpdateMap = {
|
||||
}).required(),
|
||||
transform: ({ json, property }) =>
|
||||
property === 'age' ? json.created_at : json.updated_at,
|
||||
render: ({ property, value }) => ({
|
||||
color: age(value),
|
||||
label: property === 'age' ? 'created' : 'updated',
|
||||
message: formatDate(value),
|
||||
}),
|
||||
render: ({ property, value }) => {
|
||||
const label = property === 'age' ? 'created' : 'updated'
|
||||
return {
|
||||
...renderDateBadge(value),
|
||||
label,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const milestoneMap = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from 'chai'
|
||||
import { test, given } from 'sazerac'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { formatDate, metric } from '../text-formatters.js'
|
||||
import { age, formatDate } from '../date.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { InvalidResponse } from '../index.js'
|
||||
import GithubIssueDetail from './github-issue-detail.service.js'
|
||||
import { issueStateColor, commentsColor } from './github-helpers.js'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { relativeUri } from '../validators.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import { documentation, httpErrorsFor } from './github-helpers.js'
|
||||
@@ -88,13 +87,6 @@ export default class GithubLastCommit extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ user, repo, branch, path }) {
|
||||
return this._requestJson({
|
||||
url: `/repos/${user}/${repo}/commits`,
|
||||
@@ -111,8 +103,6 @@ export default class GithubLastCommit extends GithubAuthV3Service {
|
||||
|
||||
if (!commit) throw new NotFound({ prettyMessage: 'no commits found' })
|
||||
|
||||
return this.constructor.render({
|
||||
commitDate: commit[displayTimestamp].date,
|
||||
})
|
||||
return renderDateBadge(commit[displayTimestamp].date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ export const t = new ServiceTester({
|
||||
t.create('Manifest version')
|
||||
.get('/v/sindresorhus/show-all-github-issues.json')
|
||||
.expectBadge({
|
||||
label: 'version',
|
||||
label: 'manifest',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
t.create('Manifest version (path)')
|
||||
.get('/v/RedSparr0w/IndieGala-Helper.json?filename=extension/manifest.json')
|
||||
.expectBadge({
|
||||
label: 'version',
|
||||
label: 'manifest',
|
||||
message: isVPlusDottedVersionAtLeastOne,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Joi from 'joi'
|
||||
import { ServiceTester } from '../tester.js'
|
||||
import {
|
||||
isCommitHash,
|
||||
isVPlusDottedVersionAtLeastOne,
|
||||
isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../test-validators.js'
|
||||
|
||||
// e.g. v19.3b0
|
||||
const isBlackVersion = Joi.string().regex(/^v\d+(\.\d+)*(.*)?$/)
|
||||
const isShortSha = Joi.string().regex(/[0-9a-f]{7}/)
|
||||
|
||||
export const t = new ServiceTester({
|
||||
id: 'GithubPipenv',
|
||||
@@ -82,10 +82,8 @@ t.create('Locked version of unknown dependency')
|
||||
})
|
||||
|
||||
t.create('Locked version of VCS dependency')
|
||||
.get(
|
||||
'/locked/dependency-version/thorn-oss/perception/dev/videoalignment.json',
|
||||
)
|
||||
.get('/locked/dependency-version/pypa/pipenv/dev/pypiserver.json')
|
||||
.expectBadge({
|
||||
label: 'videoalignment',
|
||||
message: isShortSha,
|
||||
label: 'pypiserver',
|
||||
message: isCommitHash,
|
||||
})
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Joi from 'joi'
|
||||
import { pathParam, queryParam } from '../index.js'
|
||||
import { age } from '../color-formatters.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import { documentation, httpErrorsFor } from './github-helpers.js'
|
||||
|
||||
@@ -63,14 +61,6 @@ export default class GithubReleaseDate extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'release date' }
|
||||
|
||||
static render({ date }) {
|
||||
const releaseDate = dayjs(date)
|
||||
return {
|
||||
message: formatDate(releaseDate),
|
||||
color: age(releaseDate),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ variant, user, repo }) {
|
||||
const url =
|
||||
variant === 'release-date'
|
||||
@@ -86,10 +76,8 @@ export default class GithubReleaseDate extends GithubAuthV3Service {
|
||||
async handle({ variant, user, repo }, queryParams) {
|
||||
const body = await this.fetch({ variant, user, repo })
|
||||
if (Array.isArray(body)) {
|
||||
return this.constructor.render({
|
||||
date: body[0][queryParams.display_date],
|
||||
})
|
||||
return renderDateBadge(body[0][queryParams.display_date])
|
||||
}
|
||||
return this.constructor.render({ date: body[queryParams.display_date] })
|
||||
return renderDateBadge(body[queryParams.display_date])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { addv } from '../text-formatters.js'
|
||||
import { version as versionColor } from '../color-formatters.js'
|
||||
import { redirector, pathParam, queryParam } from '../index.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { GithubAuthV3Service } from './github-auth-service.js'
|
||||
import {
|
||||
fetchLatestRelease,
|
||||
@@ -46,13 +45,6 @@ class GithubRelease extends GithubAuthV3Service {
|
||||
|
||||
static defaultBadgeData = { label: 'release' }
|
||||
|
||||
static render({ version, sort, isPrerelease }) {
|
||||
let color = 'blue'
|
||||
color = sort === 'semver' ? versionColor(version) : color
|
||||
color = isPrerelease ? 'orange' : color
|
||||
return { message: addv(version), color }
|
||||
}
|
||||
|
||||
static transform(latestRelease, display) {
|
||||
const { name, tag_name: tagName, prerelease: isPrerelease } = latestRelease
|
||||
if (display === 'tag') {
|
||||
@@ -72,9 +64,8 @@ class GithubRelease extends GithubAuthV3Service {
|
||||
latestRelease,
|
||||
queryParams.display_name,
|
||||
)
|
||||
return this.constructor.render({
|
||||
return renderVersionBadge({
|
||||
version,
|
||||
sort: queryParams.sort,
|
||||
isPrerelease,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
import Joi from 'joi'
|
||||
import { matcher } from 'matcher'
|
||||
import { addv } from '../text-formatters.js'
|
||||
import { version as versionColor } from '../color-formatters.js'
|
||||
import { latest } from '../version.js'
|
||||
import { latest, renderVersionBadge } from '../version.js'
|
||||
import { NotFound, redirector, pathParam } from '../index.js'
|
||||
import { GithubAuthV4Service } from './github-auth-service.js'
|
||||
import {
|
||||
@@ -55,13 +53,6 @@ class GithubTag extends GithubAuthV4Service {
|
||||
label: 'tag',
|
||||
}
|
||||
|
||||
static render({ version, sort }) {
|
||||
return {
|
||||
message: addv(version),
|
||||
color: sort === 'semver' ? versionColor(version) : 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
static getLimit({ sort, filter }) {
|
||||
if (!filter && sort === 'date') {
|
||||
return 1
|
||||
@@ -123,13 +114,12 @@ class GithubTag extends GithubAuthV4Service {
|
||||
const prettyMessage = filter ? 'no matching tags found' : 'no tags found'
|
||||
throw new NotFound({ prettyMessage })
|
||||
}
|
||||
return this.constructor.render({
|
||||
return renderVersionBadge({
|
||||
version: this.constructor.getLatestTag({
|
||||
tags,
|
||||
sort,
|
||||
includePrereleases,
|
||||
}),
|
||||
sort,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,17 +43,6 @@ describe('GithubTag', function () {
|
||||
}).expect('1.2.0-beta')
|
||||
})
|
||||
|
||||
test(GithubTag.render, () => {
|
||||
given({ usingSemver: false, version: '1.2.3' }).expect({
|
||||
message: 'v1.2.3',
|
||||
color: 'blue',
|
||||
})
|
||||
given({ usingSemver: true, version: '2.0.0' }).expect({
|
||||
message: 'v2.0.0',
|
||||
color: 'blue',
|
||||
})
|
||||
})
|
||||
|
||||
test(GithubTag.getLimit, () => {
|
||||
given({ sort: 'date', filter: undefined }).expect(1)
|
||||
given({ sort: 'date', filter: '' }).expect(1)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { age as ageColor } from '../color-formatters.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import { NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { formatDate } from '../text-formatters.js'
|
||||
import { optionalUrl, relativeUri } from '../validators.js'
|
||||
import GitLabBase from './gitlab-base.js'
|
||||
import { description, httpErrorsFor } from './gitlab-helper.js'
|
||||
@@ -66,13 +65,6 @@ export default class GitlabLastCommit extends GitLabBase {
|
||||
|
||||
static defaultBadgeData = { label: 'last commit' }
|
||||
|
||||
static render({ commitDate }) {
|
||||
return {
|
||||
message: formatDate(commitDate),
|
||||
color: ageColor(Date.parse(commitDate)),
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ project, baseUrl, ref, path }) {
|
||||
// https://docs.gitlab.com/ee/api/commits.html#list-repository-commits
|
||||
return super.fetch({
|
||||
@@ -94,6 +86,6 @@ export default class GitlabLastCommit extends GitLabBase {
|
||||
|
||||
if (!commit) throw new NotFound({ prettyMessage: 'no commits found' })
|
||||
|
||||
return this.constructor.render({ commitDate: commit.committed_date })
|
||||
return renderDateBadge(commit.committed_date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { version as versionColor } from '../color-formatters.js'
|
||||
import { optionalUrl } from '../validators.js'
|
||||
import { latest } from '../version.js'
|
||||
import { addv } from '../text-formatters.js'
|
||||
import { latest, renderVersionBadge } from '../version.js'
|
||||
import { NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { description, httpErrorsFor } from './gitlab-helper.js'
|
||||
import GitLabBase from './gitlab-base.js'
|
||||
@@ -63,13 +61,6 @@ export default class GitlabTag extends GitLabBase {
|
||||
|
||||
static defaultBadgeData = { label: 'tag' }
|
||||
|
||||
static render({ version, sort }) {
|
||||
return {
|
||||
message: addv(version),
|
||||
color: sort === 'semver' ? versionColor(version) : 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ project, baseUrl }) {
|
||||
// https://docs.gitlab.com/ee/api/tags.html
|
||||
// N.B. the documentation has contradictory information about default sort order.
|
||||
@@ -114,6 +105,6 @@ export default class GitlabTag extends GitLabBase {
|
||||
sort,
|
||||
includePrereleases: pre !== undefined,
|
||||
})
|
||||
return this.constructor.render({ version, sort })
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ describe('GitLabTag', function () {
|
||||
).to.deep.equal({
|
||||
message: 'v1.9',
|
||||
color: 'blue',
|
||||
label: undefined,
|
||||
})
|
||||
|
||||
scope.done()
|
||||
|
||||
@@ -1,48 +1,11 @@
|
||||
import { BaseService, pathParams } from '../index.js'
|
||||
import { deprecatedService } from '../index.js'
|
||||
|
||||
export default class HackageDeps extends BaseService {
|
||||
static category = 'dependencies'
|
||||
|
||||
static route = {
|
||||
export const HackageDeps = deprecatedService({
|
||||
category: 'dependencies',
|
||||
route: {
|
||||
base: 'hackage-deps/v',
|
||||
pattern: ':packageName',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/hackage-deps/v/{packageName}': {
|
||||
get: {
|
||||
summary: 'Hackage Dependencies',
|
||||
parameters: pathParams({
|
||||
name: 'packageName',
|
||||
example: 'lens',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'dependencies' }
|
||||
|
||||
static render({ isOutdated }) {
|
||||
if (isOutdated) {
|
||||
return { message: 'outdated', color: 'orange' }
|
||||
} else {
|
||||
return { message: 'up to date', color: 'brightgreen' }
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ packageName }) {
|
||||
const reverseUrl = `http://packdeps.haskellers.com/licenses/${packageName}`
|
||||
const feedUrl = `http://packdeps.haskellers.com/feed/${packageName}`
|
||||
|
||||
// first call /reverse to check if the package exists
|
||||
// this will throw a 404 if it doesn't
|
||||
await this._request({ url: reverseUrl })
|
||||
|
||||
// if the package exists, then query /feed to check the dependencies
|
||||
const { buffer } = await this._request({ url: feedUrl })
|
||||
|
||||
const outdatedStr = `Outdated dependencies for ${packageName} `
|
||||
const isOutdated = buffer.includes(outdatedStr)
|
||||
return this.constructor.render({ isOutdated })
|
||||
}
|
||||
}
|
||||
},
|
||||
label: 'hackagedeps',
|
||||
dateAdded: new Date('2024-10-18'),
|
||||
})
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import Joi from 'joi'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
import { ServiceTester } from '../tester.js'
|
||||
export const t = new ServiceTester({
|
||||
id: 'hackagedeps',
|
||||
title: 'Hackage Dependencies',
|
||||
pathPrefix: '/hackage-deps/v',
|
||||
})
|
||||
|
||||
t.create('hackage deps (valid)')
|
||||
.get('/lens.json')
|
||||
.expectBadge({
|
||||
label: 'dependencies',
|
||||
message: Joi.string().regex(/^(up to date|outdated)$/),
|
||||
})
|
||||
|
||||
t.create('hackage deps (not found)')
|
||||
.get('/not-a-package.json')
|
||||
.expectBadge({ label: 'dependencies', message: 'not found' })
|
||||
t.create('hackage deps (deprecated)')
|
||||
.get('/package.json')
|
||||
.expectBadge({ label: 'hackagedeps', message: 'no longer available' })
|
||||
|
||||
82
services/homebrew/homebrew-cask-downloads.service.js
Normal file
82
services/homebrew/homebrew-cask-downloads.service.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import Joi from 'joi'
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import { BaseJsonService, pathParams } from '../index.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
|
||||
function getSchema({ cask }) {
|
||||
return Joi.object({
|
||||
analytics: Joi.object({
|
||||
install: Joi.object({
|
||||
'30d': Joi.object({ [cask]: nonNegativeInteger }).required(),
|
||||
'90d': Joi.object({ [cask]: nonNegativeInteger }).required(),
|
||||
'365d': Joi.object({ [cask]: nonNegativeInteger }).required(),
|
||||
}).required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
}
|
||||
|
||||
const periodMap = {
|
||||
dm: {
|
||||
api_field: '30d',
|
||||
interval: 'month',
|
||||
},
|
||||
dq: {
|
||||
api_field: '90d',
|
||||
interval: 'quarter',
|
||||
},
|
||||
dy: {
|
||||
api_field: '365d',
|
||||
interval: 'year',
|
||||
},
|
||||
}
|
||||
|
||||
export default class HomebrewCaskDownloads extends BaseJsonService {
|
||||
static category = 'downloads'
|
||||
|
||||
static route = {
|
||||
base: 'homebrew/cask/installs',
|
||||
pattern: ':interval(dm|dq|dy)/:cask',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/homebrew/cask/installs/{interval}/{cask}': {
|
||||
get: {
|
||||
summary: 'Homebrew Cask Downloads',
|
||||
parameters: pathParams(
|
||||
{
|
||||
name: 'interval',
|
||||
example: 'dm',
|
||||
schema: { type: 'string', enum: this.getEnum('interval') },
|
||||
description: 'Monthly, Quarterly or Yearly downloads',
|
||||
},
|
||||
{
|
||||
name: 'cask',
|
||||
example: 'freetube',
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'downloads' }
|
||||
|
||||
async fetch({ cask }) {
|
||||
const schema = getSchema({ cask })
|
||||
return this._requestJson({
|
||||
schema,
|
||||
url: `https://formulae.brew.sh/api/cask/${cask}.json`,
|
||||
httpErrors: { 404: 'cask not found' },
|
||||
})
|
||||
}
|
||||
|
||||
async handle({ interval, cask }) {
|
||||
const {
|
||||
analytics: { install },
|
||||
} = await this.fetch({ cask })
|
||||
|
||||
return renderDownloadsBadge({
|
||||
downloads: install[periodMap[interval].api_field][cask],
|
||||
interval: periodMap[interval].interval,
|
||||
})
|
||||
}
|
||||
}
|
||||
28
services/homebrew/homebrew-cask-downloads.tester.js
Normal file
28
services/homebrew/homebrew-cask-downloads.tester.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isMetricOverTimePeriod } from '../test-validators.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('daily downloads (valid)')
|
||||
.get('/dm/freetube.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('yearly downloads (valid)')
|
||||
.get('/dq/freetube.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('yearly downloads (valid)')
|
||||
.get('/dy/freetube.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetricOverTimePeriod })
|
||||
|
||||
t.create('daily downloads (not found)')
|
||||
.get('/dm/not-a-package.json')
|
||||
.expectBadge({ label: 'downloads', message: 'cask not found' })
|
||||
|
||||
t.create('yearly downloads (not found)')
|
||||
.get('/dq/not-a-package.json')
|
||||
.expectBadge({ label: 'downloads', message: 'cask not found' })
|
||||
|
||||
t.create('yearly downloads (not found)')
|
||||
.get('/dy/not-a-package.json')
|
||||
.expectBadge({ label: 'downloads', message: 'cask not found' })
|
||||
@@ -13,7 +13,7 @@ export default class HomebrewCask extends BaseJsonService {
|
||||
static openApi = {
|
||||
'/homebrew/cask/v/{cask}': {
|
||||
get: {
|
||||
summary: 'homebrew cask',
|
||||
summary: 'Homebrew Cask Version',
|
||||
parameters: pathParams({
|
||||
name: 'cask',
|
||||
example: 'iterm2',
|
||||
@@ -41,7 +41,7 @@ export default class HomebrewDownloads extends BaseJsonService {
|
||||
static openApi = {
|
||||
'/homebrew/installs/{interval}/{formula}': {
|
||||
get: {
|
||||
summary: 'homebrew downloads',
|
||||
summary: 'Homebrew Formula Downloads',
|
||||
parameters: pathParams(
|
||||
{
|
||||
name: 'interval',
|
||||
@@ -16,7 +16,7 @@ export default class HomebrewVersion extends BaseJsonService {
|
||||
static openApi = {
|
||||
'/homebrew/v/{formula}': {
|
||||
get: {
|
||||
summary: 'homebrew version',
|
||||
summary: 'Homebrew Formula Version',
|
||||
parameters: pathParams({
|
||||
name: 'formula',
|
||||
example: 'cake',
|
||||
@@ -68,7 +68,7 @@ export default class JenkinsPluginInstalls extends BaseJsonService {
|
||||
|
||||
async fetch({ plugin, version }) {
|
||||
return this._requestJson({
|
||||
url: `https://old.stats.jenkins.io/plugin-installation-trend/${plugin}.stats.json`,
|
||||
url: `https://stats.jenkins.io/plugin-installation-trend/${plugin}.stats.json`,
|
||||
schema: version ? schemaInstallationsPerVersion : schemaInstallations,
|
||||
httpErrors: {
|
||||
404: 'plugin not found',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Joi from 'joi'
|
||||
import { addv } from '../text-formatters.js'
|
||||
import { BaseJsonService, NotFound, pathParams } from '../index.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { latestVersion } from './luarocks-version-helpers.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
@@ -42,25 +42,6 @@ export default class Luarocks extends BaseJsonService {
|
||||
label: 'luarocks',
|
||||
}
|
||||
|
||||
static render({ version }) {
|
||||
// The badge colors are following the heuristic rule where `scm < dev <
|
||||
// stable` (e.g., `scm-1` < `dev-1` < `0.1.0-1`).
|
||||
let color
|
||||
switch (version.slice(0, 3).toLowerCase()) {
|
||||
case 'dev':
|
||||
color = 'yellow'
|
||||
break
|
||||
case 'scm':
|
||||
case 'cvs':
|
||||
color = 'orange'
|
||||
break
|
||||
default:
|
||||
color = 'brightgreen'
|
||||
}
|
||||
|
||||
return { message: addv(version), color }
|
||||
}
|
||||
|
||||
async fetch({ user, moduleName }) {
|
||||
const { repository } = await this._requestJson({
|
||||
url: `https://luarocks.org/manifests/${encodeURIComponent(
|
||||
@@ -91,6 +72,6 @@ export default class Luarocks extends BaseJsonService {
|
||||
const versions = Object.keys(moduleInfo)
|
||||
version = latestVersion(versions)
|
||||
}
|
||||
return this.constructor.render({ version })
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import Luarocks from './luarocks.service.js'
|
||||
|
||||
test(Luarocks.render, () => {
|
||||
given({ version: 'dev-1' }).expect({ message: 'dev-1', color: 'yellow' })
|
||||
given({ version: 'scm-1' }).expect({ message: 'scm-1', color: 'orange' })
|
||||
given({ version: 'cvs-1' }).expect({ message: 'cvs-1', color: 'orange' })
|
||||
given({ version: '0.1-1' }).expect({
|
||||
message: 'v0.1-1',
|
||||
color: 'brightgreen',
|
||||
})
|
||||
})
|
||||
13
services/maven-central/maven-central-base.js
Normal file
13
services/maven-central/maven-central-base.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { BaseXmlService } from '../index.js'
|
||||
|
||||
export default class MavenCentralBase extends BaseXmlService {
|
||||
async fetch({ groupId, artifactId, schema }) {
|
||||
const group = encodeURIComponent(groupId).replace(/\./g, '/')
|
||||
const artifact = encodeURIComponent(artifactId)
|
||||
return this._requestXml({
|
||||
schema,
|
||||
url: `https://repo1.maven.org/maven2/${group}/${artifact}/maven-metadata.xml`,
|
||||
httpErrors: { 404: 'artifact not found' },
|
||||
})
|
||||
}
|
||||
}
|
||||
51
services/maven-central/maven-central-last-update.service.js
Normal file
51
services/maven-central/maven-central-last-update.service.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import Joi from 'joi'
|
||||
import { pathParams } from '../index.js'
|
||||
import { parseDate, renderDateBadge } from '../date.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import MavenCentralBase from './maven-central-base.js'
|
||||
|
||||
const updateResponseSchema = Joi.object({
|
||||
metadata: Joi.object({
|
||||
versioning: Joi.object({
|
||||
lastUpdated: nonNegativeInteger,
|
||||
}).required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
export default class MavenCentralLastUpdate extends MavenCentralBase {
|
||||
static category = 'activity'
|
||||
|
||||
static route = {
|
||||
base: 'maven-central/last-update',
|
||||
pattern: ':groupId/:artifactId',
|
||||
}
|
||||
|
||||
static openApi = {
|
||||
'/maven-central/last-update/{groupId}/{artifactId}': {
|
||||
get: {
|
||||
summary: 'Maven Central Last Update',
|
||||
parameters: pathParams(
|
||||
{ name: 'groupId', example: 'com.google.guava' },
|
||||
{ name: 'artifactId', example: 'guava' },
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'last updated' }
|
||||
|
||||
async handle({ groupId, artifactId }) {
|
||||
const { metadata } = await this.fetch({
|
||||
groupId,
|
||||
artifactId,
|
||||
schema: updateResponseSchema,
|
||||
})
|
||||
|
||||
const date = parseDate(
|
||||
String(metadata.versioning.lastUpdated),
|
||||
'YYYYMMDDHHmmss',
|
||||
)
|
||||
|
||||
return renderDateBadge(date)
|
||||
}
|
||||
}
|
||||
15
services/maven-central/maven-central-last-update.tester.js
Normal file
15
services/maven-central/maven-central-last-update.tester.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { isFormattedDate } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('last update date').get('/com.google.guava/guava.json').expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last update when artifact not found')
|
||||
.get('/com.fail.test/this-does-not-exist.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'artifact not found',
|
||||
})
|
||||
@@ -3,12 +3,6 @@ import {
|
||||
isMetric,
|
||||
isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
} from '../test-validators.js'
|
||||
import {
|
||||
queryIndex,
|
||||
nuGetV3VersionJsonWithDash,
|
||||
nuGetV3VersionJsonFirstCharZero,
|
||||
nuGetV3VersionJsonFirstCharNotZero,
|
||||
} from '../nuget-fixtures.js'
|
||||
import { invalidJSON } from '../response-fixtures.js'
|
||||
|
||||
export const t = new ServiceTester({
|
||||
@@ -75,66 +69,6 @@ t.create('version (tenant)')
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
})
|
||||
|
||||
t.create('version (yellow badge)')
|
||||
.get('/myget/mongodb/v/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonWithDash),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v1.2-beta',
|
||||
color: 'yellow',
|
||||
})
|
||||
|
||||
t.create('version (orange badge)')
|
||||
.get('/myget/mongodb/v/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v0.35',
|
||||
color: 'orange',
|
||||
})
|
||||
|
||||
t.create('version (blue badge)')
|
||||
.get('/myget/mongodb/v/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharNotZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v1.2.7',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('version (not found)')
|
||||
.get('/myget/foo/v/not-a-real-package.json')
|
||||
.expectBadge({ label: 'myget', message: 'package not found' })
|
||||
@@ -148,66 +82,6 @@ t.create('version (pre) (valid)')
|
||||
message: isVPlusDottedVersionNClausesWithOptionalSuffix,
|
||||
})
|
||||
|
||||
t.create('version (pre) (yellow badge)')
|
||||
.get('/myget/mongodb/vpre/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonWithDash),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v1.2-beta',
|
||||
color: 'yellow',
|
||||
})
|
||||
|
||||
t.create('version (pre) (orange badge)')
|
||||
.get('/myget/mongodb/vpre/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v0.35',
|
||||
color: 'orange',
|
||||
})
|
||||
|
||||
t.create('version (pre) (blue badge)')
|
||||
.get('/myget/mongodb/vpre/MongoDB.Driver.Core.json')
|
||||
.intercept(nock =>
|
||||
nock('https://www.myget.org')
|
||||
.get('/F/mongodb/api/v3/index.json')
|
||||
.reply(200, queryIndex),
|
||||
)
|
||||
.intercept(nock =>
|
||||
nock('https://api-v2v3search-0.nuget.org')
|
||||
.get(
|
||||
'/query?q=packageid%3Amongodb.driver.core&prerelease=true&semVerLevel=2',
|
||||
)
|
||||
.reply(200, nuGetV3VersionJsonFirstCharNotZero),
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'mongodb',
|
||||
message: 'v1.2.7',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
t.create('version (pre) (not found)')
|
||||
.get('/myget/foo/vpre/not-a-real-package.json')
|
||||
.expectBadge({ label: 'myget', message: 'package not found' })
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Joi from 'joi'
|
||||
import { version as versionColor } from '../color-formatters.js'
|
||||
import { addv } from '../text-formatters.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import {
|
||||
optionalUrl,
|
||||
optionalDottedVersionNClausesWithOptionalSuffix,
|
||||
@@ -143,13 +142,6 @@ export default class Nexus extends BaseJsonService {
|
||||
label: 'nexus',
|
||||
}
|
||||
|
||||
static render({ version }) {
|
||||
return {
|
||||
message: addv(version),
|
||||
color: versionColor(version),
|
||||
}
|
||||
}
|
||||
|
||||
addQueryParamsToQueryString({ searchParams, queryOpt }) {
|
||||
// Users specify query options with 'key=value' pairs, using a
|
||||
// colon delimiter between pairs ([:k1=v1[:k2=v2[...]]]).
|
||||
@@ -321,6 +313,6 @@ export default class Nexus extends BaseJsonService {
|
||||
})
|
||||
|
||||
const { version } = this.transform({ repo, json, actualNexusVersion })
|
||||
return this.constructor.render({ version })
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ describe('Nexus', function () {
|
||||
},
|
||||
),
|
||||
).to.deep.equal({
|
||||
label: undefined,
|
||||
message: 'v2.3.4',
|
||||
color: 'blue',
|
||||
})
|
||||
|
||||
@@ -258,7 +258,7 @@ t.create('Nexus 3 - search release version of an nonexistent artifact')
|
||||
|
||||
t.create('Nexus 3 - search snapshot version valid snapshot artifact')
|
||||
.get(
|
||||
'/s/com.tomkeuper.bedwars/bedwars-api.json?server=https://repo.tomkeuper.com&nexusVersion=3',
|
||||
'/s/net.voxelpi.event/event.json?server=https://repo.voxelpi.net&nexusVersion=3',
|
||||
)
|
||||
.expectBadge({
|
||||
label: 'nexus',
|
||||
|
||||
@@ -81,8 +81,11 @@ export default class NpmBase extends BaseJsonService {
|
||||
}
|
||||
|
||||
async _requestJson(data) {
|
||||
return super._requestJson(
|
||||
this.authHelper.withBearerAuthHeader({
|
||||
let payload
|
||||
if (data?.options?.headers?.Accept) {
|
||||
payload = data
|
||||
} else {
|
||||
payload = {
|
||||
...data,
|
||||
options: {
|
||||
headers: {
|
||||
@@ -91,8 +94,9 @@ export default class NpmBase extends BaseJsonService {
|
||||
Accept: '*/*',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
return super._requestJson(this.authHelper.withBearerAuthHeader(payload))
|
||||
}
|
||||
|
||||
async fetchPackageData({ registryUrl, scope, packageName, tag }) {
|
||||
@@ -143,4 +147,37 @@ export default class NpmBase extends BaseJsonService {
|
||||
|
||||
return this.constructor._validate(packageData, packageDataSchema)
|
||||
}
|
||||
|
||||
async fetch({
|
||||
registryUrl,
|
||||
scope,
|
||||
packageName,
|
||||
schema,
|
||||
abbreviated = false,
|
||||
}) {
|
||||
registryUrl = registryUrl || this.constructor.defaultRegistryUrl
|
||||
let url
|
||||
|
||||
if (scope === undefined) {
|
||||
url = `${registryUrl}/${packageName}`
|
||||
} else {
|
||||
const scoped = this.constructor.encodeScopedPackage({
|
||||
scope,
|
||||
packageName,
|
||||
})
|
||||
url = `${registryUrl}/${scoped}`
|
||||
}
|
||||
|
||||
// https://github.com/npm/registry/blob/main/docs/responses/package-metadata.md
|
||||
const options = abbreviated
|
||||
? { headers: { Accept: 'application/vnd.npm.install-v1+json' } }
|
||||
: {}
|
||||
|
||||
return this._requestJson({
|
||||
url,
|
||||
schema,
|
||||
options,
|
||||
httpErrors: { 404: 'package not found' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('npm', function () {
|
||||
await NpmVersion.invoke(defaultContext, config, { packageName: 'npm' }),
|
||||
).to.deep.equal({
|
||||
color: 'orange',
|
||||
label: undefined,
|
||||
label: 'npm',
|
||||
message: 'v0.1.0',
|
||||
})
|
||||
|
||||
|
||||
119
services/npm/npm-last-update.service.js
Normal file
119
services/npm/npm-last-update.service.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import Joi from 'joi'
|
||||
import { NotFound, pathParam, queryParam } from '../index.js'
|
||||
import { renderDateBadge } from '../date.js'
|
||||
import NpmBase, {
|
||||
packageNameDescription,
|
||||
queryParamSchema,
|
||||
} from './npm-base.js'
|
||||
|
||||
const fullSchema = Joi.object({
|
||||
time: Joi.object()
|
||||
.pattern(Joi.string().required(), Joi.string().required())
|
||||
.required(),
|
||||
'dist-tags': Joi.object()
|
||||
.pattern(Joi.string().required(), Joi.string().required())
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
const abbreviatedSchema = Joi.object({
|
||||
modified: Joi.string().required(),
|
||||
}).required()
|
||||
|
||||
export class NpmLastUpdateWithTag extends NpmBase {
|
||||
static category = 'activity'
|
||||
|
||||
static route = {
|
||||
base: 'npm/last-update',
|
||||
pattern: ':scope(@[^/]+)?/:packageName/:tag',
|
||||
queryParamSchema,
|
||||
}
|
||||
|
||||
static defaultBadgeData = { label: 'last updated' }
|
||||
|
||||
static openApi = {
|
||||
'/npm/last-update/{packageName}/{tag}': {
|
||||
get: {
|
||||
summary: 'NPM Last Update (with dist tag)',
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'packageName',
|
||||
example: 'verdaccio',
|
||||
packageNameDescription,
|
||||
}),
|
||||
pathParam({
|
||||
name: 'tag',
|
||||
example: 'next-8',
|
||||
}),
|
||||
queryParam({
|
||||
name: 'registry_uri',
|
||||
example: 'https://registry.npmjs.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async handle(namedParams, queryParams) {
|
||||
const { scope, packageName, tag, registryUrl } =
|
||||
this.constructor.unpackParams(namedParams, queryParams)
|
||||
|
||||
const packageData = await this.fetch({
|
||||
registryUrl,
|
||||
scope,
|
||||
packageName,
|
||||
schema: fullSchema,
|
||||
})
|
||||
|
||||
const tagVersion = packageData['dist-tags'][tag]
|
||||
|
||||
if (!tagVersion) {
|
||||
throw new NotFound({ prettyMessage: 'tag not found' })
|
||||
}
|
||||
|
||||
return renderDateBadge(packageData.time[tagVersion])
|
||||
}
|
||||
}
|
||||
|
||||
export class NpmLastUpdate extends NpmBase {
|
||||
static category = 'activity'
|
||||
|
||||
static route = this.buildRoute('npm/last-update', { withTag: false })
|
||||
|
||||
static defaultBadgeData = { label: 'last updated' }
|
||||
|
||||
static openApi = {
|
||||
'/npm/last-update/{packageName}': {
|
||||
get: {
|
||||
summary: 'NPM Last Update',
|
||||
parameters: [
|
||||
pathParam({
|
||||
name: 'packageName',
|
||||
example: 'verdaccio',
|
||||
packageNameDescription,
|
||||
}),
|
||||
queryParam({
|
||||
name: 'registry_uri',
|
||||
example: 'https://registry.npmjs.com',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async handle(namedParams, queryParams) {
|
||||
const { scope, packageName, registryUrl } = this.constructor.unpackParams(
|
||||
namedParams,
|
||||
queryParams,
|
||||
)
|
||||
|
||||
const packageData = await this.fetch({
|
||||
registryUrl,
|
||||
scope,
|
||||
packageName,
|
||||
schema: abbreviatedSchema,
|
||||
abbreviated: true,
|
||||
})
|
||||
|
||||
return renderDateBadge(packageData.modified)
|
||||
}
|
||||
}
|
||||
81
services/npm/npm-last-update.tester.js
Normal file
81
services/npm/npm-last-update.tester.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { isFormattedDate } from '../test-validators.js'
|
||||
import { createServiceTester } from '../tester.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
|
||||
t.create('last updated date, no tag, valid package')
|
||||
.get('/verdaccio.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, no tag, invalid package')
|
||||
.get('/not-a-package.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'package not found',
|
||||
})
|
||||
|
||||
t.create('last updated date, no tag, custom repository, valid package')
|
||||
.get('/verdaccio.json?registry_uri=https://registry.npmjs.com')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, no tag, valid package with scope')
|
||||
.get('/@npm/types.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, no tag, invalid package with scope')
|
||||
.get('/@not-a-scoped-package/not-a-valid-package.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'package not found',
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, valid package')
|
||||
.get('/verdaccio/latest.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, invalid package')
|
||||
.get('/not-a-package/doesnt-matter.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'package not found',
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, invalid tag')
|
||||
.get('/verdaccio/not-a-valid-tag.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'tag not found',
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, custom repository, valid package')
|
||||
.get('/verdaccio/latest.json?registry_uri=https://registry.npmjs.com')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, valid package with scope')
|
||||
.get('/@npm/types/latest.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: isFormattedDate,
|
||||
})
|
||||
|
||||
t.create('last updated date, with tag, invalid package with scope')
|
||||
.get('/@not-a-scoped-package/not-a-valid-package/doesnt-matter.json')
|
||||
.expectBadge({
|
||||
label: 'last updated',
|
||||
message: 'package not found',
|
||||
})
|
||||
@@ -1,57 +0,0 @@
|
||||
const queryIndex = JSON.stringify({
|
||||
resources: [
|
||||
{
|
||||
'@id': 'https://api-v2v3search-0.nuget.org/query',
|
||||
'@type': 'SearchQueryService',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const nuGetV3VersionJsonWithDash = JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
totalDownloads: 0,
|
||||
versions: [{ version: '1.2-beta' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
const nuGetV3VersionJsonFirstCharZero = JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
totalDownloads: 0,
|
||||
versions: [{ version: '0.35' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
const nuGetV3VersionJsonFirstCharNotZero = JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
totalDownloads: 0,
|
||||
versions: [{ version: '1.2.7' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const nuGetV3VersionJsonBuildMetadataWithDash = JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
totalDownloads: 0,
|
||||
versions: [
|
||||
{
|
||||
version: '1.16.0+388',
|
||||
},
|
||||
{
|
||||
version: '1.17.0+1b81349-429',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export {
|
||||
queryIndex,
|
||||
nuGetV3VersionJsonWithDash,
|
||||
nuGetV3VersionJsonFirstCharZero,
|
||||
nuGetV3VersionJsonFirstCharNotZero,
|
||||
nuGetV3VersionJsonBuildMetadataWithDash,
|
||||
}
|
||||
@@ -1,25 +1,8 @@
|
||||
import semver from 'semver'
|
||||
import { metric, addv } from '../text-formatters.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { downloadCount as downloadCountColor } from '../color-formatters.js'
|
||||
import { getCachedResource } from '../../core/base-service/resource-cache.js'
|
||||
|
||||
function renderVersionBadge({ version, feed }) {
|
||||
let color
|
||||
if (version.includes('-')) {
|
||||
color = 'yellow'
|
||||
} else if (version.startsWith('0')) {
|
||||
color = 'orange'
|
||||
} else {
|
||||
color = 'blue'
|
||||
}
|
||||
|
||||
return {
|
||||
message: addv(version),
|
||||
color,
|
||||
label: feed,
|
||||
}
|
||||
}
|
||||
|
||||
function renderDownloadBadge({ downloads }) {
|
||||
return {
|
||||
message: metric(downloads),
|
||||
@@ -100,7 +83,6 @@ function selectVersion(versions, includePrereleases) {
|
||||
}
|
||||
|
||||
export {
|
||||
renderVersionBadge,
|
||||
renderDownloadBadge,
|
||||
odataToObject,
|
||||
searchServiceUrl,
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
import { test, given } from 'sazerac'
|
||||
import {
|
||||
renderVersionBadge,
|
||||
odataToObject,
|
||||
stripBuildMetadata,
|
||||
selectVersion,
|
||||
} from './nuget-helpers.js'
|
||||
|
||||
describe('NuGet helpers', function () {
|
||||
test(renderVersionBadge, () => {
|
||||
given({ version: '1.2-beta' }).expect({
|
||||
label: undefined,
|
||||
message: 'v1.2-beta',
|
||||
color: 'yellow',
|
||||
})
|
||||
given({ version: '0.35' }).expect({
|
||||
label: undefined,
|
||||
message: 'v0.35',
|
||||
color: 'orange',
|
||||
})
|
||||
given({ version: '1.2.7' }).expect({
|
||||
label: undefined,
|
||||
message: 'v1.2.7',
|
||||
color: 'blue',
|
||||
})
|
||||
})
|
||||
|
||||
test(odataToObject, () => {
|
||||
given({ 'm:properties': { 'd:Version': '1.2.3' } }).expect({
|
||||
Version: '1.2.3',
|
||||
|
||||
@@ -9,11 +9,8 @@ import {
|
||||
pathParam,
|
||||
queryParam,
|
||||
} from '../index.js'
|
||||
import {
|
||||
renderVersionBadge,
|
||||
renderDownloadBadge,
|
||||
odataToObject,
|
||||
} from './nuget-helpers.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import { renderDownloadBadge, odataToObject } from './nuget-helpers.js'
|
||||
|
||||
function createFilter({ packageName, includePrereleases }) {
|
||||
const releaseTypeFilter = includePrereleases
|
||||
@@ -127,10 +124,6 @@ function createServiceFamily({
|
||||
label: defaultLabel,
|
||||
}
|
||||
|
||||
static render(props) {
|
||||
return renderVersionBadge(props)
|
||||
}
|
||||
|
||||
async handle({ packageName }, queryParams) {
|
||||
const packageData = await fetch(this, {
|
||||
baseUrl: apiBaseUrl,
|
||||
@@ -138,7 +131,7 @@ function createServiceFamily({
|
||||
includePrereleases: queryParams.include_prereleases !== undefined,
|
||||
})
|
||||
const version = packageData.NormalizedVersion || `${packageData.Version}`
|
||||
return this.constructor.render({ version })
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import RouteBuilder from '../route-builder.js'
|
||||
import { BaseJsonService, NotFound } from '../index.js'
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import {
|
||||
renderVersionBadge,
|
||||
renderDownloadBadge,
|
||||
searchServiceUrl,
|
||||
stripBuildMetadata,
|
||||
@@ -127,10 +127,6 @@ function createServiceFamily({
|
||||
label: defaultLabel,
|
||||
}
|
||||
|
||||
static render(props) {
|
||||
return renderVersionBadge(props)
|
||||
}
|
||||
|
||||
/*
|
||||
* Extract version information from the raw package info.
|
||||
*/
|
||||
@@ -158,7 +154,7 @@ function createServiceFamily({
|
||||
})
|
||||
const json = await fetch(this, { baseUrl, packageName })
|
||||
const version = this.transform({ json, includePrereleases })
|
||||
return this.constructor.render({ version, feed })
|
||||
return renderVersionBadge({ version, defaultLabel: feed })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user