Compare commits
206 commits
Author | SHA1 | Date | |
---|---|---|---|
d2253e857e | |||
d561fcf730 | |||
ca9bc34d39 | |||
f58439aa1a | |||
|
97852ced95 | ||
|
a970d8cea7 | ||
|
aeebe74ddd | ||
|
de2e2fc7b1 | ||
|
dfa1ab54e8 | ||
9a270df19e | |||
|
2c2893da21 | ||
|
0c49fe751a | ||
|
4042ded5ae | ||
|
a88977af94 | ||
|
558ce7daa8 | ||
|
c9a6d6786f | ||
|
db0a4fd5d4 | ||
|
db790e0811 | ||
|
c2ee6cd849 | ||
|
8417ea34a0 | ||
|
13ee5cc245 | ||
|
5b138fae11 | ||
|
1ce03ca19f | ||
|
cd73f4828f | ||
|
b3cc2b53dc | ||
|
55621fbbea | ||
|
d778789959 | ||
|
abe8f92ab8 | ||
|
2a2a8e0b31 | ||
|
c90116c9ba | ||
|
8e295a96de | ||
|
e87b71cc0d | ||
|
f6b0e06dc1 | ||
|
d28f9790b8 | ||
|
98ba21e797 | ||
|
3244de48fc | ||
|
1e26d04b06 | ||
|
7d9827f9fc | ||
|
f48c570215 | ||
|
b24136ec59 | ||
|
8e9c5aad48 | ||
|
5e83ab5106 | ||
|
94c589c209 | ||
|
f43f06c334 | ||
|
b15c4b009a | ||
|
5f517ae6c7 | ||
|
6fc7263f59 | ||
|
e9d1226390 | ||
e20f6fb92e | |||
|
1f9847b3ed | ||
|
ab3479f0d1 | ||
|
5fd14aada8 | ||
|
05f20f44ac | ||
|
389dd6f536 | ||
|
c99dfa8091 | ||
|
81c852126c | ||
|
8158497a73 | ||
|
52316093cd | ||
|
4cf0084e24 | ||
|
dffcd16a60 | ||
|
cab58d284e | ||
|
e37000143e | ||
|
b35cfe3f4f | ||
|
79623d9bc2 | ||
|
0b9498c541 | ||
|
34975924b1 | ||
|
a695cef57d | ||
|
c05eb562a9 | ||
|
8cb636ccc9 | ||
|
dd809ce3f3 | ||
|
44cc0c1010 | ||
|
a48af07d93 | ||
|
39a76eaa33 | ||
|
1f00bbfac5 | ||
|
8f89fc9953 | ||
|
a406bffabe | ||
|
3bda063578 | ||
|
2a1ebd7b85 | ||
|
d45e8e63c9 | ||
|
72e20d413e | ||
|
5aec8d677f | ||
|
b2070ec460 | ||
|
613815aa1e | ||
|
c82bccca90 | ||
|
1dc4fe225c | ||
|
e5b9105da1 | ||
|
2df196a2e6 | ||
|
cbd304c841 | ||
|
0b3c0325a5 | ||
|
600d18d05b | ||
|
f1d9897e83 | ||
|
992f995441 | ||
|
2e46d37950 | ||
|
9592da3d6e | ||
|
4f4716cdd8 | ||
68fec14155 | |||
|
5da4074bb9 | ||
|
51f098f4ad | ||
|
a2dc40f444 | ||
|
e63baec249 | ||
|
6ca7088b9c | ||
|
8309901a8c | ||
|
7cefbd4a67 | ||
|
a3f028f2ad | ||
|
7c684eb565 | ||
|
e436f77c3e | ||
|
2ed0cdc4a3 | ||
|
484f113646 | ||
|
23675124bd | ||
|
01c494c277 | ||
|
6f3544e3d8 | ||
89d22496d7 | |||
|
72d0cde370 | ||
|
dbcee4945c | ||
|
ca0734d470 | ||
|
923f97f8e9 | ||
|
82ca6dd6bf | ||
|
7a92941452 | ||
|
6619ef60e4 | ||
|
de4459f646 | ||
|
61f65e54db | ||
|
1d03f5b4b0 | ||
|
4eb2b22a8f | ||
|
a48e23bece | ||
|
f8a95c82e4 | ||
|
3f719ac939 | ||
|
97b9610f30 | ||
|
03597d10e5 | ||
|
f8c9d6f7cb | ||
|
22d5626bf0 | ||
|
73959c00f2 | ||
|
78179fd737 | ||
|
980e5968ea | ||
|
a88a3c6103 | ||
|
3cef317576 | ||
|
63978d79f9 | ||
|
4b3639aea7 | ||
|
8073e90f68 | ||
|
24171c9800 | ||
|
0c2b39a68a | ||
|
cc6a495998 | ||
|
ed86f30cf4 | ||
|
f9698b5a7f | ||
|
6b7d107387 | ||
|
ca174a3aa2 | ||
|
b9d0a29741 | ||
|
01e6a9c8ad | ||
|
7c2a97666d | ||
|
db67487abd | ||
83ae356b90 | |||
|
7e0aaa8362 | ||
756a86eda1 | |||
|
df805bfe83 | ||
60e6e6216f | |||
|
a7160fadde | ||
|
3ae7f8813e | ||
|
642930d397 | ||
|
057a365a0e | ||
|
b67b1730b1 | ||
|
a5d49733fd | ||
|
c5c9f85e4b | ||
|
88c635cbad | ||
|
1a5c056204 | ||
|
c9db056791 | ||
|
5f94794d24 | ||
|
f41a92243c | ||
|
a88f1ba91c | ||
|
13213861ba | ||
|
2e0626667e | ||
c06fb22913 | |||
|
06f9c278a8 | ||
97c61aaf9b | |||
197eeea75b | |||
7d2c68630e | |||
|
9767167661 | ||
|
2525022df7 | ||
|
7ebe881546 | ||
|
94191f878d | ||
|
595a9d0f41 | ||
|
cec3aa3fc0 | ||
|
797fc0c04c | ||
|
fc3b5a1530 | ||
a7a9b137a7 | |||
02fab65125 | |||
3ef67f52c8 | |||
40ae3a7f55 | |||
f0c7881d57 | |||
39f0019455 | |||
0516abb8fd | |||
3690b0244c | |||
bdc847f5d0 | |||
d8c3ab4f36 | |||
09e7c1d8bd | |||
1e36edf499 | |||
746f29fd20 | |||
14bceaa2c6 | |||
6ae7520111 | |||
f94cdcbd1f | |||
220c3767e0 | |||
5f3160e331 | |||
0f573de3e4 | |||
cd31673569 | |||
c7ba9b25b3 | |||
0d14ef6142 | |||
23b8101426 | |||
d0cdbf55a3 |
36 changed files with 3127 additions and 884 deletions
|
@ -1,6 +1,8 @@
|
|||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
@ -25,50 +27,12 @@ jobs:
|
|||
with:
|
||||
name: test
|
||||
path: target/nextest/ci/junit.xml
|
||||
|
||||
release:
|
||||
runs-on: cimaster-latest
|
||||
needs: test
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: 👁️ Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: ⚒️ Build application
|
||||
- name: 🔗 Artifactview PR comment
|
||||
if: ${{ always() && github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
PKG_CONFIG_SYSROOT_DIR=/usr/x86_64-linux-gnu cargo build --release --target x86_64-unknown-linux-gnu
|
||||
PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu cargo build --release --target aarch64-unknown-linux-gnu
|
||||
- name: 🐋 Build docker image
|
||||
uses: https://code.thetadev.de/ThetaDev/action-kaniko@v1
|
||||
with:
|
||||
image: thetadev256/artifactview
|
||||
username: thetadev256
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
tag_with_latest: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
platforms: "linux/amd64,linux/arm64"
|
||||
if [[ "$GITEA_ACTIONS" == "true" ]]; then RUN_NUMBER="$GITHUB_RUN_NUMBER"; else RUN_NUMBER="$GITHUB_RUN_ID"; fi
|
||||
echo "Run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$RUN_NUMBER"
|
||||
echo "Pull: ${{ github.event.number }}"
|
||||
|
||||
- name: Prepare release
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
mkdir dist
|
||||
tar -cJf dist/artifactview-x86_64-${{ github.ref_name }}.tar.xz -C target/x86_64-unknown-linux-gnu/release artifactview
|
||||
tar -cJf dist/artifactview-aarch64-${{ github.ref_name }}.tar.xz -C target/aarch64-unknown-linux-gnu/release artifactview
|
||||
|
||||
{
|
||||
echo 'CHANGELOG<<END_OF_FILE'
|
||||
awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' CHANGELOG.md
|
||||
echo END_OF_FILE
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: 🎉 Publish release
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
title: "artifactview ${{ github.ref_name }}"
|
||||
body: "${{ env.CHANGELOG }}"
|
||||
files: dist/*
|
||||
|
||||
- name: 🚀 Deploy to server
|
||||
run: |
|
||||
curl -s -H "Authorization: Bearer ${{ secrets.THETADEV_DE_WATCHTOWER_TOKEN }}" https://watchtower.thetadev.de/v1/update
|
||||
curl -SsL --fail-with-body -w "\n" -X POST https://av.thetadev.de/.well-known/api/prComment -H "Content-Type: application/json" \
|
||||
--data '{"url": "'"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$RUN_NUMBER"'", "pr": ${{ github.event.number }}, "artifact_titles": {"test":"🧪 Test report"}, "artifact_paths": {"test":"/junit.xml?viewer=1"}}'
|
||||
|
|
23
.forgejo/workflows/docker-readme.yaml
Normal file
23
.forgejo/workflows/docker-readme.yaml
Normal file
|
@ -0,0 +1,23 @@
|
|||
name: DockerHub README
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "README.md"
|
||||
- ".forgejo/workflows/docker-readme.yaml"
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: cimaster-latest
|
||||
steps:
|
||||
- name: 👁️ Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: https://github.com/peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: thetadev256
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
repository: thetadev256/artifactview
|
||||
enable-url-completion: true
|
51
.forgejo/workflows/release.yaml
Normal file
51
.forgejo/workflows/release.yaml
Normal file
|
@ -0,0 +1,51 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: cimaster-latest
|
||||
steps:
|
||||
- name: 👁️ Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: ⚒️ Build application
|
||||
run: |
|
||||
PKG_CONFIG_SYSROOT_DIR=/usr/x86_64-linux-gnu cargo build --release --target x86_64-unknown-linux-gnu
|
||||
PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu cargo build --release --target aarch64-unknown-linux-gnu
|
||||
- name: 🐋 Build docker image
|
||||
uses: https://code.thetadev.de/ThetaDev/action-kaniko@v1
|
||||
with:
|
||||
image: thetadev256/artifactview
|
||||
username: thetadev256
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
tag_with_latest: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
platforms: "linux/amd64,linux/arm64"
|
||||
|
||||
- name: Prepare release
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
mkdir dist
|
||||
tar -cJf dist/artifactview-x86_64-${{ github.ref_name }}.tar.xz -C target/x86_64-unknown-linux-gnu/release artifactview
|
||||
tar -cJf dist/artifactview-aarch64-${{ github.ref_name }}.tar.xz -C target/aarch64-unknown-linux-gnu/release artifactview
|
||||
|
||||
{
|
||||
echo 'CHANGELOG<<END_OF_FILE'
|
||||
awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' CHANGELOG.md
|
||||
echo END_OF_FILE
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: 🎉 Publish release
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: https://gitea.com/actions/release-action@main
|
||||
with:
|
||||
title: "artifactview ${{ github.ref_name }}"
|
||||
body: "${{ env.CHANGELOG }}"
|
||||
files: dist/*
|
||||
|
||||
- name: 🚀 Deploy to server
|
||||
run: |
|
||||
curl -SsL --fail-with-body -H "Authorization: Bearer ${{ secrets.THETADEV_DE_WATCHTOWER_TOKEN }}" https://watchtower.thetadev.de/v1/update
|
63
.forgejo/workflows/renovate.yaml.bak
Normal file
63
.forgejo/workflows/renovate.yaml.bak
Normal file
|
@ -0,0 +1,63 @@
|
|||
name: renovate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- ".forgejo/workflows/renovate.yaml"
|
||||
- "renovate.json"
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
RENOVATE_REPOSITORIES: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: renovate/renovate:39
|
||||
|
||||
steps:
|
||||
- name: Load renovate repo cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
.tmp/cache/renovate/repository
|
||||
.tmp/cache/renovate/renovate-cache-sqlite
|
||||
.tmp/osv
|
||||
key: repo-cache-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
repo-cache-
|
||||
|
||||
- name: Run renovate
|
||||
run: renovate
|
||||
env:
|
||||
LOG_LEVEL: info
|
||||
RENOVATE_BASE_DIR: ${{ github.workspace }}/.tmp
|
||||
RENOVATE_ENDPOINT: ${{ github.server_url }}
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_REPOSITORY_CACHE: 'enabled'
|
||||
RENOVATE_TOKEN: ${{ secrets.FORGEJO_CI_BOT_TOKEN }}
|
||||
GITHUB_COM_TOKEN: ${{ secrets.GH_PUBLIC_TOKEN }}
|
||||
RENOVATE_GIT_AUTHOR: 'Renovate Bot <forgejo-renovate-action@forgejo.org>'
|
||||
|
||||
RENOVATE_X_SQLITE_PACKAGE_CACHE: true
|
||||
|
||||
GIT_AUTHOR_NAME: 'Renovate Bot'
|
||||
GIT_AUTHOR_EMAIL: 'forgejo-renovate-action@forgejo.org'
|
||||
GIT_COMMITTER_NAME: 'Renovate Bot'
|
||||
GIT_COMMITTER_EMAIL: 'forgejo-renovate-action@forgejo.org'
|
||||
|
||||
OSV_OFFLINE_ROOT_DIR: ${{ github.workspace }}/.tmp/osv
|
||||
|
||||
- name: Save renovate repo cache
|
||||
if: always() && env.RENOVATE_DRY_RUN != 'full'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
.tmp/cache/renovate/repository
|
||||
.tmp/cache/renovate/renovate-cache-sqlite
|
||||
.tmp/osv
|
||||
key: repo-cache-${{ github.run_id }}
|
249
CHANGELOG.md
249
CHANGELOG.md
|
@ -3,6 +3,255 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [v0.4.8](https://codeberg.org/ThetaDev/artifactview/compare/v0.4.7..v0.4.8) - 2025-01-26
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Lifetime-related lints - ([e20f6fb](https://codeberg.org/ThetaDev/artifactview/commit/e20f6fb92e86751222d2c5143ee384cdbea1159d))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate async-compression to v0.4.15 (#85) - ([4f4716c](https://codeberg.org/ThetaDev/artifactview/commit/4f4716cdd86c317ede2b381d375ef8e736aee240))
|
||||
- *(deps)* Update rust crate async-compression to v0.4.16 (#86) - ([9592da3](https://codeberg.org/ThetaDev/artifactview/commit/9592da3d6e2c2223174fcc459e94f29bf5067ead))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.129 (#87) - ([2e46d37](https://codeberg.org/ThetaDev/artifactview/commit/2e46d3795089ef7b2739db4d216f4db99f792071))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.131 (#88) - ([992f995](https://codeberg.org/ThetaDev/artifactview/commit/992f9954414af550fce90c9c7424ab0da2296875))
|
||||
- *(deps)* Update rust crate governor to v0.6.4 (#89) - ([f1d9897](https://codeberg.org/ThetaDev/artifactview/commit/f1d9897e832b5cdb99fd81edcb38d27bd6b445f8))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.132 (#90) - ([600d18d](https://codeberg.org/ThetaDev/artifactview/commit/600d18d05b356c641a32b93b2407a1f23e2370c4))
|
||||
- *(deps)* Update rust crate async-compression to v0.4.17 (#91) - ([0b3c032](https://codeberg.org/ThetaDev/artifactview/commit/0b3c0325a50cd456cb9b62f97e916f0760785a3c))
|
||||
- *(deps)* Update rust crate governor to 0.7.0 (#92) - ([cbd304c](https://codeberg.org/ThetaDev/artifactview/commit/cbd304c84195983596156a95c26d65a89e93df8a))
|
||||
- *(deps)* Update rust crate thiserror to v1.0.65 (#94) - ([2df196a](https://codeberg.org/ThetaDev/artifactview/commit/2df196a2e666e8186d9ff66e885123c8a48c743c))
|
||||
- *(deps)* Update rust crate serde to v1.0.213 (#93) - ([e5b9105](https://codeberg.org/ThetaDev/artifactview/commit/e5b9105da1fb0c63584cc409b5dd98c1fe045f9b))
|
||||
- *(deps)* Update rust crate tokio to v1.41.0 (#95) - ([1dc4fe2](https://codeberg.org/ThetaDev/artifactview/commit/1dc4fe225c01f237f6a698fced7eff72cfb8ee85))
|
||||
- *(deps)* Update rust crate pin-project to v1.1.7 (#96) - ([c82bccc](https://codeberg.org/ThetaDev/artifactview/commit/c82bccca9098281fa42ded5d5527eadb3cbcce88))
|
||||
- *(deps)* Update rust crate regex to v1.11.1 (#97) - ([613815a](https://codeberg.org/ThetaDev/artifactview/commit/613815aa1ebc01643e117ca9fafcb37bfe7d29d7))
|
||||
- *(deps)* Update rust crate axum-test to v16.3.0 (#98) - ([b2070ec](https://codeberg.org/ThetaDev/artifactview/commit/b2070ec460e45eeb9997885bd7cf54913f9f4183))
|
||||
- *(deps)* Update rust crate insta to v1.41.0 (#99) - ([5aec8d6](https://codeberg.org/ThetaDev/artifactview/commit/5aec8d677f089ff5092fe78655170d52e544baeb))
|
||||
- *(deps)* Update rust crate quick-xml to 0.37.0 (#100) - ([72e20d4](https://codeberg.org/ThetaDev/artifactview/commit/72e20d413e80b86d49ea0b275ce6aece99d75314))
|
||||
- *(deps)* Update rust crate reqwest to v0.12.9 (#101) - ([d45e8e6](https://codeberg.org/ThetaDev/artifactview/commit/d45e8e63c9ce4067a05839643581b9f42e4048ec))
|
||||
- *(deps)* Update rust crate serde to v1.0.214 (#102) - ([2a1ebd7](https://codeberg.org/ThetaDev/artifactview/commit/2a1ebd7b854ca82cceb768202e4d9ee984007311))
|
||||
- *(deps)* Update rust crate futures-lite to v2.4.0 (#103) - ([3bda063](https://codeberg.org/ThetaDev/artifactview/commit/3bda06357809e68c0804aa425e625893af1bbde2))
|
||||
- *(deps)* Update rust crate insta to v1.41.1 (#104) - ([a406bff](https://codeberg.org/ThetaDev/artifactview/commit/a406bffabeeb2627cd5ef74f3520add6eb0a8d6c))
|
||||
- *(deps)* Update rust crate thiserror to v1.0.66 (#105) - ([8f89fc9](https://codeberg.org/ThetaDev/artifactview/commit/8f89fc9953370c4325c63762e4451d37a1a4a64a))
|
||||
- *(deps)* Update rust crate scraper to 0.21.0 (#106) - ([1f00bbf](https://codeberg.org/ThetaDev/artifactview/commit/1f00bbfac53521cf0f76cf6fc4bf23e7a5e10562))
|
||||
- *(deps)* Update rust crate thiserror to v1.0.67 (#107) - ([39a76ea](https://codeberg.org/ThetaDev/artifactview/commit/39a76eaa334d3f57e30e3ba95eb781dfa7aee1ee))
|
||||
- *(deps)* Update rust crate thiserror to v1.0.68 (#108) - ([a48af07](https://codeberg.org/ThetaDev/artifactview/commit/a48af07d936a77977d602649fe579941b2cd2630))
|
||||
- *(deps)* Update rust crate url to v2.5.3 (#109) - ([44cc0c1](https://codeberg.org/ThetaDev/artifactview/commit/44cc0c10103eb8fc10ddcc17e559479fadcbe4f1))
|
||||
- *(deps)* Update rust crate tokio to v1.41.1 (#111) - ([dd809ce](https://codeberg.org/ThetaDev/artifactview/commit/dd809ce3f322fe8dafadeb802be7fcf905aa2f02))
|
||||
- *(deps)* Update rust crate thiserror to v2 (#110) - ([8cb636c](https://codeberg.org/ThetaDev/artifactview/commit/8cb636ccc9c93800e6e98522de5c38bb05e76fdb))
|
||||
- *(deps)* Update rust crate futures-lite to v2.5.0 (#112) - ([c05eb56](https://codeberg.org/ThetaDev/artifactview/commit/c05eb562a9d10945ee534ef50208aeb004023c51))
|
||||
- *(deps)* Update rust crate thiserror to v2.0.3 (#113) - ([a695cef](https://codeberg.org/ThetaDev/artifactview/commit/a695cef57d5c73492d806958290bb37bd9613125))
|
||||
- *(deps)* Update rust crate serde to v1.0.215 (#114) - ([3497592](https://codeberg.org/ThetaDev/artifactview/commit/34975924b1e1c3d5367346a1e0274a435496091f))
|
||||
- *(deps)* Update rust crate flate2 to v1.0.35 (#115) - ([0b9498c](https://codeberg.org/ThetaDev/artifactview/commit/0b9498c541c0188fd7c82a99b2f778251b831df3))
|
||||
- *(deps)* Update rust crate axum to v0.7.8 (#116) - ([79623d9](https://codeberg.org/ThetaDev/artifactview/commit/79623d9bc2d40e43374c4a2934d6bbac1235bae7))
|
||||
- *(deps)* Update rust crate axum-extra to v0.9.5 (#117) - ([b35cfe3](https://codeberg.org/ThetaDev/artifactview/commit/b35cfe3f4fe402943ee3238b671c18449c5437ba))
|
||||
- *(deps)* Update rust crate axum-test to v16.4.0 (#118) - ([e370001](https://codeberg.org/ThetaDev/artifactview/commit/e37000143e752b4bc496797de2410be33d3adf2b))
|
||||
- *(deps)* Update rust crate axum to v0.7.9 (#119) - ([cab58d2](https://codeberg.org/ThetaDev/artifactview/commit/cab58d284e6b9f2e6b730d9b3f6d648d0955832d))
|
||||
- *(deps)* Update rust crate quick-xml to v0.37.1 (#121) - ([dffcd16](https://codeberg.org/ThetaDev/artifactview/commit/dffcd16a60b456e9ed547b2a01342df3585e607f))
|
||||
- *(deps)* Update rust crate axum-extra to v0.9.6 (#120) - ([4cf0084](https://codeberg.org/ThetaDev/artifactview/commit/4cf0084e24969d15149468de42cf6ed15e1169f5))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.133 (#122) - ([5231609](https://codeberg.org/ThetaDev/artifactview/commit/52316093cdad4ceca274e4c65035842e0413892e))
|
||||
- *(deps)* Update rust crate tower-http to v0.6.2 (#123) - ([8158497](https://codeberg.org/ThetaDev/artifactview/commit/8158497a73367fb2280d48350e3afa868c006d65))
|
||||
- *(deps)* Update rust crate zip to v2.2.1 (#124) - ([81c8521](https://codeberg.org/ThetaDev/artifactview/commit/81c852126ca45172bd00f75a5007263fefb5967b))
|
||||
- *(deps)* Update rust crate url to v2.5.4 (#125) - ([c99dfa8](https://codeberg.org/ThetaDev/artifactview/commit/c99dfa809175e84261245b2680a964b95d81b4e3))
|
||||
- *(deps)* Update rust crate comrak to 0.30.0 (#126) - ([389dd6f](https://codeberg.org/ThetaDev/artifactview/commit/389dd6f536b9044a8b9675c3ad34fa218028d154))
|
||||
- *(deps)* Update rust crate async-compression to v0.4.18 (#127) - ([05f20f4](https://codeberg.org/ThetaDev/artifactview/commit/05f20f44ac4910e600f318d656d376d52fd6b131))
|
||||
- *(deps)* Update rust crate comrak to 0.31.0 (#128) - ([5fd14aa](https://codeberg.org/ThetaDev/artifactview/commit/5fd14aada8f310fdeb0b2dc33a15de0b195ebc81))
|
||||
- *(deps)* Update rust crate tracing to v0.1.41 (#129) - ([ab3479f](https://codeberg.org/ThetaDev/artifactview/commit/ab3479f0d1b3a30413df654cc9f2e7a52081b542))
|
||||
- *(deps)* Update rust crate tracing-subscriber to v0.3.19 (#130) - ([1f9847b](https://codeberg.org/ThetaDev/artifactview/commit/1f9847b3edd73c1e905feb88087ee4325364ddda))
|
||||
- *(deps)* Update rust crate thiserror to v2.0.4 (#131) - ([e9d1226](https://codeberg.org/ThetaDev/artifactview/commit/e9d122639022d3b5b68b4626e00442a81acf85f5))
|
||||
- *(deps)* Update rust crate time to v0.3.37 (#132) - ([6fc7263](https://codeberg.org/ThetaDev/artifactview/commit/6fc7263f5996abefdb71fd5ecae277ec707cbdd9))
|
||||
- *(deps)* Update rust crate http to v1.2.0 (#133) - ([5f517ae](https://codeberg.org/ThetaDev/artifactview/commit/5f517ae6c784e4fbc99ca69650d85621e90f776c))
|
||||
- *(deps)* Update rust crate tokio to v1.42.0 (#134) - ([b15c4b0](https://codeberg.org/ThetaDev/artifactview/commit/b15c4b009a268cd4227846f166131aad5628f87d))
|
||||
- *(deps)* Update rust crate tokio-util to v0.7.13 (#135) - ([f43f06c](https://codeberg.org/ThetaDev/artifactview/commit/f43f06c33462c15a8a752493f14c86d9dbde0e8d))
|
||||
- *(deps)* Update rust crate thiserror to v2.0.5 (#136) - ([94c589c](https://codeberg.org/ThetaDev/artifactview/commit/94c589c20936c8e56be3147fd01e51d8b25e617f))
|
||||
- *(deps)* Update rust crate thiserror to v2.0.6 (#137) - ([5e83ab5](https://codeberg.org/ThetaDev/artifactview/commit/5e83ab510614db696a7245ad5027c717b3493cab))
|
||||
- *(deps)* Update rust crate chrono to v0.4.39 (#138) - ([8e9c5aa](https://codeberg.org/ThetaDev/artifactview/commit/8e9c5aad48378c2fa1a1d3d370b96de2fca5dd86))
|
||||
- *(deps)* Update rust crate governor to 0.8.0 (#139) - ([b24136e](https://codeberg.org/ThetaDev/artifactview/commit/b24136ec597d2b4c579176a6a7845878549478bb))
|
||||
- *(deps)* Update rust crate scraper to 0.22.0 (#140) - ([f48c570](https://codeberg.org/ThetaDev/artifactview/commit/f48c57021505091b22c36bd49bd4527e652b8d78))
|
||||
- *(deps)* Update rust crate serde to v1.0.216 (#141) - ([7d9827f](https://codeberg.org/ThetaDev/artifactview/commit/7d9827f9fc52aa88f19efa371e36877723328248))
|
||||
- *(deps)* Update rust crate thiserror to v2.0.7 (#142) - ([1e26d04](https://codeberg.org/ThetaDev/artifactview/commit/1e26d04b068f9c2e0a65a3dbcc43f66d118a6043))
|
||||
- *(deps)* Update rust crate axum-test to v16.4.1 (#143) - ([3244de4](https://codeberg.org/ThetaDev/artifactview/commit/3244de48fc2691898599ab20f2baa888db9f3c82))
|
||||
- *(deps)* Update rust crate zip to v2.2.2 (#144) - ([98ba21e](https://codeberg.org/ThetaDev/artifactview/commit/98ba21e7979bfbdd06fb46da8e45f97f71dc2c99))
|
||||
- *(deps)* Update rust crate proptest to v1.6.0 (#145) - ([d28f979](https://codeberg.org/ThetaDev/artifactview/commit/d28f9790b825b2af749e3fcd07718faa91c2a942))
|
||||
- *(deps)* Update rust crate comrak to 0.32.0 (#146) - ([f6b0e06](https://codeberg.org/ThetaDev/artifactview/commit/f6b0e06dc1a39311c4473e0f4c3c5742a352e00f))
|
||||
- *(deps)* Update rust crate thiserror to v2.0.8 (#147) - ([e87b71c](https://codeberg.org/ThetaDev/artifactview/commit/e87b71cc0df91f0ee71a88a3bd0127d4f8c74eea))
|
||||
- *(deps)* Update rust crate env_logger to v0.11.6 (#148) - ([8e295a9](https://codeberg.org/ThetaDev/artifactview/commit/8e295a96decd532fdd854ee8950a68d496c1617d))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.134 (#149) - ([c90116c](https://codeberg.org/ThetaDev/artifactview/commit/c90116c9bac801e57062a462bd7a08a5fd6389b2))
|
||||
- *(deps)* Update rust crate thiserror to v2.0.9 (#150) - ([2a2a8e0](https://codeberg.org/ThetaDev/artifactview/commit/2a2a8e0b310b4ddcf15f20b1189bf768499f6b75))
|
||||
- *(deps)* Update rust crate serde to v1.0.217 (#151) - ([abe8f92](https://codeberg.org/ThetaDev/artifactview/commit/abe8f92ab8cdee926ca1ad4faf280427a9439e0e))
|
||||
- *(deps)* Update rust crate quick-xml to v0.37.2 (#152) - ([d778789](https://codeberg.org/ThetaDev/artifactview/commit/d7787899593d60243c1efa6dd036fc44e9c51868))
|
||||
- *(deps)* Update rust crate axum-extra to 0.10.0 (#154) - ([55621fb](https://codeberg.org/ThetaDev/artifactview/commit/55621fbbea51aed2234e43fa4b5ed61524ca1805))
|
||||
- *(deps)* Update rust crate rstest to 0.24.0 (#155) - ([b3cc2b5](https://codeberg.org/ThetaDev/artifactview/commit/b3cc2b53dc032d2821748895c983dde49c56eb65))
|
||||
- *(deps)* Update rust crate reqwest to v0.12.12 (#156) - ([cd73f48](https://codeberg.org/ThetaDev/artifactview/commit/cd73f4828f67d31dee356cb98a1ac45354e0e728))
|
||||
- *(deps)* Update rust crate comrak to 0.33.0 (#158) - ([1ce03ca](https://codeberg.org/ThetaDev/artifactview/commit/1ce03ca19f4032d5e88c8efbe83b98c45d98247d))
|
||||
- *(deps)* Update rust crate insta to v1.42.0 (#159) - ([5b138fa](https://codeberg.org/ThetaDev/artifactview/commit/5b138fae112d47a08b0940d61bd403047a8567fa))
|
||||
- *(deps)* Update rust crate pin-project to v1.1.8 (#161) - ([13ee5cc](https://codeberg.org/ThetaDev/artifactview/commit/13ee5cc2456180bfdc47a2d9e3a0dfe9a2dacd7d))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.135 (#162) - ([8417ea3](https://codeberg.org/ThetaDev/artifactview/commit/8417ea34a0808d2ba25194021bbe38538ce52ddf))
|
||||
- *(deps)* Update rust crate thiserror to v2.0.10 (#163) - ([c2ee6cd](https://codeberg.org/ThetaDev/artifactview/commit/c2ee6cd84933e7cb2167b4cd34ec47f926105e59))
|
||||
- *(deps)* Update rust crate tokio to v1.43.0 (#164) - ([db790e0](https://codeberg.org/ThetaDev/artifactview/commit/db790e0811e9a67a63dc4708ca928efbcff1eb49))
|
||||
- *(deps)* Update rust crate thiserror to v2.0.11 (#165) - ([db0a4fd](https://codeberg.org/ThetaDev/artifactview/commit/db0a4fd5d48842ff48a375d241139ad91796422b))
|
||||
- *(deps)* Update rust crate futures-lite to v2.6.0 (#166) - ([c9a6d67](https://codeberg.org/ThetaDev/artifactview/commit/c9a6d6786f763825874520776653b311bc7bd5d8))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.137 (#167) - ([558ce7d](https://codeberg.org/ThetaDev/artifactview/commit/558ce7daa8fbedac507f2d6e01961896eb8daac0))
|
||||
- *(deps)* Update rust crate comrak to 0.34.0 (#168) - ([a88977a](https://codeberg.org/ThetaDev/artifactview/commit/a88977af942d275422670984ddbefa53f8d61e13))
|
||||
- *(deps)* Update rust crate comrak to 0.35.0 (#169) - ([4042ded](https://codeberg.org/ThetaDev/artifactview/commit/4042ded5aee3763c293ffd264cb2428eb4266845))
|
||||
- *(deps)* Update rust crate insta to v1.42.1 (#170) - ([0c49fe7](https://codeberg.org/ThetaDev/artifactview/commit/0c49fe751a833ca684bfc39d19e2f1eb7ea269f5))
|
||||
- *(deps)* Update rust crate axum to 0.8.0 (#157) - ([2c2893d](https://codeberg.org/ThetaDev/artifactview/commit/2c2893da218737572e3943e2b72f7cec4ca6798f))
|
||||
|
||||
|
||||
## [v0.4.7](https://codeberg.org/ThetaDev/artifactview/compare/v0.4.6..v0.4.7) - 2024-10-12
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(deps)* Update rust crate serde_json to v1.0.121 (#29) - ([df805bf](https://codeberg.org/ThetaDev/artifactview/commit/df805bfe8394dd148ded1d4d3af901eb97593885))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.122 (#32) - ([db67487](https://codeberg.org/ThetaDev/artifactview/commit/db67487abdfc15fe55854fcb233e0bb876b603b3))
|
||||
- *(deps)* Update rust crate regex to v1.10.6 (#33) - ([7c2a976](https://codeberg.org/ThetaDev/artifactview/commit/7c2a97666d98d4959affbb8ece93c4ba162a760d))
|
||||
- *(deps)* Update rust crate serde-env to 0.2.0 (#37) - ([6b7d107](https://codeberg.org/ThetaDev/artifactview/commit/6b7d107387ff3e52e62e4ed19c64e63f8048c478))
|
||||
- *(deps)* Update rust crate serde to v1.0.205 (#38) - ([f9698b5](https://codeberg.org/ThetaDev/artifactview/commit/f9698b5a7f9c7f3748d4d7aa38f7dc4c0f5f2029))
|
||||
- *(deps)* Update rust crate serde to v1.0.206 (#39) - ([ed86f30](https://codeberg.org/ThetaDev/artifactview/commit/ed86f30cf4a736eeb4a3d471e81b8e7f7344b53b))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.124 (#40) - ([cc6a495](https://codeberg.org/ThetaDev/artifactview/commit/cc6a4959983205ae2f40d81c9a40c8514165c0bb))
|
||||
- *(deps)* Update rust crate serde to v1.0.207 (#41) - ([0c2b39a](https://codeberg.org/ThetaDev/artifactview/commit/0c2b39a68a1adb567a1582f0c1b9e024fda9ed53))
|
||||
- *(deps)* Update rust crate serde to v1.0.208 (#43) - ([8073e90](https://codeberg.org/ThetaDev/artifactview/commit/8073e90f685d80565db81e23769841c16c2af261))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.125 (#44) - ([4b3639a](https://codeberg.org/ThetaDev/artifactview/commit/4b3639aea7beed4ebc421fdfe26823be164d5c1c))
|
||||
- *(deps)* Update rust crate comrak to 0.27.0 (#46) - ([3cef317](https://codeberg.org/ThetaDev/artifactview/commit/3cef3175767170824f604fcccdc912bf09745bf9))
|
||||
- *(deps)* Update rust crate comrak to 0.28.0 (#47) - ([a88a3c6](https://codeberg.org/ThetaDev/artifactview/commit/a88a3c6103e776a4d10b3f7e6e9a37a2c672cfba))
|
||||
- *(deps)* Update rust crate quick_cache to v0.6.6 (#50) - ([73959c0](https://codeberg.org/ThetaDev/artifactview/commit/73959c00f2c54b682c3db8640ca12319ce4ee37d))
|
||||
- *(deps)* Update rust crate reqwest to v0.12.7 (#51) - ([22d5626](https://codeberg.org/ThetaDev/artifactview/commit/22d5626bf025783a127cd99faa0052778e0253b1))
|
||||
- *(deps)* Update rust crate serde to v1.0.210 (#52) - ([f8c9d6f](https://codeberg.org/ThetaDev/artifactview/commit/f8c9d6f7cb475f4642f5e4f11108c4d053cc8c7e))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.128 (#57) - ([a48e23b](https://codeberg.org/ThetaDev/artifactview/commit/a48e23beceefc1b4c51910dc7114ab62abfd189c))
|
||||
- *(deps)* Update rust crate quick_cache to v0.6.9 (#59) - ([4eb2b22](https://codeberg.org/ThetaDev/artifactview/commit/4eb2b22a8f1c2b1b28f72303d364708d04790eca))
|
||||
- *(deps)* Update rust crate tower-http to 0.6.0 (#61) - ([1d03f5b](https://codeberg.org/ThetaDev/artifactview/commit/1d03f5b4b09596a68893126d8b177226b62fb38a))
|
||||
- *(deps)* Update rust crate axum to v0.7.6 (#62) - ([61f65e5](https://codeberg.org/ThetaDev/artifactview/commit/61f65e54db431b3e94d21188920ae88233c44d3b))
|
||||
- *(deps)* Update rust crate quick-xml to v0.36.2 (#64) - ([de4459f](https://codeberg.org/ThetaDev/artifactview/commit/de4459f646444a949c390394524f284a1944a0da))
|
||||
- *(deps)* Update rust crate axum-extra to v0.9.4 (#63) - ([6619ef6](https://codeberg.org/ThetaDev/artifactview/commit/6619ef60e44832dd8839bcaf82d43707965b772a))
|
||||
- *(deps)* Update rust crate thiserror to v1.0.64 (#66) - ([923f97f](https://codeberg.org/ThetaDev/artifactview/commit/923f97f8e9d0855ff97685496221a180018ae686))
|
||||
- *(deps)* Update rust crate tower-http to v0.6.1 (#68) - ([ca0734d](https://codeberg.org/ThetaDev/artifactview/commit/ca0734d47072d555ba07b2f512975b9379305a58))
|
||||
- *(deps)* Update rust crate secrecy to 0.10.0 (#60) - ([72d0cde](https://codeberg.org/ThetaDev/artifactview/commit/72d0cde37075220cc6a938840ad30781ecdcbaa7))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate zip to v2.1.6 (#31) - ([7e0aaa8](https://codeberg.org/ThetaDev/artifactview/commit/7e0aaa8362005b56526ed5a6114f473893a5cf46))
|
||||
- *(deps)* Update rust crate flate2 to v1.0.31 (#34) - ([01e6a9c](https://codeberg.org/ThetaDev/artifactview/commit/01e6a9c8ad1f7c7dd5307a02a4a2b3c381aeacd6))
|
||||
- *(deps)* Update rust crate rstest to 0.22.0 (#35) - ([b9d0a29](https://codeberg.org/ThetaDev/artifactview/commit/b9d0a29741138a4dd7b758417b003c9bddc35f3e))
|
||||
- *(deps)* Update rust crate scraper to 0.20.0 (#36) - ([ca174a3](https://codeberg.org/ThetaDev/artifactview/commit/ca174a3aa21d0466930c2aa3291c2ecbed2ac31f))
|
||||
- *(deps)* Update rust crate axum-test to v15.3.1 (#42) - ([24171c9](https://codeberg.org/ThetaDev/artifactview/commit/24171c9800aa52270c203e5a5fc40f520b6ac74c))
|
||||
- *(deps)* Update rust crate tokio to v1.39.3 (#45) - ([63978d7](https://codeberg.org/ThetaDev/artifactview/commit/63978d79f9c3e276e59d7f3a558cec5f8f88e17e))
|
||||
- *(deps)* Update rust crate flate2 to v1.0.33 (#48) - ([980e596](https://codeberg.org/ThetaDev/artifactview/commit/980e5968eaa909c9d7a72d78156d3465c4599abe))
|
||||
- *(deps)* Update rust crate tokio-util to v0.7.12 (#49) - ([78179fd](https://codeberg.org/ThetaDev/artifactview/commit/78179fd73791c47cd3a60a37e704472109b50c15))
|
||||
- *(deps)* Update rust crate insta to v1.40.0 (#54) - ([03597d1](https://codeberg.org/ThetaDev/artifactview/commit/03597d10e58eca8a56e4708971cac1750e7707f4))
|
||||
- *(deps)* Update rust crate tokio to v1.40.0 (#55) - ([97b9610](https://codeberg.org/ThetaDev/artifactview/commit/97b9610f308c8ad0a083f6522c8b661c4b2c1e4f))
|
||||
- *(deps)* Update rust crate zip to v2.2.0 (#56) - ([3f719ac](https://codeberg.org/ThetaDev/artifactview/commit/3f719ac939612722b82b8bad2744b4570ff40df7))
|
||||
- *(deps)* Update rust crate once_cell to v1.20.0 (#58) - ([f8a95c8](https://codeberg.org/ThetaDev/artifactview/commit/f8a95c82e4a627d10906330327e94cd829d6f4c8))
|
||||
- *(deps)* Update rust crate axum-test to v15.7.1 (#53) - ([7a92941](https://codeberg.org/ThetaDev/artifactview/commit/7a92941452b0e2f59e0ceda49a378f0ef43784cb))
|
||||
- *(deps)* Update rust crate axum-test to v15.7.3 (#65) - ([82ca6dd](https://codeberg.org/ThetaDev/artifactview/commit/82ca6dd6bff9d8cb87a349cc8edb46f42a9721fc))
|
||||
- *(deps)* Update rust crate axum-test to v16 (#69) - ([dbcee49](https://codeberg.org/ThetaDev/artifactview/commit/dbcee4945c0be57534bdd2d7d98d85767e0cf92c))
|
||||
- *(deps)* Update rust crate flate2 to v1.0.34 (#70) - ([6f3544e](https://codeberg.org/ThetaDev/artifactview/commit/6f3544e3d8051697053cad5a751df6a1b4bc658b))
|
||||
- *(deps)* Update rust crate axum to v0.7.7 (#71) - ([01c494c](https://codeberg.org/ThetaDev/artifactview/commit/01c494c2773dc67f922beed7e84aa2ff59fb575f))
|
||||
- *(deps)* Update rust crate axum-test to v16.0.1 (#72) - ([2367512](https://codeberg.org/ThetaDev/artifactview/commit/23675124bdb15e01f037a6380f97f4d2921a34eb))
|
||||
- *(deps)* Update rust crate once_cell to v1.20.1 (#73) - ([484f113](https://codeberg.org/ThetaDev/artifactview/commit/484f1136469f5459b1d464944621e9db450d2f6a))
|
||||
- *(deps)* Update rust crate axum-test to v16.1.0 (#74) - ([2ed0cdc](https://codeberg.org/ThetaDev/artifactview/commit/2ed0cdc4a372c0a40de2ddc70a14b658a3619eec))
|
||||
- *(deps)* Update rust crate regex to v1.11.0 (#75) - ([e436f77](https://codeberg.org/ThetaDev/artifactview/commit/e436f77c3e4969d83c42d45201c15f0375d90ad0))
|
||||
- *(deps)* Update rust crate rstest to 0.23.0 (#76) - ([7c684eb](https://codeberg.org/ThetaDev/artifactview/commit/7c684eb5657eda84668bf3a1aadf38f1e4ac51db))
|
||||
- *(deps)* Update rust crate reqwest to v0.12.8 (#77) - ([a3f028f](https://codeberg.org/ThetaDev/artifactview/commit/a3f028f2ad11d3e599bf20d1f92679bf8b8dafc4))
|
||||
- *(deps)* Update rust crate async-compression to v0.4.13 (#78) - ([7cefbd4](https://codeberg.org/ThetaDev/artifactview/commit/7cefbd4a67e5636f046177f35fce14fff6300cb4))
|
||||
- *(deps)* Update rust crate once_cell to v1.20.2 (#79) - ([8309901](https://codeberg.org/ThetaDev/artifactview/commit/8309901a8c658cdf948889df57b731147d77c949))
|
||||
- *(deps)* Update rust crate pin-project to v1.1.6 (#80) - ([6ca7088](https://codeberg.org/ThetaDev/artifactview/commit/6ca7088b9c7ecd001df52f9cb35e86301c231bbb))
|
||||
- *(deps)* Update rust crate axum-test to v16.2.0 (#81) - ([e63baec](https://codeberg.org/ThetaDev/artifactview/commit/e63baec2490e069953f63d158f9af212f154055b))
|
||||
- *(deps)* Update rust crate secrecy to v0.10.3 (#82) - ([a2dc40f](https://codeberg.org/ThetaDev/artifactview/commit/a2dc40f4443cb3c3d1c7cf2b9e7178777c4b73e7))
|
||||
- *(deps)* Update rust crate async-compression to v0.4.14 (#83) - ([51f098f](https://codeberg.org/ThetaDev/artifactview/commit/51f098f4ada63ddc550774a60a81eb3d55697b44))
|
||||
- *(deps)* Update rust crate comrak to 0.29.0 (#84) - ([5da4074](https://codeberg.org/ThetaDev/artifactview/commit/5da4074bb9e87c9d17ed70669b6e47a3d57b1e8f))
|
||||
|
||||
|
||||
## [v0.4.6](https://codeberg.org/ThetaDev/artifactview/compare/v0.4.5..v0.4.6) - 2024-07-28
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(deps)* Update rust crate serde_json to v1.0.120 (#14) - ([06f9c27](https://codeberg.org/ThetaDev/artifactview/commit/06f9c278a857a272580ee1c4f8e58078556accda))
|
||||
- *(deps)* Update rust crate quick_cache to 0.6.0 (#15) - ([2e06266](https://codeberg.org/ThetaDev/artifactview/commit/2e0626667e5fc344df9870cd8c924b9dd60886bb))
|
||||
- *(deps)* Update rust crate serde to v1.0.204 (#16) - ([1321386](https://codeberg.org/ThetaDev/artifactview/commit/13213861ba0ea30504caa50da2a99af567876e5c))
|
||||
- *(deps)* Update rust crate quick-xml to 0.36.0 (#19) - ([5f94794](https://codeberg.org/ThetaDev/artifactview/commit/5f94794d24f300762da2ab162f4336508b516eda))
|
||||
- *(deps)* Update rust crate async-compression to v0.4.12 (#22) - ([88c635c](https://codeberg.org/ThetaDev/artifactview/commit/88c635cbad535eb902ea54a314e8511965a792b4))
|
||||
- *(deps)* Update rust crate quick-xml to v0.36.1 (#23) - ([c5c9f85](https://codeberg.org/ThetaDev/artifactview/commit/c5c9f85e4baec58dcc2001ac3a7f005c7f501557))
|
||||
- *(deps)* Update rust crate quick_cache to v0.6.2 (#24) - ([a5d4973](https://codeberg.org/ThetaDev/artifactview/commit/a5d49733fd84ba37e8c258f191bf79f4affb86f9))
|
||||
- *(deps)* Update rust crate thiserror to v1.0.63 (#25) - ([b67b173](https://codeberg.org/ThetaDev/artifactview/commit/b67b1730b1e9c06b4ff99774c37c71391f48f93a))
|
||||
- *(deps)* Update rust crate comrak to 0.26.0 (#27) - ([642930d](https://codeberg.org/ThetaDev/artifactview/commit/642930d397efa106267bd2aff8c413ab4173a5c6))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate axum-test to v15.3.0 (#17) - ([a88f1ba](https://codeberg.org/ThetaDev/artifactview/commit/a88f1ba91c054fdb267f0edef10aacf14a909694))
|
||||
- *(deps)* Lock file maintenance (#18) - ([f41a922](https://codeberg.org/ThetaDev/artifactview/commit/f41a92243c91086d5d774410b1452303fad64ccf))
|
||||
- *(deps)* Update rust crate env_logger to v0.11.5 (#20) - ([c9db056](https://codeberg.org/ThetaDev/artifactview/commit/c9db0567916e500017034d6a99eb48a25a1671e0))
|
||||
- *(deps)* Update rust crate scraper to v0.19.1 (#21) - ([1a5c056](https://codeberg.org/ThetaDev/artifactview/commit/1a5c056204b488e36ef95145b05a674c661a2154))
|
||||
- *(deps)* Update rust crate tokio to v1.39.1 (#26) - ([057a365](https://codeberg.org/ThetaDev/artifactview/commit/057a365a0ecafe00fa84e53c736272f5db26f0f1))
|
||||
- *(deps)* Update rust crate tokio to v1.39.2 (#28) - ([3ae7f88](https://codeberg.org/ThetaDev/artifactview/commit/3ae7f8813e19818098a6b67d0c6770db8a79defe))
|
||||
- *(deps)* Update rust crate zip to v2 (#9) - ([a7160fa](https://codeberg.org/ThetaDev/artifactview/commit/a7160fadde6c95c6c03c90c5d2301738c8e559c2))
|
||||
|
||||
|
||||
## [v0.4.5](https://codeberg.org/ThetaDev/artifactview/compare/v0.4.4..v0.4.5) - 2024-07-01
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Update PR comment format - ([197eeea](https://codeberg.org/ThetaDev/artifactview/commit/197eeea75baa8ba44d27ec46c5f552028052869b))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(deps)* Update rust crate serde_json to v1.0.118 (#5) - ([fc3b5a1](https://codeberg.org/ThetaDev/artifactview/commit/fc3b5a1530985012ff8364a8fa676626e7544eaf))
|
||||
- *(deps)* Update rust crate quick-xml to 0.34.0 (#8) - ([595a9d0](https://codeberg.org/ThetaDev/artifactview/commit/595a9d0f4115faf5056653406b6d05bf671dc2b3))
|
||||
- *(deps)* Update rust crate mime_guess to v2.0.5 (#10) - ([94191f8](https://codeberg.org/ThetaDev/artifactview/commit/94191f878d774bef165cca850fcdf00fde16d662))
|
||||
- *(deps)* Update rust crate quick-xml to 0.35.0 (#11) - ([7ebe881](https://codeberg.org/ThetaDev/artifactview/commit/7ebe8815462f3e704a79af038b9b1850ed4677ed))
|
||||
- *(deps)* Update rust crate serde_json to v1.0.119 (#12) - ([2525022](https://codeberg.org/ThetaDev/artifactview/commit/2525022df76b3c16951983c14a55fa9617114a8e))
|
||||
- Swap crc and size column (#3) - ([7d2c686](https://codeberg.org/ThetaDev/artifactview/commit/7d2c68630ec6e75061c050a4c8b035edb472d150))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Make example CI step compatible with GitHub+Forgejo - ([39f0019](https://codeberg.org/ThetaDev/artifactview/commit/39f0019455cc23f1b8c39b77d2aaa5af278731a9))
|
||||
- Update README - ([40ae3a7](https://codeberg.org/ThetaDev/artifactview/commit/40ae3a7f557c63a0bb2abcd595218c8ec1095fe7))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(deps)* Update rust crate proptest to v1.5.0 (#6) - ([797fc0c](https://codeberg.org/ThetaDev/artifactview/commit/797fc0c04c2a51811a24cfc431496e6e5dbf0bea))
|
||||
- *(deps)* Update rust crate rstest to 0.21.0 (#7) - ([cec3aa3](https://codeberg.org/ThetaDev/artifactview/commit/cec3aa3fc02e6a871d9c221f61c3f2d8828f9f63))
|
||||
- *(deps)* Lock file maintenance (#13) - ([9767167](https://codeberg.org/ThetaDev/artifactview/commit/9767167661e22775614cea7b888a19ee16c17d65))
|
||||
|
||||
|
||||
## [v0.4.4](https://codeberg.org/ThetaDev/artifactview/compare/v0.4.3..v0.4.4) - 2024-06-22
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Use forge aliases for PR comment links - ([3690b02](https://codeberg.org/ThetaDev/artifactview/commit/3690b0244cf47d0d73511f5f69f5d12abe0f1837))
|
||||
|
||||
|
||||
## [v0.4.3](https://codeberg.org/ThetaDev/artifactview/compare/v0.4.2..v0.4.3) - 2024-06-22
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- 404 error on GitHub comment creation - ([d8c3ab4](https://codeberg.org/ThetaDev/artifactview/commit/d8c3ab4f36727f118b31683db87d287d9945ee14))
|
||||
|
||||
|
||||
## [v0.4.2](https://codeberg.org/ThetaDev/artifactview/compare/v0.4.1..v0.4.2) - 2024-06-22
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- PR comment emoji prefix detection - ([1e36edf](https://codeberg.org/ThetaDev/artifactview/commit/1e36edf49978e8ba24a85d4663ff3ebaf9642a29))
|
||||
|
||||
|
||||
## [v0.4.1](https://codeberg.org/ThetaDev/artifactview/compare/v0.4.0..v0.4.1) - 2024-06-22
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Improved PR comment format, add `artifact_paths` parameter - ([6ae7520](https://codeberg.org/ThetaDev/artifactview/commit/6ae7520111469b04764115ccb5eeb3b756a4572e))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Return correct status code on API errors - ([f94cdcb](https://codeberg.org/ThetaDev/artifactview/commit/f94cdcbd1fec5474145e845a8673470a174bf6f5))
|
||||
|
||||
|
||||
## [v0.4.0](https://codeberg.org/ThetaDev/artifactview/compare/v0.3.1..v0.4.0) - 2024-06-22
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Create PR comments - ([d0cdbf5](https://codeberg.org/ThetaDev/artifactview/commit/d0cdbf55a3a278ce21cab11885170e9f4a6d4094))
|
||||
- Add url parameter to /artifacts API - ([23b8101](https://codeberg.org/ThetaDev/artifactview/commit/23b81014266728e3db1cb200a5cf46a212baed72))
|
||||
|
||||
|
||||
## [v0.3.1](https://codeberg.org/ThetaDev/artifactview/compare/v0.3.0..v0.3.1) - 2024-06-19
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
|
1603
Cargo.lock
generated
1603
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
27
Cargo.toml
27
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "artifactview"
|
||||
version = "0.3.1"
|
||||
version = "0.4.8"
|
||||
edition = "2021"
|
||||
authors = ["ThetaDev <thetadev@magenta.de>"]
|
||||
license = "MIT"
|
||||
|
@ -21,20 +21,21 @@ async_zip = { path = "crates/async_zip", features = [
|
|||
"tokio-fs",
|
||||
"deflate",
|
||||
] }
|
||||
axum = { version = "0.7.5", default-features = false, features = [
|
||||
axum = { version = "0.8.0", default-features = false, features = [
|
||||
"http1",
|
||||
"http2",
|
||||
"json",
|
||||
"query",
|
||||
"tokio",
|
||||
"tracing",
|
||||
] }
|
||||
axum-extra = { version = "0.9.3", features = ["typed-header"] }
|
||||
comrak = { version = "0.24.1", default-features = false }
|
||||
axum-extra = { version = "0.10.0", features = ["typed-header"] }
|
||||
comrak = { version = "0.35.0", default-features = false }
|
||||
dotenvy = "0.15.7"
|
||||
envy = { path = "crates/envy" }
|
||||
flate2 = "1.0.30"
|
||||
futures-lite = "2.3.0"
|
||||
governor = "0.6.3"
|
||||
governor = "0.8.0"
|
||||
headers = "0.4.0"
|
||||
http = "1.1.0"
|
||||
humansize = "2.1.3"
|
||||
|
@ -45,15 +46,16 @@ once_cell = "1.19.0"
|
|||
path_macro = "1.0.0"
|
||||
percent-encoding = "2.3.1"
|
||||
pin-project = "1.1.5"
|
||||
quick_cache = "0.5.1"
|
||||
quick_cache = "0.6.0"
|
||||
rand = "0.8.5"
|
||||
regex = "1.10.4"
|
||||
reqwest = { version = "0.12.4", default-features = false, features = [
|
||||
"json",
|
||||
"stream",
|
||||
] }
|
||||
secrecy = { version = "0.10.0", features = ["serde"] }
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde-env = "0.1.1"
|
||||
serde-env = "0.2.0"
|
||||
serde-hex = "0.1.0"
|
||||
serde_json = "1.0.117"
|
||||
serde_urlencoded = "0.7.1"
|
||||
|
@ -64,10 +66,11 @@ syntect = { version = "5.2.0", default-features = false, features = [
|
|||
"html",
|
||||
"regex-onig",
|
||||
] }
|
||||
thiserror = "1.0.61"
|
||||
thiserror = "2.0.0"
|
||||
time = { version = "0.3.36", features = ["serde-human-readable", "macros"] }
|
||||
tokio = { version = "1.37.0", features = ["macros", "fs", "rt-multi-thread"] }
|
||||
tokio-util = { version = "0.7.11", features = ["io"] }
|
||||
tower-http = { version = "0.5.2", features = ["trace", "set-header"] }
|
||||
tower-http = { version = "0.6.0", features = ["trace", "set-header"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
url = "2.5.0"
|
||||
|
@ -77,13 +80,13 @@ yarte = { version = "0.15.7", features = ["json"] }
|
|||
yarte_helpers = "0.15.8"
|
||||
|
||||
[dev-dependencies]
|
||||
axum-test = "15.0.1"
|
||||
axum-test = "17.0.0"
|
||||
flate2 = "1.0.30"
|
||||
httpdate = "1.0.3"
|
||||
insta = { version = "1.39.0", features = ["json"] }
|
||||
proptest = "1.4.0"
|
||||
rstest = { version = "0.20.0", default-features = false }
|
||||
scraper = "0.19.0"
|
||||
rstest = { version = "0.24.0", default-features = false }
|
||||
scraper = "0.22.0"
|
||||
temp_testdir = "0.2.3"
|
||||
|
||||
[workspace]
|
||||
|
|
2
Justfile
2
Justfile
|
@ -25,7 +25,7 @@ release:
|
|||
eval "git-cliff $CLIFF_ARGS --output '$CHANGELOG'"
|
||||
fi
|
||||
|
||||
git add "$CHANGELOG"
|
||||
git add .
|
||||
git commit -m "chore(release): release $CRATE v$VERSION"
|
||||
|
||||
awk 'BEGIN{RS="(^|\n)## [^\n]+\n*"} NR==2 { print }' "$CHANGELOG" | git tag -as -F - --cleanup whitespace "$TAG"
|
||||
|
|
254
README.md
254
README.md
|
@ -1,23 +1,29 @@
|
|||
# Artifactview
|
||||
|
||||
View CI build artifacts from Forgejo/Github using your web browser.
|
||||
View CI build artifacts from Forgejo/GitHub using your web browser!
|
||||
|
||||
Forgejo and GitHub's CI systems allow you to upload files and directories as
|
||||
[artifacts](https://github.com/actions/upload-artifact). These can be downloaded as zip
|
||||
files. However there is no simple way to view individual files of an artifact.
|
||||
|
||||
Artifactview is a small web application that fetches these CI artifacts and displays
|
||||
their contents.
|
||||
That's why I developed Artifactview. It is a small web application that fetches these CI
|
||||
artifacts and serves their contents.
|
||||
|
||||
It offers full support for single page applications and custom 404 error pages.
|
||||
Single-page applications require a file named `200.html` placed in the root directory,
|
||||
which will be served in case no file exists for the requested path. A custom 404 error
|
||||
page is defined using a file named `404.html` in the root directory.
|
||||
It is a valuable tool in open source software development: you can quickly look at test
|
||||
reports or coverage data or showcase your single page web applications to your
|
||||
teammates.
|
||||
|
||||
Artifactview displays a file listing if there is no `index.html` or fallback page
|
||||
present, so you can browse artifacts that dont contain websites.
|
||||
## Features
|
||||
|
||||

|
||||
- 📦 Quickly view CI artifacts in your browser without messing with zip files
|
||||
- 📂 File listing for directories without index page
|
||||
- 🏠 Every artifact has a unique subdomain to support pages with absolute paths
|
||||
- 🌎 Full SPA support with `200.html` and `404.html` fallback pages
|
||||
- 👁️ Viewer for Markdown, syntax-highlighted code and JUnit test reports
|
||||
- 🐵 Greasemonkey userscript to automatically add a "View artifact" button to
|
||||
GitHub/Gitea/Forgejo
|
||||
- 🦀 Fast and efficient, only extracts files from zip archive if the client does not support gzip
|
||||
- 🔗 Automatically creates pull request comments with links to all build artifacts
|
||||
|
||||
## How to use
|
||||
|
||||
|
@ -27,6 +33,153 @@ box on the main page. You can also pass the run URL with the `?url=` parameter.
|
|||
Artifactview will show you a selection page where you will be able to choose the
|
||||
artifact you want to browse.
|
||||
|
||||
If there is no `index.html` or fallback page present, a file listing will be shown so
|
||||
you can browse the contents of the artifact.
|
||||
|
||||

|
||||
|
||||
If you want to use Artifactview to showcase a static website, you can make use of
|
||||
fallback pages. If a file named `200.html` is placed in the root directory, it will be
|
||||
served in case no file exists for the requested path. This allows serving single-page
|
||||
applications with custom routing. A custom 404 error page is defined using a file named
|
||||
`404.html` in the root directory.
|
||||
|
||||
The behavior is the same as with other web hosts like surge.sh, so a lot of website
|
||||
build tools already follow that convention.
|
||||
|
||||
Artifactview includes different viewers to better display files of certain types that
|
||||
browsers cannot handle by default. There is a renderer for markdown files as well as a
|
||||
syntax highlighter for source code files. The viewers are only shown if the files are
|
||||
accessed with the `?viewer=` URL parameter which is automatically set when opening a
|
||||
file from a directory listing. You can always download the raw version of the file via
|
||||
the link in the top right corner.
|
||||
|
||||

|
||||
|
||||
Artifactview even includes an interactive viewer for JUnit test reports (XML files with
|
||||
`junit` in their filename). The application has been designed to be easily extendable,
|
||||
so if you have suggestions on other viewers that should be added, feel free to create an
|
||||
issue or a PR.
|
||||
|
||||

|
||||
|
||||
Accessing Artifactview by copying the CI run URL into its homepage may be a little bit
|
||||
tedious. That's why there are some convenient alternatives available.
|
||||
|
||||
You can install the Greasemonkey userscript from the link at the bottom of the homepage.
|
||||
The script adds a "View artifact" link with an eye icon next to every CI artifact on
|
||||
both GitHub and Forgejo.
|
||||
|
||||
If you want to give every collaborator to your project easy access to previews, you can
|
||||
automatically create a comment with links to the artifacts under every pull request.
|
||||
|
||||

|
||||
|
||||
To accomplish that, simply add this step to your CI workflow (after uploading the
|
||||
artifacts).
|
||||
|
||||
```yaml
|
||||
- name: 🔗 Artifactview PR comment
|
||||
if: ${{ always() && github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
if [[ "$GITEA_ACTIONS" == "true" ]]; then RUN_NUMBER="$GITHUB_RUN_NUMBER"; else RUN_NUMBER="$GITHUB_RUN_ID"; fi
|
||||
curl -SsL --fail-with-body -w "\n" -X POST https://av.thetadev.de/.well-known/api/prComment -H "Content-Type: application/json" \
|
||||
--data '{"url": "'"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$RUN_NUMBER"'", "pr": ${{ github.event.number }}}'
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
Artifactview does have a HTTP API to access data about the CI artifacts. To make the API
|
||||
available to every site without interfering with any paths from the artifacts, the
|
||||
endpoints are located within the reserved `/.well-known/api` directory.
|
||||
|
||||
### Get list of artifacts of a CI run
|
||||
|
||||
`GET /.well-known/api/artifacts?url=<RUN_URL>`
|
||||
|
||||
`GET <HOST>--<USER>--<REPO>--<RUN>-<ARTIFACT>.example.com/.well-known/api/artifacts`
|
||||
|
||||
**Response**
|
||||
|
||||
**Note:** the difference between `download_url` and `user_download_url` is that the
|
||||
first one is used by the API client and the second one is shown to the user.
|
||||
`user_download_url` is only set for GitHub artifacts. Forgejo does not have different
|
||||
download URLs since it does not require authentication to download artifacts.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Example",
|
||||
"size": 1523222,
|
||||
"expired": false,
|
||||
"download_url": "https://codeberg.org/thetadev/artifactview/actions/runs/28/artifacts/Example",
|
||||
"user_download_url": null
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Get metadata of the current artifact
|
||||
|
||||
`GET <HOST>--<USER>--<REPO>--<RUN>-<ARTIFACT>.example.com/.well-known/api/artifact`
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Example",
|
||||
"size": 1523222,
|
||||
"expired": false,
|
||||
"download_url": "https://codeberg.org/thetadev/artifactview/actions/runs/28/artifacts/Example",
|
||||
"user_download_url": null
|
||||
}
|
||||
```
|
||||
|
||||
### Get all files from the artifact
|
||||
|
||||
`GET <HOST>--<USER>--<REPO>--<RUN>-<ARTIFACT>.example.com/.well-known/api/files`
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "name": "example.rs", "size": 406, "crc32": "2013120c" },
|
||||
{ "name": "README.md", "size": 13060, "crc32": "61c692f0" }
|
||||
]
|
||||
```
|
||||
|
||||
### Create a pull request comment
|
||||
|
||||
`POST /.well-known/api/prComment`
|
||||
|
||||
Artifactview can create a comment under a pull request containing links to view the
|
||||
artifacts. This way everyone looking at a project can easily access the artifact
|
||||
previews.
|
||||
|
||||
To use this feature, you need to setup an access token with the permission to create
|
||||
comments for every code forge you want to use (more details in the section
|
||||
[Access tokens](#access-tokens)).
|
||||
|
||||
To prevent abuse and spamming, this endpoint is rate-limited and Artifactview will only
|
||||
create comments after it verified that the workflow matches the given pull request and
|
||||
the worflow is still running.
|
||||
|
||||
| JSON parameter | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `url` (string) ❕ | CI workflow URL<br />Example: https://codeberg.org/ThetaDev/artifactview/actions/runs/31 |
|
||||
| `pr` (int) ❕ | Pull request number |
|
||||
| `recreate` (bool) | If set to true, the pull request comment will be deleted and recreated if it already exists. If set to false or omitted, the comment will be edited instead. |
|
||||
| `title` (string) | Comment title (default: "Latest build artifacts") |
|
||||
| `artifact_titles` (map) | Set custom titles for your artifacts.<br />Example: `{"Hello": "🏠 Hello World ;-)"}` |
|
||||
| `artifact_paths` (map) | Set custom paths for your artifacts if you want the links to point to a specific file (e.g. a test report).<br />Example: `{"Test": "/junit.xml?viewer=1"}` |
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{ "status": 200, "msg": "created comment #2183634497" }
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
You can run artifactview using the docker image provided under
|
||||
|
@ -70,27 +223,56 @@ networks:
|
|||
|
||||
Artifactview is configured using environment variables.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `PORT` | 3000 | HTTP port |
|
||||
| `CACHE_DIR` | /tmp/artifactview | Temporary directory where to store the artifacts |
|
||||
| `ROOT_DOMAIN` | localhost:3000 | Public hostname+port number under which artifactview is accessible. If this is configured incorrectly, artifactview will show the error message "host does not end with configured ROOT_DOMAIN" |
|
||||
| `RUST_LOG` | info | Logging level |
|
||||
| `NO_HTTPS` | false | Set to True if the website is served without HTTPS (used if testing artifactview without an ) |
|
||||
| `MAX_ARTIFACT_SIZE` | 100000000 (100 MB) | Maximum size of the artifact zip file to be downloaded |
|
||||
| `MAX_FILE_SIZE` | 100000000 (100 MB) | Maximum contained file size to be served |
|
||||
| `MAX_FILE_COUNT` | 10000 | Maximum amount of files within a zip file |
|
||||
| `MAX_AGE_H` | 12 | Maximum age in hours after which cached artifacts are deleted |
|
||||
| `ZIP_TIMEOUT_MS` | 1000 | Maximum time in milliseconds for reading the index of a zip file. If this takes too long, the zip file is most likely excessively large or malicious (zip bomb) |
|
||||
| `GITHUB_TOKEN` | - | GitHub API token for downloading artifacts. Using a fine-grained token with public read permissions is recommended |
|
||||
| `MEM_CACHE_SIZE` | 50 | Artifactview keeps artifact metadata as well as the zip file indexes in memory to improve performance. The amount of cached items is adjustable. |
|
||||
| `REAL_IP_HEADER` | - | Get the client IP address from a HTTP request header<br />If Artifactview is exposed to the network directly, this option has to be unset. If you are using a reverse proxy the proxy needs to be configured to send the actual client IP as a request header.<br />For most proxies this header is `x-forwarded-for`. |
|
||||
| `LIMIT_ARTIFACTS_PER_MIN` | 5 | Limit the amount of downloaded artifacts per IP address and minute |
|
||||
| `REPO_BLACKLIST` | - | List of sites/users/repos that can NOT be accessed. The blacklist takes precedence over the whitelist (repos included in both lists cannot be accessed)<br />Example: `github.com/evil-corp/world-destruction;codeberg.org/blackhat;example.org` |
|
||||
| `REPO_WHITELIST` | - | List of sites/users/repos that can ONLY be accessed. If the whitelist is empty, it will be ignored and any repository can be accessed. Uses the same syntax as `REPO_BLACKLIST`. |
|
||||
| `SITE_ALIASES` | - | Aliases for sites to make URLs shorter<br />Example: `gh => github.com;cb => codeberg.org` |
|
||||
| `SUGGESTED_SITES` | codeberg.org; github.com; gitea.com | List of suggested code forges (host only, without https://, separated by `;`). If repo_whitelist is empty, this value is used for the matched sites in the userscript. The first value is used in the placeholder URL on the home page. |
|
||||
| `VIEWER_MAX_SIZE` | 500000 | Maximum file size to be displayed using the viewer |
|
||||
Note that some variables contain lists and maps of values. Lists need to have their
|
||||
values separated with semicolons. Maps use an arrow `=>` between key and value, with
|
||||
pairs separated by semicolons.
|
||||
|
||||
Example list: `foo;bar`, example map: `foo=>f1;bar=>b1`
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `PORT` | 3000 | HTTP port |
|
||||
| `CACHE_DIR` | /tmp/artifactview | Temporary directory where to store the artifacts |
|
||||
| `ROOT_DOMAIN` | localhost:3000 | Public hostname+port number under which artifactview is accessible. If this is configured incorrectly, artifactview will show the error message "host does not end with configured ROOT_DOMAIN" |
|
||||
| `RUST_LOG` | info | Logging level |
|
||||
| `NO_HTTPS` | false | Set to True if the website is served without HTTPS (used if testing artifactview without an ) |
|
||||
| `MAX_ARTIFACT_SIZE` | 100000000 (100 MB) | Maximum size of the artifact zip file to be downloaded |
|
||||
| `MAX_FILE_SIZE` | 100000000 (100 MB) | Maximum contained file size to be served |
|
||||
| `MAX_FILE_COUNT` | 10000 | Maximum amount of files within a zip file |
|
||||
| `MAX_AGE_H` | 12 | Maximum age in hours after which cached artifacts are deleted |
|
||||
| `ZIP_TIMEOUT_MS` | 1000 | Maximum time in milliseconds for reading the index of a zip file. If this takes too long, the zip file is most likely excessively large or malicious (zip bomb) |
|
||||
| `GITHUB_TOKEN` | - | GitHub API token for downloading artifacts and creating PR comments. Using a fine-grained token with public read permissions is recommended |
|
||||
| `FORGEJO_TOKENS` | - | Forgejo API tokens for creating PR comments<br />Example: `codeberg.org=>fc010f65348468d05e570806275528c936ce93a4` |
|
||||
| `MEM_CACHE_SIZE` | 50 | Artifactview keeps artifact metadata as well as the zip file indexes in memory to improve performance. The amount of cached items is adjustable. |
|
||||
| `REAL_IP_HEADER` | - | Get the client IP address from a HTTP request header<br />If Artifactview is exposed to the network directly, this option has to be unset. If you are using a reverse proxy the proxy needs to be configured to send the actual client IP as a request header.<br />For most proxies this header is `x-forwarded-for`. |
|
||||
| `LIMIT_ARTIFACTS_PER_MIN` | 5 | Limit the amount of downloaded artifacts per IP address and minute to prevent excessive resource usage. |
|
||||
| `LIMIT_PR_COMMENTS_PER_MIN` | 5 | Limit the amount of pull request comment requests per IP address and minute to prevent spamming. |
|
||||
| `REPO_BLACKLIST` | - | List of sites/users/repos that can NOT be accessed. The blacklist takes precedence over the whitelist (repos included in both lists cannot be accessed)<br />Example: `github.com/evil-corp/world-destruction;codeberg.org/blackhat;example.org` |
|
||||
| `REPO_WHITELIST` | - | List of sites/users/repos that can ONLY be accessed. If the whitelist is empty, it will be ignored and any repository can be accessed. Uses the same syntax as `REPO_BLACKLIST`. |
|
||||
| `SITE_ALIASES` | - | Aliases for sites to make URLs shorter<br />Example: `gh => github.com;cb => codeberg.org` |
|
||||
| `SUGGESTED_SITES` | codeberg.org; github.com; gitea.com | List of suggested code forges (host only, without https://, separated by `;`). If repo_whitelist is empty, this value is used for the matched sites in the userscript. The first value is used in the placeholder URL on the home page. |
|
||||
| `VIEWER_MAX_SIZE` | 500000 | Maximum file size to be displayed using the viewer |
|
||||
|
||||
### Access tokens
|
||||
|
||||
GitHub does not allow downloading artifacts for public repositories for unauthenticated
|
||||
users. So you need to setup an access token to use Artifactview with GitHub.
|
||||
|
||||
If you are not using the `prComment` feature, you can use a fine-grained access token
|
||||
with the "Public repositories (read-only)" permission. If you want to create pull
|
||||
request comments, you have to use a classic token with the "public_repo" scope enabled
|
||||
(the fine-grained tokens did not work in my test).
|
||||
|
||||
Forgejo does not require access tokens to download artifacts on public repositories, so
|
||||
you only need to create a token if you want to use the `prComment`-API. In this case,
|
||||
the token needs the following permissions:
|
||||
|
||||
- Repository and Organization Access: Public only
|
||||
- issue: Read and write
|
||||
- user: Read (for determining own user ID)
|
||||
|
||||
Note that if you are using Artifactview to create pull request comments, it is
|
||||
recommended to create a second bot account instead of using your main account.
|
||||
|
||||
## Technical details
|
||||
|
||||
|
@ -104,8 +286,8 @@ Example: `https://github-com--theta-dev--example-project--4-11.example.com`
|
|||
The reason for using subdomains instead of URL paths is that many websites expect to be
|
||||
served from a separate subdomain and access resources using absolute paths. Using URLs
|
||||
like `example.com/github.com/theta-dev/example-project/4/11/path/to/file` would make the
|
||||
application easier to host, but it would not be possible to simply preview a
|
||||
React/Vue/Svelte web project.
|
||||
application easier to host, but it would not be possible to preview a React/Vue/Svelte
|
||||
web project.
|
||||
|
||||
Since domains only allow letters, numbers and dashes but repository names allow dots and
|
||||
underscores, these escape sequences are used to access repositories with special
|
||||
|
@ -137,7 +319,7 @@ website (like `.well-known/acme-challenge` for issuing TLS certificates), Artifa
|
|||
will serve no files from the `.well-known` folder.
|
||||
|
||||
There is a configurable limit for both the maximum downloaded artifact size and the
|
||||
maximum size of individual files to be served (100MB by default). Additionally there is
|
||||
maximum size of individual files to be served (100 MB by default). Additionally there is
|
||||
a configurable timeout for the zip file indexing operation. These measures should
|
||||
protect the server againt denial-of-service attacks like overfilling the server drive or
|
||||
uploading zip bombs.
|
||||
protect the server against denial-of-service attacks like overfilling the server drive
|
||||
or uploading zip bombs.
|
||||
|
|
|
@ -37,7 +37,7 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||
crc32fast = "1"
|
||||
futures-lite = { version = "2.1.0", default-features = false, features = ["std"] }
|
||||
pin-project = "1"
|
||||
thiserror = "1"
|
||||
thiserror = "2"
|
||||
|
||||
async-compression = { version = "0.4.2", default-features = false, features = ["futures-io"], optional = true }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"], optional = true }
|
||||
|
@ -49,7 +49,7 @@ tokio-util = { version = "0.7", features = ["compat"], optional = true }
|
|||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7", features = ["compat"] }
|
||||
env_logger = "0.11.2"
|
||||
zip = "0.6.3"
|
||||
zip = "2.2.2"
|
||||
|
||||
# shared across multiple examples
|
||||
# anyhow = "1"
|
||||
|
|
|
@ -51,7 +51,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, R, E> AsyncRead for ZipEntryReader<'a, R, E>
|
||||
impl<R, E> AsyncRead for ZipEntryReader<'_, R, E>
|
||||
where
|
||||
R: AsyncBufRead + Unpin,
|
||||
{
|
||||
|
@ -60,7 +60,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, R, E> ZipEntryReader<'a, R, E>
|
||||
impl<R, E> ZipEntryReader<'_, R, E>
|
||||
where
|
||||
R: AsyncBufRead + Unpin,
|
||||
{
|
||||
|
@ -118,7 +118,7 @@ enum OwnedEntry<'a> {
|
|||
Borrow(&'a ZipEntry),
|
||||
}
|
||||
|
||||
impl<'a> OwnedEntry<'a> {
|
||||
impl OwnedEntry<'_> {
|
||||
pub fn entry(&self) -> &'_ ZipEntry {
|
||||
match self {
|
||||
OwnedEntry::Owned(entry) => entry,
|
||||
|
|
|
@ -17,7 +17,7 @@ pub(crate) enum OwnedReader<'a, R> {
|
|||
Borrow(#[pin] &'a mut R),
|
||||
}
|
||||
|
||||
impl<'a, R> OwnedReader<'a, R>
|
||||
impl<R> OwnedReader<'_, R>
|
||||
where
|
||||
R: AsyncBufRead + Unpin,
|
||||
{
|
||||
|
@ -30,7 +30,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, R> AsyncBufRead for OwnedReader<'a, R>
|
||||
impl<R> AsyncBufRead for OwnedReader<'_, R>
|
||||
where
|
||||
R: AsyncBufRead + Unpin,
|
||||
{
|
||||
|
@ -49,7 +49,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, R> AsyncRead for OwnedReader<'a, R>
|
||||
impl<R> AsyncRead for OwnedReader<'_, R>
|
||||
where
|
||||
R: AsyncBufRead + Unpin,
|
||||
{
|
||||
|
|
|
@ -64,7 +64,7 @@ impl<'b, W: AsyncWrite + Unpin> CompressedAsyncWriter<'b, W> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'b, W: AsyncWrite + Unpin> AsyncWrite for CompressedAsyncWriter<'b, W> {
|
||||
impl<W: AsyncWrite + Unpin> AsyncWrite for CompressedAsyncWriter<'_, W> {
|
||||
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<std::result::Result<usize, Error>> {
|
||||
match *self {
|
||||
CompressedAsyncWriter::Stored(ref mut inner) => Pin::new(inner).poll_write(cx, buf),
|
||||
|
|
|
@ -251,7 +251,7 @@ impl<'b, W: AsyncWrite + Unpin> EntryStreamWriter<'b, W> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, W: AsyncWrite + Unpin> AsyncWrite for EntryStreamWriter<'a, W> {
|
||||
impl<W: AsyncWrite + Unpin> AsyncWrite for EntryStreamWriter<'_, W> {
|
||||
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<std::result::Result<usize, Error>> {
|
||||
let poll = Pin::new(&mut self.writer).poll_write(cx, buf);
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ fn generate_zip64many_zip() -> std::path::PathBuf {
|
|||
|
||||
let zip_file = std::fs::File::create(&path).unwrap();
|
||||
let mut zip = zip::ZipWriter::new(zip_file);
|
||||
let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||
let options = FileOptions::<()>::default().compression_method(zip::CompressionMethod::Stored);
|
||||
|
||||
for i in 0..2_u32.pow(16) + 1 {
|
||||
zip.start_file(format!("{i}.txt"), options).unwrap();
|
||||
|
|
|
@ -36,7 +36,7 @@ async fn test_write_zip64_file() {
|
|||
let cursor = std::io::Cursor::new(buffer);
|
||||
let mut zip = zip::read::ZipArchive::new(cursor).unwrap();
|
||||
let mut file1 = zip.by_name("file1").unwrap();
|
||||
assert_eq!(file1.extra_data(), &[] as &[u8]);
|
||||
assert_eq!(file1.extra_data(), Some(&[] as &[u8]));
|
||||
let mut buffer = Vec::new();
|
||||
file1.read_to_end(&mut buffer).unwrap();
|
||||
assert_eq!(buffer.as_slice(), &[0, 0, 0, 0]);
|
||||
|
|
|
@ -77,7 +77,7 @@ where
|
|||
|
||||
struct Val(String, String);
|
||||
|
||||
impl<'de> IntoDeserializer<'de, Error> for Val {
|
||||
impl IntoDeserializer<'_, Error> for Val {
|
||||
type Deserializer = Self;
|
||||
|
||||
fn into_deserializer(self) -> Self::Deserializer {
|
||||
|
@ -87,7 +87,7 @@ impl<'de> IntoDeserializer<'de, Error> for Val {
|
|||
|
||||
struct VarName(String);
|
||||
|
||||
impl<'de> IntoDeserializer<'de, Error> for VarName {
|
||||
impl IntoDeserializer<'_, Error> for VarName {
|
||||
type Deserializer = Self;
|
||||
|
||||
fn into_deserializer(self) -> Self::Deserializer {
|
||||
|
@ -248,7 +248,7 @@ struct Deserializer<'de, Iter: Iterator<Item = (String, String)>> {
|
|||
inner: MapDeserializer<'de, Vars<Iter>, Error>,
|
||||
}
|
||||
|
||||
impl<'de, Iter: Iterator<Item = (String, String)>> Deserializer<'de, Iter> {
|
||||
impl<Iter: Iterator<Item = (String, String)>> Deserializer<'_, Iter> {
|
||||
fn new(vars: Iter) -> Self {
|
||||
Deserializer {
|
||||
inner: MapDeserializer::new(Vars(vars)),
|
||||
|
@ -308,7 +308,7 @@ where
|
|||
/// These types are created with with the [prefixed](fn.prefixed.html) module function
|
||||
pub struct Prefixed<'a>(Cow<'a, str>);
|
||||
|
||||
impl<'a> Prefixed<'a> {
|
||||
impl Prefixed<'_> {
|
||||
/// Deserializes a type based on prefixed env variables
|
||||
pub fn from_env<T>(&self) -> Result<T>
|
||||
where
|
||||
|
@ -390,7 +390,7 @@ impl<'a> SplitEscaped<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for SplitEscaped<'a> {
|
||||
impl Iterator for SplitEscaped<'_> {
|
||||
type Item = String;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
|
|
|
@ -7,8 +7,8 @@ license = "BSD-2-Clause"
|
|||
repository = "https://github.com/borisfaure/junit-parser"
|
||||
|
||||
[dependencies]
|
||||
quick-xml = { version = "0.32.0", features = ["escape-html"] }
|
||||
thiserror = "1.0.61"
|
||||
quick-xml = { version = "0.37.0", features = ["escape-html"] }
|
||||
thiserror = "2.0.0"
|
||||
time = { version = "0.3.36", features = ["parsing", "serde-well-known"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
|
|
14
renovate.json
Normal file
14
renovate.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:best-practices",
|
||||
":semanticCommitTypeAll(chore)",
|
||||
":preserveSemverRanges"
|
||||
],
|
||||
"automerge": true,
|
||||
"automergeStrategy": "squash",
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"labels": ["dependency-upgrade"],
|
||||
"enabledManagers": ["cargo"],
|
||||
"prHourlyLimit": 5
|
||||
}
|
BIN
resources/screenshotCode.png
Normal file
BIN
resources/screenshotCode.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
BIN
resources/screenshotJUnit.png
Normal file
BIN
resources/screenshotJUnit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
BIN
resources/screenshotPrComment.png
Normal file
BIN
resources/screenshotPrComment.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
365
src/app.rs
365
src/app.rs
|
@ -1,17 +1,25 @@
|
|||
use std::{
|
||||
collections::BTreeMap, net::SocketAddr, ops::Bound, path::Path, str::FromStr, sync::Arc,
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::Write,
|
||||
net::{IpAddr, SocketAddr},
|
||||
ops::Bound,
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use async_zip::tokio::read::ZipEntryReader;
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Host, Request, State},
|
||||
extract::{Query as XQuery, Request, State},
|
||||
http::{Response, Uri},
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{any, get},
|
||||
Router,
|
||||
routing::{any, get, post},
|
||||
Json, RequestExt, Router,
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use futures_lite::AsyncReadExt as LiteAsyncReadExt;
|
||||
use governor::{Quota, RateLimiter};
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use serde::Deserialize;
|
||||
|
@ -29,7 +37,7 @@ use tower_http::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
artifact_api::ArtifactApi,
|
||||
artifact_api::{Artifact, ArtifactApi, WorkflowRun},
|
||||
cache::{Cache, CacheEntry, GetFileResult, GetFileResultFile},
|
||||
config::Config,
|
||||
error::Error,
|
||||
|
@ -52,6 +60,12 @@ struct AppInner {
|
|||
cache: Cache,
|
||||
api: ArtifactApi,
|
||||
viewers: Viewers,
|
||||
lim_pr_comment: Option<
|
||||
governor::DefaultKeyedRateLimiter<
|
||||
IpAddr,
|
||||
governor::middleware::NoOpMiddleware<governor::clock::QuantaInstant>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
|
@ -65,6 +79,39 @@ struct FileQparams {
|
|||
viewer: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UrlQuery {
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PrCommentReq {
|
||||
/// Workflow run URL
|
||||
url: String,
|
||||
/// Pull request number
|
||||
pr: u64,
|
||||
/// If set to true, it will delete the previous PR comment and create a new one instead
|
||||
/// of updating it
|
||||
#[serde(default)]
|
||||
recreate: bool,
|
||||
/// Comment title
|
||||
///
|
||||
/// Default: "👁️ Latest build artifacts"
|
||||
title: Option<String>,
|
||||
/// Map of custom artifact titles
|
||||
#[serde(default)]
|
||||
artifact_titles: HashMap<String, String>,
|
||||
/// Map of custom artifact paths
|
||||
///
|
||||
/// If you want the artifact links in the comment to point to a specific file in the
|
||||
/// artifact (e.g. a test report), you can set a custom path for the artifact
|
||||
#[serde(default)]
|
||||
artifact_paths: HashMap<String, String>,
|
||||
}
|
||||
|
||||
const DATE_FORMAT: &[time::format_description::FormatItem] =
|
||||
time::macros::format_description!("[day].[month].[year] [hour]:[minute]:[second]");
|
||||
|
||||
const FAVICON_PATH: &str = "/favicon.ico";
|
||||
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
|
@ -126,9 +173,10 @@ impl App {
|
|||
.route("/.well-known/api/artifacts", get(Self::get_artifacts))
|
||||
.route("/.well-known/api/artifact", get(Self::get_artifact))
|
||||
.route("/.well-known/api/files", get(Self::get_files))
|
||||
.route("/.well-known/api/prComment", post(Self::pr_comment))
|
||||
// Prevent access to the .well-known folder since it enables abuse
|
||||
// (e.g. SSL certificate registration by an attacker)
|
||||
.route("/.well-known/*path", any(|| async { Error::Inaccessible }))
|
||||
.route("/.well-known/{*path}", any(|| async { Error::Inaccessible }))
|
||||
// Serve artifact pages
|
||||
.route("/", get(Self::get_page))
|
||||
.fallback(get(Self::get_page))
|
||||
|
@ -331,8 +379,9 @@ impl App {
|
|||
.query()
|
||||
.and_then(|q| serde_urlencoded::from_str::<Params>(q).ok())
|
||||
{
|
||||
let query = RunQuery::from_forge_url(¶ms.url, &state.i.cfg.load().site_aliases)?;
|
||||
let artifacts = state.i.api.list(&query).await?;
|
||||
let query =
|
||||
RunQuery::from_forge_url_alias(¶ms.url, &state.i.cfg.load().site_aliases)?;
|
||||
let artifacts = state.i.api.list(&query, true).await?;
|
||||
|
||||
if artifacts.is_empty() {
|
||||
Err(Error::NotFound("artifacts".into()))
|
||||
|
@ -545,7 +594,7 @@ impl App {
|
|||
.typed_header(headers::ContentLength(content_length))
|
||||
.typed_header(
|
||||
headers::ContentRange::bytes(range, total_len)
|
||||
.map_err(|e| Error::Internal(e.to_string().into()))?,
|
||||
.map_err(|e| Error::Other(e.to_string().into()))?,
|
||||
)
|
||||
.body(Body::from_stream(ReaderStream::new(
|
||||
bufreader.take(content_length),
|
||||
|
@ -562,11 +611,18 @@ impl App {
|
|||
async fn get_artifacts(
|
||||
State(state): State<AppState>,
|
||||
Host(host): Host,
|
||||
url_query: XQuery<UrlQuery>,
|
||||
) -> Result<Response<Body>, ErrorJson> {
|
||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||
let query = ArtifactQuery::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
|
||||
let query = match &url_query.url {
|
||||
Some(url) => RunQuery::from_forge_url(url)?,
|
||||
None => {
|
||||
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
|
||||
ArtifactQuery::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?.into()
|
||||
}
|
||||
};
|
||||
|
||||
state.i.cfg.check_filterlist(&query)?;
|
||||
let artifacts = state.i.api.list(&query.into()).await?;
|
||||
let artifacts = state.i.api.list(&query, true).await?;
|
||||
Ok(Response::builder().cache().json(&artifacts)?)
|
||||
}
|
||||
|
||||
|
@ -603,6 +659,84 @@ impl App {
|
|||
.json(&files)?)
|
||||
}
|
||||
|
||||
/// Create a comment under a workflow's pull request with links to view the artifacts
|
||||
///
|
||||
/// To prevent abuse/spamming, Artifactview will only create a comment if
|
||||
/// - The workflow is still running
|
||||
/// - The workflow was triggered by the given pull request
|
||||
async fn pr_comment(
|
||||
State(state): State<AppState>,
|
||||
request: Request,
|
||||
) -> Result<ErrorJson, ErrorJson> {
|
||||
let ip = util::get_ip_address(&request, state.i.cfg.load().real_ip_header.as_deref())?;
|
||||
let req = request
|
||||
.extract::<Json<PrCommentReq>, _>()
|
||||
.await
|
||||
.map_err(|e| Error::BadRequest(e.body_text().into()))?;
|
||||
let query = RunQuery::from_forge_url_alias(&req.url, &state.i.cfg.load().site_aliases)?;
|
||||
|
||||
if let Some(limiter) = &state.i.lim_pr_comment {
|
||||
limiter.check_key(&ip).map_err(Error::from)?;
|
||||
}
|
||||
|
||||
let run = state.i.api.workflow_run(&query).await?;
|
||||
if !run.from_pr {
|
||||
return Err(
|
||||
Error::BadRequest("workflow run not triggered by pull request".into()).into(),
|
||||
);
|
||||
}
|
||||
if run.done {
|
||||
return Err(Error::BadRequest("workflow run is not running".into()).into());
|
||||
}
|
||||
if let Some(pr_number) = run.pr_number {
|
||||
if pr_number != req.pr {
|
||||
return Err(Error::BadRequest(
|
||||
format!(
|
||||
"workflow run was triggered by pr#{}, expected: {}",
|
||||
pr_number, req.pr
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
} else {
|
||||
let pr = state.i.api.get_pr(query.as_ref(), req.pr).await?;
|
||||
if run.head_sha != pr.head.sha {
|
||||
return Ok(ErrorJson::ok("head of pr does not match workflow run"));
|
||||
}
|
||||
}
|
||||
|
||||
let artifacts = match state.i.api.list(&query, false).await {
|
||||
Ok(a) => a,
|
||||
Err(Error::NotFound(_)) => return Ok(ErrorJson::ok("no artifacts")),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let old_comment = state.i.api.find_comment(query.as_ref(), req.pr).await?;
|
||||
let content = pr_comment_text(PrCommentTextParams {
|
||||
query: &query,
|
||||
old_comment: old_comment.as_ref().map(|c| c.body.as_str()),
|
||||
run: &run,
|
||||
artifacts: &artifacts,
|
||||
title: req.title.as_deref(),
|
||||
artifact_titles: &req.artifact_titles,
|
||||
artifact_paths: &req.artifact_paths,
|
||||
cfg: &state.i.cfg,
|
||||
});
|
||||
|
||||
let c_id = state
|
||||
.i
|
||||
.api
|
||||
.add_comment(
|
||||
query.as_ref(),
|
||||
req.pr,
|
||||
&content,
|
||||
old_comment.map(|c| c.id),
|
||||
req.recreate,
|
||||
)
|
||||
.await?;
|
||||
Ok(ErrorJson::ok(format!("created comment #{c_id}")))
|
||||
}
|
||||
|
||||
fn favicon() -> Result<Response<Body>, Error> {
|
||||
Ok(Response::builder()
|
||||
.typed_header(headers::ContentType::from_str("image/x-icon").unwrap())
|
||||
|
@ -620,7 +754,7 @@ impl App {
|
|||
.typed_header(headers::ContentType::from(mime::TEXT_CSS))
|
||||
.cache_immutable();
|
||||
|
||||
// Dont serve compressed stylesheets in debug mode to allow live changes
|
||||
// Don't serve compressed stylesheets in debug mode to allow live changes
|
||||
#[cfg(not(debug_assertions))]
|
||||
if util::accepts_gzip(hdrs) {
|
||||
return Ok(resp
|
||||
|
@ -642,10 +776,14 @@ impl AppState {
|
|||
let api = ArtifactApi::new(cfg.clone());
|
||||
Self {
|
||||
i: Arc::new(AppInner {
|
||||
cfg,
|
||||
cache,
|
||||
api,
|
||||
viewers: Viewers::new(),
|
||||
lim_pr_comment: cfg
|
||||
.load()
|
||||
.limit_artifacts_per_min
|
||||
.map(|lim| RateLimiter::keyed(Quota::per_minute(lim))),
|
||||
cfg,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -689,3 +827,202 @@ fn path_components(
|
|||
}
|
||||
path_components
|
||||
}
|
||||
|
||||
struct PrCommentTextParams<'a> {
|
||||
query: &'a RunQuery,
|
||||
old_comment: Option<&'a str>,
|
||||
run: &'a WorkflowRun,
|
||||
artifacts: &'a [Artifact],
|
||||
title: Option<&'a str>,
|
||||
artifact_titles: &'a HashMap<String, String>,
|
||||
artifact_paths: &'a HashMap<String, String>,
|
||||
cfg: &'a Config,
|
||||
}
|
||||
|
||||
/// Build pull request comment text
|
||||
#[allow(clippy::assigning_clones)]
|
||||
fn pr_comment_text(p: PrCommentTextParams) -> String {
|
||||
let query = p.query;
|
||||
let mut content = "### ".to_owned();
|
||||
let mut prevln = "- ".to_owned();
|
||||
let a_opts = r#"target="_blank" rel="noopener noreferrer""#;
|
||||
let date_started = p
|
||||
.run
|
||||
.date_started
|
||||
.and_then(|d| d.to_offset(time::UtcOffset::UTC).format(&DATE_FORMAT).ok());
|
||||
|
||||
let mut prev_builds = None;
|
||||
let mut np_content = None;
|
||||
if let Some(old_comment) = p.old_comment {
|
||||
prev_builds = util::extract_delim(old_comment, "</summary>", "<!--NEXT_PREV");
|
||||
np_content = util::extract_delim(old_comment, "<!--NEXT_PREV", "-->");
|
||||
}
|
||||
|
||||
let write_commit = |s: &mut String, sha: &str| {
|
||||
_ = write!(
|
||||
s,
|
||||
"[{}](https://{}/{}/{}/commit/{})",
|
||||
&sha[..10],
|
||||
query.host,
|
||||
query.user,
|
||||
query.repo,
|
||||
sha
|
||||
);
|
||||
};
|
||||
|
||||
let write_link_icon = |s: &mut String, title: &str, href: &str| {
|
||||
let (title_pfx, title) = util::split_icon_prefix(title);
|
||||
_ = write!(s, r#"{title_pfx}<a href="{href}" {a_opts}>{title}</a>"#,);
|
||||
};
|
||||
|
||||
// Comment title
|
||||
let run_url = query.forge_url();
|
||||
let artifacts_url = format!("{}/?url={}", p.cfg.main_url(), run_url);
|
||||
write_link_icon(
|
||||
&mut content,
|
||||
p.title.unwrap_or("Latest build artifacts"),
|
||||
&artifacts_url,
|
||||
);
|
||||
_ = write!(&mut content, "\n\n Run [#{}]({}) · ", query.run, run_url);
|
||||
write_commit(&mut content, &p.run.head_sha);
|
||||
if let Some(date_started) = &date_started {
|
||||
_ = write!(&mut content, " · {} UTC", date_started);
|
||||
}
|
||||
_ = content.write_str("\n\n");
|
||||
|
||||
// Previous run line
|
||||
_ = write!(&mut prevln, "[#{}]({}) [", query.run, run_url);
|
||||
write_commit(&mut prevln, &p.run.head_sha);
|
||||
_ = write!(
|
||||
&mut prevln,
|
||||
"] <a href=\"{artifacts_url}\" {a_opts}>Artifacts</a>: "
|
||||
);
|
||||
|
||||
for a in p.artifacts.iter().filter(|a| !a.expired) {
|
||||
let mut url = p
|
||||
.cfg
|
||||
.url_with_subdomain(&query.subdomain_with_artifact(a.id));
|
||||
// Do not process the same run twice
|
||||
if np_content.as_ref().is_some_and(|c| c.contains(&url)) {
|
||||
np_content = None;
|
||||
}
|
||||
|
||||
if let Some(path) = p.artifact_paths.get(&a.name) {
|
||||
url += path;
|
||||
}
|
||||
|
||||
write_link_icon(
|
||||
&mut content,
|
||||
p.artifact_titles.get(&a.name).unwrap_or(&a.name),
|
||||
&url,
|
||||
);
|
||||
_ = content.write_str("<br>\n");
|
||||
_ = write!(
|
||||
&mut prevln,
|
||||
r#" <a href="{url}" {a_opts}>`{}`</a>,"#,
|
||||
a.name
|
||||
);
|
||||
}
|
||||
|
||||
if prevln.ends_with(',') {
|
||||
prevln.pop();
|
||||
}
|
||||
|
||||
if let Some(date_started) = &date_started {
|
||||
_ = write!(&mut prevln, " ({} UTC)", date_started);
|
||||
}
|
||||
|
||||
if np_content.is_some() || prev_builds.is_some() {
|
||||
_ = write!(
|
||||
&mut content,
|
||||
"<details>\n<summary>Previous builds</summary>\n\n"
|
||||
);
|
||||
if let Some(prev_builds) = prev_builds {
|
||||
_ = writeln!(&mut content, "{prev_builds}");
|
||||
}
|
||||
if let Some(np_content) = np_content {
|
||||
_ = writeln!(&mut content, "{np_content}");
|
||||
}
|
||||
_ = writeln!(&mut content, "<!--NEXT_PREV {prevln} -->\n</details>");
|
||||
} else {
|
||||
_ = writeln!(&mut content, "<!--NEXT_PREV {prevln} -->");
|
||||
}
|
||||
|
||||
_ = write!(&mut content, "\n<sup>generated by [Artifactview {VERSION}](https://codeberg.org/ThetaDev/artifactview)</sup>");
|
||||
content
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use time::macros::datetime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pr_comment() {
|
||||
let mut query = RunQuery::from_forge_url(
|
||||
"https://code.thetadev.de/ThetaDev/test-actions/actions/runs/104",
|
||||
)
|
||||
.unwrap();
|
||||
let artifacts: [Artifact; 3] = [
|
||||
Artifact {
|
||||
id: 1,
|
||||
name: "Hello".to_owned(),
|
||||
size: 0,
|
||||
expired: false,
|
||||
download_url: String::new(),
|
||||
user_download_url: None,
|
||||
},
|
||||
Artifact {
|
||||
id: 2,
|
||||
name: "Test".to_owned(),
|
||||
size: 0,
|
||||
expired: false,
|
||||
download_url: String::new(),
|
||||
user_download_url: None,
|
||||
},
|
||||
Artifact {
|
||||
id: 3,
|
||||
name: "Expired".to_owned(),
|
||||
size: 0,
|
||||
expired: true,
|
||||
download_url: String::new(),
|
||||
user_download_url: None,
|
||||
},
|
||||
];
|
||||
let mut artifact_titles = HashMap::new();
|
||||
artifact_titles.insert("Hello".to_owned(), "🏠 Hello World ;-)".to_owned());
|
||||
let mut artifact_paths = HashMap::new();
|
||||
artifact_paths.insert("Test".to_owned(), "/junit.xml?viewer=1".to_owned());
|
||||
|
||||
let cfg = Config::default();
|
||||
|
||||
let footer = format!("<sup>generated by [Artifactview {VERSION}](https://codeberg.org/ThetaDev/artifactview)</sup>");
|
||||
|
||||
let mut old_comment = None;
|
||||
for i in 1..=3 {
|
||||
query.run = i.into();
|
||||
let run = WorkflowRun {
|
||||
head_sha: format!("{i}5eed48a8382513147a949117ef4aa659989d397"),
|
||||
from_pr: true,
|
||||
pr_number: None,
|
||||
date_started: Some(datetime!(2024-06-15 15:30 UTC).replace_hour(i).unwrap()),
|
||||
done: false,
|
||||
};
|
||||
let comment = pr_comment_text(PrCommentTextParams {
|
||||
query: &query,
|
||||
old_comment: old_comment.as_deref(),
|
||||
run: &run,
|
||||
artifacts: &artifacts,
|
||||
title: None,
|
||||
artifact_titles: &artifact_titles,
|
||||
artifact_paths: &artifact_paths,
|
||||
cfg: &cfg,
|
||||
});
|
||||
let res = comment.replace(&footer, ""); // Remove footer since it depends on the version
|
||||
insta::assert_snapshot!(format!("pr_comment_{i}"), res);
|
||||
|
||||
old_comment = Some(comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
//! API-Client to fetch CI artifacts from Github and Forgejo
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use futures_lite::StreamExt;
|
||||
use http::header;
|
||||
use http::{header, Method};
|
||||
use once_cell::sync::Lazy;
|
||||
use quick_cache::sync::Cache as QuickCache;
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, ClientBuilder, IntoUrl, RequestBuilder, Response, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use secrecy::ExposeSecret;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::{fs::File, io::AsyncWriteExt};
|
||||
|
||||
use crate::{
|
||||
|
@ -19,9 +22,10 @@ pub struct ArtifactApi {
|
|||
http: Client,
|
||||
cfg: Config,
|
||||
qc: QuickCache<String, Vec<Artifact>>,
|
||||
user_ids: QuickCache<String, u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Artifact {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
|
@ -35,7 +39,7 @@ pub struct Artifact {
|
|||
pub user_download_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GithubArtifact {
|
||||
id: u64,
|
||||
name: String,
|
||||
|
@ -44,24 +48,24 @@ struct GithubArtifact {
|
|||
archive_download_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ForgejoArtifact {
|
||||
name: String,
|
||||
size: u64,
|
||||
status: ForgejoArtifactStatus,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ApiError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ArtifactsWrap<T> {
|
||||
artifacts: Vec<T>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ForgejoArtifactStatus {
|
||||
Completed,
|
||||
|
@ -100,6 +104,154 @@ impl ForgejoArtifact {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WorkflowRun {
|
||||
pub head_sha: String,
|
||||
pub from_pr: bool,
|
||||
pub pr_number: Option<u64>,
|
||||
pub date_started: Option<OffsetDateTime>,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ForgejoWorkflowRun {
|
||||
state: ForgejoWorkflowState,
|
||||
logs: ForgejoWorkflowLogs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ForgejoWorkflowState {
|
||||
run: ForgejoWorkflowStateRun,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ForgejoWorkflowStateRun {
|
||||
done: bool,
|
||||
commit: ForgejoWorkflowCommit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ForgejoWorkflowCommit {
|
||||
link: String,
|
||||
branch: ForgejoWorkflowBranch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ForgejoWorkflowBranch {
|
||||
link: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ForgejoWorkflowLogs {
|
||||
steps_log: Vec<ForgejoWorkflowLogStep>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ForgejoWorkflowLogStep {
|
||||
started: i64,
|
||||
lines: Vec<LogMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LogMessage {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct IdEntity {
|
||||
id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Comment {
|
||||
pub id: u64,
|
||||
pub body: String,
|
||||
user: IdEntity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CommentBody<'a> {
|
||||
body: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PullRequest {
|
||||
pub head: Commit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Commit {
|
||||
pub sha: String,
|
||||
}
|
||||
|
||||
const GITHUB_ACCEPT: &str = "application/vnd.github+json";
|
||||
const COMMENT_TAG_PATTERN: &str = "<!-- Artifactview -->";
|
||||
|
||||
impl TryFrom<ForgejoWorkflowRun> for WorkflowRun {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: ForgejoWorkflowRun) -> Result<Self> {
|
||||
static RE_COMMIT_SHA: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^/[\w\-\.]+/[\w\-\.]+/commit/([a-f\d]+)$"#).unwrap());
|
||||
static RE_PULL_ID: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^/[\w\-\.]+/[\w\-\.]+/pulls/(\d+)$"#).unwrap());
|
||||
|
||||
let from_pr = value
|
||||
.logs
|
||||
.steps_log
|
||||
.first()
|
||||
.and_then(|l| l.lines.first())
|
||||
.map(|l| l.message.contains("be triggered by event: pull_request"))
|
||||
.unwrap_or(true);
|
||||
|
||||
Ok(Self {
|
||||
head_sha: RE_COMMIT_SHA
|
||||
.captures(&value.state.run.commit.link)
|
||||
.map(|cap| cap[1].to_string())
|
||||
.ok_or(Error::Other(
|
||||
"could not parse workflow run commit sha".into(),
|
||||
))?,
|
||||
from_pr,
|
||||
pr_number: if from_pr {
|
||||
RE_PULL_ID
|
||||
.captures(&value.state.run.commit.branch.link)
|
||||
.and_then(|cap| cap[1].parse().ok())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
date_started: value
|
||||
.logs
|
||||
.steps_log
|
||||
.first()
|
||||
.and_then(|l| OffsetDateTime::from_unix_timestamp(l.started).ok()),
|
||||
done: value.state.run.done,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GitHubWorkflowRun {
|
||||
head_sha: String,
|
||||
event: String,
|
||||
conclusion: Option<String>,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
run_started_at: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl From<GitHubWorkflowRun> for WorkflowRun {
|
||||
fn from(value: GitHubWorkflowRun) -> Self {
|
||||
Self {
|
||||
head_sha: value.head_sha,
|
||||
from_pr: value.event == "pull_request",
|
||||
pr_number: None,
|
||||
date_started: value.run_started_at,
|
||||
done: value.conclusion.is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArtifactApi {
|
||||
pub fn new(cfg: Config) -> Self {
|
||||
Self {
|
||||
|
@ -112,28 +264,34 @@ impl ArtifactApi {
|
|||
.build()
|
||||
.unwrap(),
|
||||
qc: QuickCache::new(cfg.load().mem_cache_size),
|
||||
user_ids: QuickCache::new(50),
|
||||
cfg,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(&self, query: &RunQuery) -> Result<Vec<Artifact>> {
|
||||
#[tracing::instrument(level = "error", skip_all)]
|
||||
pub async fn list(&self, query: &RunQuery, cached: bool) -> Result<Vec<Artifact>> {
|
||||
let cache_key = query.cache_key();
|
||||
self.qc
|
||||
.get_or_insert_async(&cache_key, async {
|
||||
let res = if query.is_github() {
|
||||
self.list_github(query.as_ref()).await
|
||||
} else {
|
||||
self.list_forgejo(query.as_ref()).await
|
||||
};
|
||||
if res.as_ref().is_ok_and(|v| v.is_empty()) {
|
||||
Err(Error::NotFound("artifact".into()))
|
||||
} else {
|
||||
res
|
||||
}
|
||||
})
|
||||
.await
|
||||
let fut = async {
|
||||
let res = if query.is_github() {
|
||||
self.list_github(query.as_ref()).await
|
||||
} else {
|
||||
self.list_forgejo(query.as_ref()).await
|
||||
};
|
||||
if res.as_ref().is_ok_and(|v| v.is_empty()) {
|
||||
Err(Error::NotFound("artifact".into()))
|
||||
} else {
|
||||
res
|
||||
}
|
||||
};
|
||||
if cached {
|
||||
self.qc.get_or_insert_async(&cache_key, fut).await
|
||||
} else {
|
||||
fut.await
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "error", skip_all)]
|
||||
pub async fn fetch(&self, query: &ArtifactQuery) -> Result<Artifact> {
|
||||
if query.is_github() {
|
||||
self.fetch_github(query).await
|
||||
|
@ -149,6 +307,7 @@ impl ArtifactApi {
|
|||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "error", skip_all)]
|
||||
pub async fn download(&self, artifact: &Artifact, path: &Path) -> Result<()> {
|
||||
if artifact.expired {
|
||||
return Err(Error::Expired);
|
||||
|
@ -177,7 +336,7 @@ impl ArtifactApi {
|
|||
|
||||
let url = Url::parse(&artifact.download_url)?;
|
||||
let req = if url.domain() == Some("api.github.com") {
|
||||
self.get_github(url)
|
||||
self.get_github_any(url)
|
||||
} else {
|
||||
self.http.get(url)
|
||||
};
|
||||
|
@ -212,8 +371,7 @@ impl ArtifactApi {
|
|||
);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.get(url)
|
||||
.get_forgejo(url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
|
@ -236,10 +394,8 @@ impl ArtifactApi {
|
|||
query.user, query.repo, query.run
|
||||
);
|
||||
|
||||
let resp = Self::handle_github_error(self.get_github(url).send().await?)
|
||||
.await?
|
||||
.json::<ArtifactsWrap<GithubArtifact>>()
|
||||
.await?;
|
||||
let resp =
|
||||
Self::send_api_req::<ArtifactsWrap<GithubArtifact>>(self.get_github(url)).await?;
|
||||
|
||||
Ok(resp
|
||||
.artifacts
|
||||
|
@ -254,41 +410,351 @@ impl ArtifactApi {
|
|||
query.user, query.repo, query.artifact
|
||||
);
|
||||
|
||||
let artifact = Self::handle_github_error(self.get_github(url).send().await?)
|
||||
.await?
|
||||
.json::<GithubArtifact>()
|
||||
.await?;
|
||||
let artifact = Self::send_api_req::<GithubArtifact>(self.get_github(url)).await?;
|
||||
Ok(artifact.into_artifact(query.as_ref()))
|
||||
}
|
||||
|
||||
async fn handle_github_error(resp: Response) -> Result<Response> {
|
||||
async fn send_api_req_empty(req: RequestBuilder) -> Result<Response> {
|
||||
let resp = req.send().await?;
|
||||
if let Err(e) = resp.error_for_status_ref() {
|
||||
let status = resp.status();
|
||||
let msg = resp.json::<ApiError>().await.ok();
|
||||
Err(Error::HttpClient(
|
||||
msg.map(|msg| msg.message).unwrap_or(e.to_string()).into(),
|
||||
status,
|
||||
))
|
||||
let msg_str = msg.map(|msg| msg.message).unwrap_or(e.to_string()).into();
|
||||
tracing::error!("API error: {msg_str}");
|
||||
Err(Error::HttpClient(msg_str, status))
|
||||
} else {
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_github<U: IntoUrl>(&self, url: U) -> RequestBuilder {
|
||||
async fn send_api_req<T: DeserializeOwned>(req: RequestBuilder) -> Result<T> {
|
||||
Ok(Self::send_api_req_empty(req).await?.json().await?)
|
||||
}
|
||||
|
||||
fn get_github_any<U: IntoUrl>(&self, url: U) -> RequestBuilder {
|
||||
let mut builder = self.http.get(url);
|
||||
|
||||
if let Some(github_token) = &self.cfg.load().github_token {
|
||||
builder = builder.header(header::AUTHORIZATION, format!("Bearer {github_token}"));
|
||||
builder = builder.header(
|
||||
header::AUTHORIZATION,
|
||||
format!("Bearer {}", github_token.expose_secret()),
|
||||
);
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
fn get_github<U: IntoUrl>(&self, url: U) -> RequestBuilder {
|
||||
self.get_github_any(url)
|
||||
.header(header::ACCEPT, GITHUB_ACCEPT)
|
||||
}
|
||||
|
||||
/// Authorized GitHub request
|
||||
fn req_github<U: IntoUrl>(&self, method: Method, url: U) -> Result<RequestBuilder> {
|
||||
Ok(self
|
||||
.http
|
||||
.request(method, url)
|
||||
.header(header::ACCEPT, GITHUB_ACCEPT)
|
||||
.header(header::CONTENT_TYPE, GITHUB_ACCEPT)
|
||||
.header(
|
||||
header::AUTHORIZATION,
|
||||
format!(
|
||||
"Bearer {}",
|
||||
self.cfg
|
||||
.load()
|
||||
.github_token
|
||||
.as_ref()
|
||||
.map(ExposeSecret::expose_secret)
|
||||
.ok_or(Error::Other("GitHub token required".into()))?
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_forgejo<U: IntoUrl>(&self, url: U) -> RequestBuilder {
|
||||
self.http
|
||||
.get(url)
|
||||
.header(header::ACCEPT, mime::APPLICATION_JSON.essence_str())
|
||||
}
|
||||
|
||||
/// Authorized Forgejo request
|
||||
fn req_forgejo<U: IntoUrl>(&self, method: Method, url: U) -> Result<RequestBuilder> {
|
||||
let u = url.into_url()?;
|
||||
let host = u.host_str().ok_or(Error::InvalidUrl)?;
|
||||
let token = self
|
||||
.cfg
|
||||
.load()
|
||||
.forgejo_tokens
|
||||
.get(host)
|
||||
.ok_or_else(|| Error::Other(format!("Forgejo token for {host} required").into()))?
|
||||
.expose_secret();
|
||||
Ok(self
|
||||
.http
|
||||
.request(method, u)
|
||||
.header(header::ACCEPT, mime::APPLICATION_JSON.essence_str())
|
||||
.header(header::CONTENT_TYPE, mime::APPLICATION_JSON.essence_str())
|
||||
.header(header::AUTHORIZATION, format!("token {token}")))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "error", skip_all)]
|
||||
pub async fn workflow_run(&self, query: &RunQuery) -> Result<WorkflowRun> {
|
||||
if query.is_github() {
|
||||
self.workflow_run_github(query).await
|
||||
} else {
|
||||
self.workflow_run_forgejo(query).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn workflow_run_forgejo(&self, query: &RunQuery) -> Result<WorkflowRun> {
|
||||
// Since the workflow needs to be fetched with a POST request, we need a CSRF token
|
||||
let resp = self
|
||||
.http
|
||||
.get(format!("https://{}", query.host))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
let mut i_like_gitea = None;
|
||||
let mut csrf = None;
|
||||
for (k, v) in resp
|
||||
.headers()
|
||||
.get_all(header::SET_COOKIE)
|
||||
.into_iter()
|
||||
.filter_map(|v| v.to_str().ok())
|
||||
.filter_map(|v| v.split(';').next())
|
||||
.filter_map(|v| v.split_once('='))
|
||||
{
|
||||
match k {
|
||||
"i_like_gitea" => i_like_gitea = Some(v),
|
||||
"_csrf" => csrf = Some(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let i_like_gitea =
|
||||
i_like_gitea.ok_or(Error::Other("missing header: i_like_gitea".into()))?;
|
||||
let csrf = csrf.ok_or(Error::Other("missing header: _csrf".into()))?;
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.post(format!(
|
||||
"https://{}/{}/{}/actions/runs/{}/jobs/0",
|
||||
query.host, query.user, query.repo, query.run
|
||||
))
|
||||
.header(header::CONTENT_TYPE, mime::APPLICATION_JSON.essence_str())
|
||||
.header(header::COOKIE, format!("i_like_gitea={i_like_gitea}"))
|
||||
.header("x-csrf-token", csrf)
|
||||
.body(r#"{"logCursors":[{"step":0,"cursor":null,"expanded":true}]}"#)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
let run: WorkflowRun = resp.json::<ForgejoWorkflowRun>().await?.try_into()?;
|
||||
Ok(run)
|
||||
}
|
||||
|
||||
async fn workflow_run_github(&self, query: &RunQuery) -> Result<WorkflowRun> {
|
||||
let run = Self::send_api_req::<GitHubWorkflowRun>(self.get_github(format!(
|
||||
"https://api.github.com/repos/{}/{}/actions/runs/{}",
|
||||
query.user, query.repo, query.run
|
||||
)))
|
||||
.await?;
|
||||
Ok(run.into())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "error", skip_all)]
|
||||
pub async fn add_comment(
|
||||
&self,
|
||||
query: QueryRef<'_>,
|
||||
issue_id: u64,
|
||||
content: &str,
|
||||
old_comment_id: Option<u64>,
|
||||
recreate: bool,
|
||||
) -> Result<u64> {
|
||||
let body = format!("{COMMENT_TAG_PATTERN}\n{content}");
|
||||
if query.is_github() {
|
||||
self.add_comment_github(query, issue_id, &body, old_comment_id, recreate)
|
||||
.await
|
||||
} else {
|
||||
self.add_comment_forgejo(query, issue_id, &body, old_comment_id, recreate)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_comment_forgejo(
|
||||
&self,
|
||||
query: QueryRef<'_>,
|
||||
issue_id: u64,
|
||||
body: &str,
|
||||
old_comment_id: Option<u64>,
|
||||
recreate: bool,
|
||||
) -> Result<u64> {
|
||||
if let Some(old_comment_id) = old_comment_id {
|
||||
let url = format!(
|
||||
"https://{}/api/v1/repos/{}/{}/issues/comments/{}",
|
||||
query.host, query.user, query.repo, old_comment_id
|
||||
);
|
||||
if recreate {
|
||||
Self::send_api_req_empty(self.req_forgejo(Method::DELETE, url)?).await?;
|
||||
} else {
|
||||
Self::send_api_req_empty(
|
||||
self.req_forgejo(Method::PATCH, url)?
|
||||
.json(&CommentBody { body }),
|
||||
)
|
||||
.await?;
|
||||
return Ok(old_comment_id);
|
||||
}
|
||||
}
|
||||
|
||||
let new_c = Self::send_api_req::<IdEntity>(
|
||||
self.req_forgejo(
|
||||
Method::POST,
|
||||
format!(
|
||||
"https://{}/api/v1/repos/{}/{}/issues/{}/comments",
|
||||
query.host, query.user, query.repo, issue_id
|
||||
),
|
||||
)?
|
||||
.json(&CommentBody { body }),
|
||||
)
|
||||
.await?;
|
||||
Ok(new_c.id)
|
||||
}
|
||||
|
||||
async fn add_comment_github(
|
||||
&self,
|
||||
query: QueryRef<'_>,
|
||||
issue_id: u64,
|
||||
body: &str,
|
||||
old_comment_id: Option<u64>,
|
||||
recreate: bool,
|
||||
) -> Result<u64> {
|
||||
if let Some(old_comment_id) = old_comment_id {
|
||||
let url = format!(
|
||||
"https://api.github.com/repos/{}/{}/issues/comments/{}",
|
||||
query.user, query.repo, old_comment_id
|
||||
);
|
||||
if recreate {
|
||||
Self::send_api_req_empty(self.req_github(Method::DELETE, url)?).await?;
|
||||
} else {
|
||||
Self::send_api_req_empty(
|
||||
self.req_github(Method::PATCH, url)?
|
||||
.json(&CommentBody { body }),
|
||||
)
|
||||
.await?;
|
||||
return Ok(old_comment_id);
|
||||
}
|
||||
}
|
||||
|
||||
let new_c = Self::send_api_req::<IdEntity>(
|
||||
self.req_github(
|
||||
Method::POST,
|
||||
format!(
|
||||
"https://api.github.com/repos/{}/{}/issues/{}/comments",
|
||||
query.user, query.repo, issue_id
|
||||
),
|
||||
)?
|
||||
.json(&CommentBody { body }),
|
||||
)
|
||||
.await?;
|
||||
Ok(new_c.id)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "error", skip_all)]
|
||||
pub async fn find_comment(
|
||||
&self,
|
||||
query: QueryRef<'_>,
|
||||
issue_id: u64,
|
||||
) -> Result<Option<Comment>> {
|
||||
let user_id = self.get_user_id(query).await?;
|
||||
if query.is_github() {
|
||||
self.find_comment_github(query, issue_id, user_id).await
|
||||
} else {
|
||||
self.find_comment_forgejo(query, issue_id, user_id).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_comment_forgejo(
|
||||
&self,
|
||||
query: QueryRef<'_>,
|
||||
issue_id: u64,
|
||||
user_id: u64,
|
||||
) -> Result<Option<Comment>> {
|
||||
let comments = Self::send_api_req::<Vec<Comment>>(self.get_forgejo(format!(
|
||||
"https://{}/api/v1/repos/{}/{}/issues/{}/comments",
|
||||
query.host, query.user, query.repo, issue_id
|
||||
)))
|
||||
.await?;
|
||||
|
||||
Ok(comments
|
||||
.into_iter()
|
||||
.find(|c| c.user.id == user_id && c.body.starts_with(COMMENT_TAG_PATTERN)))
|
||||
}
|
||||
|
||||
async fn find_comment_github(
|
||||
&self,
|
||||
query: QueryRef<'_>,
|
||||
issue_id: u64,
|
||||
user_id: u64,
|
||||
) -> Result<Option<Comment>> {
|
||||
for page in 1..=5 {
|
||||
let comments = Self::send_api_req::<Vec<Comment>>(self.get_github(format!(
|
||||
"https://api.github.com/repos/{}/{}/issues/{}/comments?page={}",
|
||||
query.user, query.repo, issue_id, page
|
||||
)))
|
||||
.await?;
|
||||
if let Some(comment) = comments
|
||||
.into_iter()
|
||||
.find(|c| c.user.id == user_id && c.body.starts_with(COMMENT_TAG_PATTERN))
|
||||
{
|
||||
return Ok(Some(comment));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "error", skip_all)]
|
||||
pub async fn get_pr(&self, query: QueryRef<'_>, pr_id: u64) -> Result<PullRequest> {
|
||||
let req = if query.is_github() {
|
||||
self.get_github(format!(
|
||||
"https://api.github.com/repos/{}/{}/pulls/{}",
|
||||
query.user, query.repo, pr_id
|
||||
))
|
||||
} else {
|
||||
self.get_forgejo(format!(
|
||||
"https://{}/api/v1/repos/{}/{}/pulls/{}",
|
||||
query.host, query.user, query.repo, pr_id
|
||||
))
|
||||
};
|
||||
Self::send_api_req(req).await
|
||||
}
|
||||
|
||||
async fn get_user_id(&self, query: QueryRef<'_>) -> Result<u64> {
|
||||
self.user_ids
|
||||
.get_or_insert_async(query.host, async {
|
||||
let user =
|
||||
if query.is_github() {
|
||||
Self::send_api_req::<IdEntity>(
|
||||
self.req_github(Method::GET, "https://api.github.com/user")?,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
Self::send_api_req::<IdEntity>(self.req_forgejo(
|
||||
Method::GET,
|
||||
format!("https://{}/api/v1/user", query.host),
|
||||
)?)
|
||||
.await?
|
||||
};
|
||||
Ok::<_, Error>(user.id)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{config::Config, query::ArtifactQuery};
|
||||
use time::macros::datetime;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
query::{ArtifactQuery, RunQuery},
|
||||
};
|
||||
|
||||
use super::ArtifactApi;
|
||||
|
||||
|
@ -321,4 +787,31 @@ mod tests {
|
|||
assert_eq!(res.id, 1440556464);
|
||||
assert_eq!(res.size, 334);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn workflow_run_forgejo() {
|
||||
let query =
|
||||
RunQuery::from_forge_url("https://codeberg.org/forgejo/forgejo/actions/runs/20471")
|
||||
.unwrap();
|
||||
let api = ArtifactApi::new(Config::default());
|
||||
let res = api.workflow_run(&query).await.unwrap();
|
||||
assert_eq!(res.head_sha, "03581511024aca9b56bc6083565bdcebeacb9d05");
|
||||
assert!(res.from_pr);
|
||||
assert_eq!(res.date_started, Some(datetime!(2024-06-21 9:13:23 UTC)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn workflow_run_github() {
|
||||
let query =
|
||||
RunQuery::from_forge_url("https://github.com/orhun/git-cliff/actions/runs/9588266559")
|
||||
.unwrap();
|
||||
let api = ArtifactApi::new(Config::default());
|
||||
let res = api.workflow_run(&query).await.unwrap();
|
||||
dbg!(&res);
|
||||
assert_eq!(res.head_sha, "0500cb2c5c5ec225e109584236940ee068be2372");
|
||||
assert!(res.from_pr);
|
||||
assert_eq!(res.date_started, Some(datetime!(2024-06-21 9:13:23 UTC)));
|
||||
}
|
||||
}
|
||||
|
|
12
src/cache.rs
12
src/cache.rs
|
@ -166,10 +166,10 @@ impl Cache {
|
|||
let metadata = tokio::fs::metadata(&zip_path).await?;
|
||||
let modified = metadata
|
||||
.modified()
|
||||
.map_err(|_| Error::Internal("no file modified time".into()))?;
|
||||
.map_err(|_| Error::Other("no file modified time".into()))?;
|
||||
let accessed = metadata
|
||||
.accessed()
|
||||
.map_err(|_| Error::Internal("no file accessed time".into()))?;
|
||||
.map_err(|_| Error::Other("no file accessed time".into()))?;
|
||||
if modified != entry.last_modified {
|
||||
tracing::info!("cached file {zip_path:?} changed");
|
||||
entry = Arc::new(
|
||||
|
@ -182,7 +182,7 @@ impl Cache {
|
|||
let now = SystemTime::now();
|
||||
if now
|
||||
.duration_since(accessed)
|
||||
.map_err(|e| Error::Internal(e.to_string().into()))?
|
||||
.map_err(|e| Error::Other(e.to_string().into()))?
|
||||
> Duration::from_secs(1800)
|
||||
{
|
||||
let file = std::fs::File::open(&zip_path)?;
|
||||
|
@ -215,10 +215,10 @@ impl Cache {
|
|||
.metadata()
|
||||
.await?
|
||||
.accessed()
|
||||
.map_err(|_| Error::Internal("no file accessed time".into()))?;
|
||||
.map_err(|_| Error::Other("no file accessed time".into()))?;
|
||||
if now
|
||||
.duration_since(accessed)
|
||||
.map_err(|e| Error::Internal(e.to_string().into()))?
|
||||
.map_err(|e| Error::Other(e.to_string().into()))?
|
||||
> max_age
|
||||
{
|
||||
let path = entry.path();
|
||||
|
@ -289,7 +289,7 @@ impl CacheEntry {
|
|||
name,
|
||||
last_modified: meta
|
||||
.modified()
|
||||
.map_err(|_| Error::Internal("no file modified time".into()))?,
|
||||
.map_err(|_| Error::Other("no file modified time".into()))?,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -5,11 +5,12 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use secrecy::SecretString;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
query::{ArtifactQuery, QueryFilterList},
|
||||
query::{Query, QueryFilterList},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -48,7 +49,9 @@ pub struct ConfigData {
|
|||
/// GitHub API token for downloading GitHub artifacts
|
||||
///
|
||||
/// Using a fine-grained token with public read permissions is recommended.
|
||||
pub github_token: Option<String>,
|
||||
pub github_token: Option<SecretString>,
|
||||
/// Forgejo/Gitea API tokens by host
|
||||
pub forgejo_tokens: HashMap<String, SecretString>,
|
||||
/// Number of artifact indexes to keep in memory
|
||||
pub mem_cache_size: usize,
|
||||
/// Get the client IP address from a HTTP request header
|
||||
|
@ -61,6 +64,8 @@ pub struct ConfigData {
|
|||
pub real_ip_header: Option<String>,
|
||||
/// Limit the amount of downloaded artifacts per IP address and minute
|
||||
pub limit_artifacts_per_min: Option<NonZeroU32>,
|
||||
/// Limit the amount of PR comment API requests per IP address and minute
|
||||
pub limit_pr_comments_per_min: Option<NonZeroU32>,
|
||||
/// List of sites/users/repos that can NOT be accessed
|
||||
pub repo_blacklist: QueryFilterList,
|
||||
/// List of sites/users/repos that can ONLY be accessed
|
||||
|
@ -89,9 +94,11 @@ impl Default for ConfigData {
|
|||
max_age_h: NonZeroU32::new(12).unwrap(),
|
||||
zip_timeout_ms: Some(NonZeroU32::new(1000).unwrap()),
|
||||
github_token: None,
|
||||
forgejo_tokens: HashMap::new(),
|
||||
mem_cache_size: 50,
|
||||
real_ip_header: None,
|
||||
limit_artifacts_per_min: Some(NonZeroU32::new(5).unwrap()),
|
||||
limit_pr_comments_per_min: Some(NonZeroU32::new(5).unwrap()),
|
||||
repo_blacklist: QueryFilterList::default(),
|
||||
repo_whitelist: QueryFilterList::default(),
|
||||
suggested_sites: vec![
|
||||
|
@ -124,7 +131,7 @@ impl ConfigData {
|
|||
impl Config {
|
||||
pub fn new() -> Result<Self> {
|
||||
let data =
|
||||
envy::from_env::<ConfigData>().map_err(|e| Error::Internal(e.to_string().into()))?;
|
||||
envy::from_env::<ConfigData>().map_err(|e| Error::Other(e.to_string().into()))?;
|
||||
Self::from_data(data)
|
||||
}
|
||||
|
||||
|
@ -173,7 +180,7 @@ impl Config {
|
|||
.unwrap_or("codeberg.org")
|
||||
}
|
||||
|
||||
pub fn check_filterlist(&self, query: &ArtifactQuery) -> Result<()> {
|
||||
pub fn check_filterlist<Q: Query>(&self, query: &Q) -> Result<()> {
|
||||
if !self.i.data.repo_blacklist.passes(query, true) {
|
||||
Err(Error::Forbidden("repository is blacklisted".into()))
|
||||
} else if !self.i.data.repo_whitelist.passes(query, false) {
|
||||
|
|
|
@ -20,8 +20,8 @@ pub enum Error {
|
|||
Io(#[from] std::io::Error),
|
||||
#[error("Zip: {0}")]
|
||||
Zip(#[from] async_zip::error::ZipError),
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(Cow<'static, str>),
|
||||
#[error("Error: {0}")]
|
||||
Other(Cow<'static, str>),
|
||||
|
||||
#[error("Invalid request: {0}")]
|
||||
BadRequest(Cow<'static, str>),
|
||||
|
@ -58,13 +58,13 @@ impl From<reqwest::Error> for Error {
|
|||
|
||||
impl From<std::num::TryFromIntError> for Error {
|
||||
fn from(value: std::num::TryFromIntError) -> Self {
|
||||
Self::Internal(value.to_string().into())
|
||||
Self::Other(value.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(value: url::ParseError) -> Self {
|
||||
Self::Internal(value.to_string().into())
|
||||
Self::Other(value.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
18
src/query.rs
18
src/query.rs
|
@ -148,7 +148,11 @@ impl ArtifactQuery {
|
|||
}
|
||||
|
||||
impl RunQuery {
|
||||
pub fn from_forge_url(url: &str, aliases: &HashMap<String, String>) -> Result<Self> {
|
||||
pub fn from_forge_url(url: &str) -> Result<Self> {
|
||||
Self::from_forge_url_alias(url, &HashMap::new())
|
||||
}
|
||||
|
||||
pub fn from_forge_url_alias(url: &str, aliases: &HashMap<String, String>) -> Result<Self> {
|
||||
let (host, mut path_segs) = util::parse_url(url)?;
|
||||
|
||||
let user = path_segs
|
||||
|
@ -160,8 +164,8 @@ impl RunQuery {
|
|||
.ok_or(Error::BadRequest("no repository".into()))?
|
||||
.to_ascii_lowercase();
|
||||
|
||||
if !path_segs.next().is_some_and(|s| s == "actions")
|
||||
|| !path_segs.next().is_some_and(|s| s == "runs")
|
||||
if path_segs.next().is_none_or(|s| s != "actions")
|
||||
|| path_segs.next().is_none_or(|s| s != "runs")
|
||||
{
|
||||
return Err(Error::BadRequest("invalid Actions URL".into()));
|
||||
}
|
||||
|
@ -331,12 +335,12 @@ impl FromStr for QueryFilter {
|
|||
|
||||
if let Some(user) = &user {
|
||||
if !RE_REPO_NAME.is_match(user) {
|
||||
return Err(Error::Internal("invalid username".into()));
|
||||
return Err(Error::Other("invalid username".into()));
|
||||
}
|
||||
}
|
||||
if let Some(repo) = &repo {
|
||||
if !RE_REPO_NAME.is_match(repo) {
|
||||
return Err(Error::Internal("invalid repository name".into()));
|
||||
return Err(Error::Other("invalid repository name".into()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -370,7 +374,7 @@ impl FromStr for QueryFilterList {
|
|||
}
|
||||
|
||||
impl QueryFilterList {
|
||||
pub fn passes(&self, query: &ArtifactQuery, blacklist: bool) -> bool {
|
||||
pub fn passes<Q: Query>(&self, query: &Q, blacklist: bool) -> bool {
|
||||
if self.0.is_empty() {
|
||||
true
|
||||
} else {
|
||||
|
@ -398,7 +402,7 @@ impl<'de> Deserialize<'de> for QueryFilterList {
|
|||
{
|
||||
struct QueryFilterListVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for QueryFilterListVisitor {
|
||||
impl Visitor<'_> for QueryFilterListVisitor {
|
||||
type Value = QueryFilterList;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
|
|
11
src/snapshots/artifactview__app__tests__pr_comment_1.snap
Normal file
11
src/snapshots/artifactview__app__tests__pr_comment_1.snap
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: src/app.rs
|
||||
expression: res
|
||||
---
|
||||
### <a href="https://localhost:3000/?url=https://code.thetadev.de/thetadev/test-actions/actions/runs/1" target="_blank" rel="noopener noreferrer">Latest build artifacts</a>
|
||||
|
||||
Run [#1](https://code.thetadev.de/thetadev/test-actions/actions/runs/1) · [15eed48a83](https://code.thetadev.de/thetadev/test-actions/commit/15eed48a8382513147a949117ef4aa659989d397) · 15.06.2024 01:30:00 UTC
|
||||
|
||||
🏠 <a href="https://code-thetadev-de--thetadev--test-actions--1-1.localhost:3000" target="_blank" rel="noopener noreferrer">Hello World ;-)</a><br>
|
||||
<a href="https://code-thetadev-de--thetadev--test-actions--1-2.localhost:3000/junit.xml?viewer=1" target="_blank" rel="noopener noreferrer">Test</a><br>
|
||||
<!--NEXT_PREV - [#1](https://code.thetadev.de/thetadev/test-actions/actions/runs/1) [[15eed48a83](https://code.thetadev.de/thetadev/test-actions/commit/15eed48a8382513147a949117ef4aa659989d397)] <a href="https://localhost:3000/?url=https://code.thetadev.de/thetadev/test-actions/actions/runs/1" target="_blank" rel="noopener noreferrer">Artifacts</a>: <a href="https://code-thetadev-de--thetadev--test-actions--1-1.localhost:3000" target="_blank" rel="noopener noreferrer">`Hello`</a>, <a href="https://code-thetadev-de--thetadev--test-actions--1-2.localhost:3000/junit.xml?viewer=1" target="_blank" rel="noopener noreferrer">`Test`</a> (15.06.2024 01:30:00 UTC) -->
|
16
src/snapshots/artifactview__app__tests__pr_comment_2.snap
Normal file
16
src/snapshots/artifactview__app__tests__pr_comment_2.snap
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: src/app.rs
|
||||
expression: res
|
||||
---
|
||||
### <a href="https://localhost:3000/?url=https://code.thetadev.de/thetadev/test-actions/actions/runs/2" target="_blank" rel="noopener noreferrer">Latest build artifacts</a>
|
||||
|
||||
Run [#2](https://code.thetadev.de/thetadev/test-actions/actions/runs/2) · [25eed48a83](https://code.thetadev.de/thetadev/test-actions/commit/25eed48a8382513147a949117ef4aa659989d397) · 15.06.2024 02:30:00 UTC
|
||||
|
||||
🏠 <a href="https://code-thetadev-de--thetadev--test-actions--2-1.localhost:3000" target="_blank" rel="noopener noreferrer">Hello World ;-)</a><br>
|
||||
<a href="https://code-thetadev-de--thetadev--test-actions--2-2.localhost:3000/junit.xml?viewer=1" target="_blank" rel="noopener noreferrer">Test</a><br>
|
||||
<details>
|
||||
<summary>Previous builds</summary>
|
||||
|
||||
- [#1](https://code.thetadev.de/thetadev/test-actions/actions/runs/1) [[15eed48a83](https://code.thetadev.de/thetadev/test-actions/commit/15eed48a8382513147a949117ef4aa659989d397)] <a href="https://localhost:3000/?url=https://code.thetadev.de/thetadev/test-actions/actions/runs/1" target="_blank" rel="noopener noreferrer">Artifacts</a>: <a href="https://code-thetadev-de--thetadev--test-actions--1-1.localhost:3000" target="_blank" rel="noopener noreferrer">`Hello`</a>, <a href="https://code-thetadev-de--thetadev--test-actions--1-2.localhost:3000/junit.xml?viewer=1" target="_blank" rel="noopener noreferrer">`Test`</a> (15.06.2024 01:30:00 UTC)
|
||||
<!--NEXT_PREV - [#2](https://code.thetadev.de/thetadev/test-actions/actions/runs/2) [[25eed48a83](https://code.thetadev.de/thetadev/test-actions/commit/25eed48a8382513147a949117ef4aa659989d397)] <a href="https://localhost:3000/?url=https://code.thetadev.de/thetadev/test-actions/actions/runs/2" target="_blank" rel="noopener noreferrer">Artifacts</a>: <a href="https://code-thetadev-de--thetadev--test-actions--2-1.localhost:3000" target="_blank" rel="noopener noreferrer">`Hello`</a>, <a href="https://code-thetadev-de--thetadev--test-actions--2-2.localhost:3000/junit.xml?viewer=1" target="_blank" rel="noopener noreferrer">`Test`</a> (15.06.2024 02:30:00 UTC) -->
|
||||
</details>
|
17
src/snapshots/artifactview__app__tests__pr_comment_3.snap
Normal file
17
src/snapshots/artifactview__app__tests__pr_comment_3.snap
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
source: src/app.rs
|
||||
expression: res
|
||||
---
|
||||
### <a href="https://localhost:3000/?url=https://code.thetadev.de/thetadev/test-actions/actions/runs/3" target="_blank" rel="noopener noreferrer">Latest build artifacts</a>
|
||||
|
||||
Run [#3](https://code.thetadev.de/thetadev/test-actions/actions/runs/3) · [35eed48a83](https://code.thetadev.de/thetadev/test-actions/commit/35eed48a8382513147a949117ef4aa659989d397) · 15.06.2024 03:30:00 UTC
|
||||
|
||||
🏠 <a href="https://code-thetadev-de--thetadev--test-actions--3-1.localhost:3000" target="_blank" rel="noopener noreferrer">Hello World ;-)</a><br>
|
||||
<a href="https://code-thetadev-de--thetadev--test-actions--3-2.localhost:3000/junit.xml?viewer=1" target="_blank" rel="noopener noreferrer">Test</a><br>
|
||||
<details>
|
||||
<summary>Previous builds</summary>
|
||||
|
||||
- [#1](https://code.thetadev.de/thetadev/test-actions/actions/runs/1) [[15eed48a83](https://code.thetadev.de/thetadev/test-actions/commit/15eed48a8382513147a949117ef4aa659989d397)] <a href="https://localhost:3000/?url=https://code.thetadev.de/thetadev/test-actions/actions/runs/1" target="_blank" rel="noopener noreferrer">Artifacts</a>: <a href="https://code-thetadev-de--thetadev--test-actions--1-1.localhost:3000" target="_blank" rel="noopener noreferrer">`Hello`</a>, <a href="https://code-thetadev-de--thetadev--test-actions--1-2.localhost:3000/junit.xml?viewer=1" target="_blank" rel="noopener noreferrer">`Test`</a> (15.06.2024 01:30:00 UTC)
|
||||
- [#2](https://code.thetadev.de/thetadev/test-actions/actions/runs/2) [[25eed48a83](https://code.thetadev.de/thetadev/test-actions/commit/25eed48a8382513147a949117ef4aa659989d397)] <a href="https://localhost:3000/?url=https://code.thetadev.de/thetadev/test-actions/actions/runs/2" target="_blank" rel="noopener noreferrer">Artifacts</a>: <a href="https://code-thetadev-de--thetadev--test-actions--2-1.localhost:3000" target="_blank" rel="noopener noreferrer">`Hello`</a>, <a href="https://code-thetadev-de--thetadev--test-actions--2-2.localhost:3000/junit.xml?viewer=1" target="_blank" rel="noopener noreferrer">`Test`</a> (15.06.2024 02:30:00 UTC)
|
||||
<!--NEXT_PREV - [#3](https://code.thetadev.de/thetadev/test-actions/actions/runs/3) [[35eed48a83](https://code.thetadev.de/thetadev/test-actions/commit/35eed48a8382513147a949117ef4aa659989d397)] <a href="https://localhost:3000/?url=https://code.thetadev.de/thetadev/test-actions/actions/runs/3" target="_blank" rel="noopener noreferrer">Artifacts</a>: <a href="https://code-thetadev-de--thetadev--test-actions--3-1.localhost:3000" target="_blank" rel="noopener noreferrer">`Hello`</a>, <a href="https://code-thetadev-de--thetadev--test-actions--3-2.localhost:3000/junit.xml?viewer=1" target="_blank" rel="noopener noreferrer">`Test`</a> (15.06.2024 03:30:00 UTC) -->
|
||||
</details>
|
46
src/util.rs
46
src/util.rs
|
@ -194,7 +194,7 @@ pub fn get_ip_address(request: &Request, real_ip_header: Option<&str>) -> Result
|
|||
let socket_addr = request
|
||||
.extensions()
|
||||
.get::<ConnectInfo<SocketAddr>>()
|
||||
.ok_or(Error::Internal("could get request ip address".into()))?
|
||||
.ok_or(Error::Other("could get request ip address".into()))?
|
||||
.0;
|
||||
Ok(socket_addr.ip())
|
||||
}
|
||||
|
@ -263,6 +263,15 @@ pub struct ErrorJson {
|
|||
msg: String,
|
||||
}
|
||||
|
||||
impl ErrorJson {
|
||||
pub fn ok<S: Into<String>>(msg: S) -> Self {
|
||||
Self {
|
||||
status: 200,
|
||||
msg: msg.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for ErrorJson {
|
||||
fn from(value: Error) -> Self {
|
||||
Self {
|
||||
|
@ -280,10 +289,31 @@ impl From<http::Error> for ErrorJson {
|
|||
|
||||
impl IntoResponse for ErrorJson {
|
||||
fn into_response(self) -> Response {
|
||||
Response::builder().json(&self).unwrap()
|
||||
Response::builder().status(self.status).json(&self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_delim<'a>(s: &'a str, start: &str, end: &str) -> Option<&'a str> {
|
||||
if let Some(np) = s.find(start) {
|
||||
if let Some(np_end) = s[np + start.len()..].find(end) {
|
||||
return Some(s[np + start.len()..np + start.len() + np_end].trim());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn split_icon_prefix(s: &str) -> (&str, &str) {
|
||||
if let Some((i, c)) = s
|
||||
.char_indices()
|
||||
.find(|(_, c)| c.is_ascii() || c.is_alphanumeric())
|
||||
{
|
||||
if i > 0 && c == ' ' && s.get(i + 1..).is_some() {
|
||||
return (&s[..i + 1], &s[i + 1..]);
|
||||
}
|
||||
}
|
||||
("", s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -372,4 +402,16 @@ pub(crate) mod tests {
|
|||
let res = super::filename_ext(filename);
|
||||
assert_eq!(res, expect);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("🧪 Test", ("🧪 ", "Test"))]
|
||||
#[case("🧪👨👩👦 Test", ("🧪👨👩👦 ", "Test"))]
|
||||
#[case("🧪 👨👩👦 Test", ("🧪 ", "👨👩👦 Test"))]
|
||||
#[case("", ("", ""))]
|
||||
#[case("Test", ("", "Test"))]
|
||||
#[case("運命 Test", ("", "運命 Test"))]
|
||||
fn split_icon_prefix(#[case] s: &str, #[case] expect: (&str, &str)) {
|
||||
let res = super::split_icon_prefix(s);
|
||||
assert_eq!(res, expect);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,10 +22,10 @@
|
|||
<th><a href="?C=N&O=A">Name</a> <a
|
||||
href="?C=N&O=D"
|
||||
> ↓ </a></th>
|
||||
<th>CRC32</th>
|
||||
<th><a href="?C=S&O=A">Size</a> <a
|
||||
href="?C=S&O=D"
|
||||
> ↓ </a></th>
|
||||
<th>CRC32</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -45,8 +45,8 @@
|
|||
<span class="name">{{name}}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{#if is_dir}}—{{else}}{{size}}{{/if}}</td>
|
||||
<td>{{#if is_dir}}—{{else}}{{crc32}}{{/if}}</td>
|
||||
<td>{{#if is_dir}}—{{else}}{{size}}{{/if}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
|
|
320
tests/testfiles/giteaWorkflowRun.json
Normal file
320
tests/testfiles/giteaWorkflowRun.json
Normal file
|
@ -0,0 +1,320 @@
|
|||
{
|
||||
"state": {
|
||||
"run": {
|
||||
"link": "/ThetaDev/test-actions/actions/runs/92",
|
||||
"title": "Update README.md",
|
||||
"status": "success",
|
||||
"canCancel": false,
|
||||
"canApprove": false,
|
||||
"canRerun": true,
|
||||
"canDeleteArtifact": true,
|
||||
"done": true,
|
||||
"jobs": [
|
||||
{
|
||||
"id": 377,
|
||||
"name": "Test",
|
||||
"status": "success",
|
||||
"canRerun": true,
|
||||
"duration": "2s"
|
||||
}
|
||||
],
|
||||
"commit": {
|
||||
"localeCommit": "Commit",
|
||||
"localePushedBy": "pushed by",
|
||||
"localeWorkflow": "Workflow",
|
||||
"shortSHA": "6185409d45",
|
||||
"link": "/ThetaDev/test-actions/commit/6185409d457e0a7833ee122811b138a950273229",
|
||||
"pusher": { "displayName": "ThetaDev", "link": "/ThetaDev" },
|
||||
"branch": { "name": "#3", "link": "/ThetaDev/test-actions/pulls/3" }
|
||||
}
|
||||
},
|
||||
"currentJob": {
|
||||
"title": "Test",
|
||||
"detail": "Success",
|
||||
"steps": [
|
||||
{
|
||||
"summary": "Set up job",
|
||||
"duration": "1s",
|
||||
"status": "success"
|
||||
},
|
||||
{ "summary": "Test", "duration": "0s", "status": "success" },
|
||||
{
|
||||
"summary": "Comment PR",
|
||||
"duration": "1s",
|
||||
"status": "success"
|
||||
},
|
||||
{
|
||||
"summary": "Complete job",
|
||||
"duration": "0s",
|
||||
"status": "success"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"stepsLog": [
|
||||
{
|
||||
"step": 0,
|
||||
"cursor": 51,
|
||||
"lines": [
|
||||
{
|
||||
"index": 1,
|
||||
"message": "ocloud(version:v3.4.1) received task 431 of job 377, be triggered by event: pull_request",
|
||||
"timestamp": 1718902104.1911685
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"message": "workflow prepared",
|
||||
"timestamp": 1718902104.1916893
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"message": "evaluating expression 'success()'",
|
||||
"timestamp": 1718902104.1919434
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"message": "expression 'success()' evaluated to 'true'",
|
||||
"timestamp": 1718902104.1920443
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"message": "🚀 Start image=thetadev256/cimaster:latest",
|
||||
"timestamp": 1718902104.1920674
|
||||
},
|
||||
{
|
||||
"index": 6,
|
||||
"message": " 🐳 docker pull image=thetadev256/cimaster:latest platform= username= forcePull=true",
|
||||
"timestamp": 1718902104.203115
|
||||
},
|
||||
{
|
||||
"index": 7,
|
||||
"message": " 🐳 docker pull thetadev256/cimaster:latest",
|
||||
"timestamp": 1718902104.2031355
|
||||
},
|
||||
{
|
||||
"index": 8,
|
||||
"message": "pulling image 'docker.io/thetadev256/cimaster:latest' ()",
|
||||
"timestamp": 1718902104.2031558
|
||||
},
|
||||
{
|
||||
"index": 9,
|
||||
"message": "Pulling from thetadev256/cimaster :: latest",
|
||||
"timestamp": 1718902105.179988
|
||||
},
|
||||
{
|
||||
"index": 10,
|
||||
"message": "Digest: sha256:260659581e2900354877f31d5fec14db1c40999ad085a90a1a27c44b9cab8c48 :: ",
|
||||
"timestamp": 1718902105.1935806
|
||||
},
|
||||
{
|
||||
"index": 11,
|
||||
"message": "Status: Image is up to date for thetadev256/cimaster:latest :: ",
|
||||
"timestamp": 1718902105.1936345
|
||||
},
|
||||
{
|
||||
"index": 12,
|
||||
"message": "[{host 26303131f98bfa59ece2ca2dc1742040c8d125e22f1c07de603bd235bccf1b84 2024-04-02 20:48:57.065188715 +0000 UTC local host false {default map[] []} false false false {} false map[] map[] map[] [] map[]} {GITEA-ACTIONS-TASK-375_WORKFLOW-Build-and-push-cimaster-image_JOB-build-build-network 59ca72b83dbd990bda7af5e760fdb0e31010b855ca7e15df1a471fda8908aa47 2024-05-30 20:56:22.698163954 +0000 UTC local bridge false {default map[] [{172.24.0.0/16 172.24.0.1 map[]}]} false false false {} false map[] map[] map[] [] map[]} {none 627a84562dca9a81bd4a1fe570919035f1d608382d2b66b6b8559756d7aa2a6c 2024-04-02 20:48:57.050063369 +0000 UTC local null false {default map[] []} false false false {} false map[] map[] map[] [] map[]} {bridge 9b05952ef8d41f6a92bbc3c7f85c9fd5602e941b9e8e3cbcf84716de27d77aa9 2024-06-20 09:15:48.433095842 +0000 UTC local bridge false {default map[] [{172.17.0.0/16 172.17.0.1 map[]}]} false false false {} false map[] map[com.docker.network.bridge.default_bridge:true com.docker.network.bridge.enable_icc:true com.docker.network.bridge.enable_ip_masquerade:true com.docker.network.bridge.host_binding_ipv4:0.0.0.0 com.docker.network.bridge.name:docker0 com.docker.network.driver.mtu:1500] map[] [] map[]}]",
|
||||
"timestamp": 1718902105.203977
|
||||
},
|
||||
{
|
||||
"index": 13,
|
||||
"message": " 🐳 docker create image=thetadev256/cimaster:latest platform= entrypoint=[\"tail\" \"-f\" \"/dev/null\"] cmd=[] network=\"GITEA-ACTIONS-TASK-431_WORKFLOW-Rust-test_JOB-Test-Test-network\"",
|
||||
"timestamp": 1718902105.2669988
|
||||
},
|
||||
{
|
||||
"index": 14,
|
||||
"message": "Common container.Config ==\u003e \u0026{Hostname: Domainname: User: AttachStdin:false AttachStdout:false AttachStderr:false ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=ARM64 RUNNER_TEMP=/tmp LANG=C.UTF-8] Cmd:[] Healthcheck:\u003cnil\u003e ArgsEscaped:false Image:thetadev256/cimaster:latest Volumes:map[] WorkingDir:/workspace/ThetaDev/test-actions Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:\u003cnil\u003e Shell:[]}",
|
||||
"timestamp": 1718902105.2674763
|
||||
},
|
||||
{
|
||||
"index": 15,
|
||||
"message": "Common container.HostConfig ==\u003e \u0026{Binds:[] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-431_WORKFLOW-Rust-test_JOB-Test-Test-network PortBindings:map[] RestartPolicy:{Name: MaximumRetryCount:0} AutoRemove:true VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:\u003cnil\u003e OomKillDisable:\u003cnil\u003e PidsLimit:\u003cnil\u003e Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[{Type:volume Source:GITEA-ACTIONS-TASK-431_WORKFLOW-Rust-test_JOB-Test-env Target:/var/run/act ReadOnly:false Consistency: BindOptions:\u003cnil\u003e VolumeOptions:\u003cnil\u003e TmpfsOptions:\u003cnil\u003e ClusterOptions:\u003cnil\u003e} {Type:volume Source:GITEA-ACTIONS-TASK-431_WORKFLOW-Rust-test_JOB-Test Target:/workspace/ThetaDev/test-actions ReadOnly:false Consistency: BindOptions:\u003cnil\u003e VolumeOptions:\u003cnil\u003e TmpfsOptions:\u003cnil\u003e ClusterOptions:\u003cnil\u003e} {Type:volume Source:act-toolcache Target:/opt/hostedtoolcache ReadOnly:false Consistency: BindOptions:\u003cnil\u003e VolumeOptions:\u003cnil\u003e TmpfsOptions:\u003cnil\u003e ClusterOptions:\u003cnil\u003e}] MaskedPaths:[] ReadonlyPaths:[] Init:\u003cnil\u003e}",
|
||||
"timestamp": 1718902105.2731254
|
||||
},
|
||||
{
|
||||
"index": 16,
|
||||
"message": "input.NetworkAliases ==\u003e [Test]",
|
||||
"timestamp": 1718902105.2733588
|
||||
},
|
||||
{
|
||||
"index": 17,
|
||||
"message": "Created container name=GITEA-ACTIONS-TASK-431_WORKFLOW-Rust-test_JOB-Test id=953c4349622d86e68aa9b0d25b341f0ff4b36d87d5ffbae957d9efd72c3f6d64 from image thetadev256/cimaster:latest (platform: )",
|
||||
"timestamp": 1718902105.3252785
|
||||
},
|
||||
{
|
||||
"index": 18,
|
||||
"message": "ENV ==\u003e [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=ARM64 RUNNER_TEMP=/tmp LANG=C.UTF-8]",
|
||||
"timestamp": 1718902105.3253257
|
||||
},
|
||||
{
|
||||
"index": 19,
|
||||
"message": " 🐳 docker run image=thetadev256/cimaster:latest platform= entrypoint=[\"tail\" \"-f\" \"/dev/null\"] cmd=[] network=\"GITEA-ACTIONS-TASK-431_WORKFLOW-Rust-test_JOB-Test-Test-network\"",
|
||||
"timestamp": 1718902105.3253412
|
||||
},
|
||||
{
|
||||
"index": 20,
|
||||
"message": "Starting container: 953c4349622d86e68aa9b0d25b341f0ff4b36d87d5ffbae957d9efd72c3f6d64",
|
||||
"timestamp": 1718902105.3253546
|
||||
},
|
||||
{
|
||||
"index": 21,
|
||||
"message": "Started container: 953c4349622d86e68aa9b0d25b341f0ff4b36d87d5ffbae957d9efd72c3f6d64",
|
||||
"timestamp": 1718902105.5463858
|
||||
},
|
||||
{
|
||||
"index": 22,
|
||||
"message": " 🐳 docker exec cmd=[chown -R 1000:1000 /workspace/ThetaDev/test-actions] user=0 workdir=",
|
||||
"timestamp": 1718902105.6031785
|
||||
},
|
||||
{
|
||||
"index": 23,
|
||||
"message": "Exec command '[chown -R 1000:1000 /workspace/ThetaDev/test-actions]'",
|
||||
"timestamp": 1718902105.6032245
|
||||
},
|
||||
{
|
||||
"index": 24,
|
||||
"message": "Working directory '/workspace/ThetaDev/test-actions'",
|
||||
"timestamp": 1718902105.6032348
|
||||
},
|
||||
{
|
||||
"index": 25,
|
||||
"message": "Writing entry to tarball workflow/event.json len:8795",
|
||||
"timestamp": 1718902105.6331673
|
||||
},
|
||||
{
|
||||
"index": 26,
|
||||
"message": "Writing entry to tarball workflow/envs.txt len:0",
|
||||
"timestamp": 1718902105.6332214
|
||||
},
|
||||
{
|
||||
"index": 27,
|
||||
"message": "Extracting content to '/var/run/act/'",
|
||||
"timestamp": 1718902105.6332443
|
||||
},
|
||||
{
|
||||
"index": 28,
|
||||
"message": " ☁ git clone 'https://code.thetadev.de/actions/comment-pull-request' # ref=v1",
|
||||
"timestamp": 1718902105.6492677
|
||||
},
|
||||
{
|
||||
"index": 29,
|
||||
"message": " cloning https://code.thetadev.de/actions/comment-pull-request to /data/.cache/act/https---code.thetadev.de-actions-comment-pull-request@v1",
|
||||
"timestamp": 1718902105.6493008
|
||||
},
|
||||
{
|
||||
"index": 30,
|
||||
"message": "Cloned https://code.thetadev.de/actions/comment-pull-request to /data/.cache/act/https---code.thetadev.de-actions-comment-pull-request@v1",
|
||||
"timestamp": 1718902105.6817598
|
||||
},
|
||||
{
|
||||
"index": 31,
|
||||
"message": "Checked out v1",
|
||||
"timestamp": 1718902105.7090926
|
||||
},
|
||||
{
|
||||
"index": 32,
|
||||
"message": "Read action \u0026{Comment Pull Request Comments a pull request with the provided message map[GITHUB_TOKEN:{Github token of the repository (automatically created by Github) false ${{ github.token }}} comment_tag:{A tag on your comment that will be used to identify a comment in case of replacement. false } create_if_not_exists:{Whether a comment should be created even if comment_tag is not found. false true} filePath:{Path of the file that should be commented false } message:{Message that should be printed in the pull request false } pr_number:{Manual pull request number false } reactions:{You can set some reactions on your comments through the `reactions` input. false } recreate:{Delete and recreate the comment instead of updating it false false}] map[] {node20 map[] act/index.js always() always() [] []} {blue message-circle}} from 'Unknown'",
|
||||
"timestamp": 1718902105.709367
|
||||
},
|
||||
{
|
||||
"index": 33,
|
||||
"message": "setupEnv =\u003e map[ACT:true ACTIONS_CACHE_URL:http://192.168.96.3:44491/ ACTIONS_RESULTS_URL:https://code.thetadev.de ACTIONS_RUNTIME_TOKEN:*** ACTIONS_RUNTIME_URL:https://code.thetadev.de/api/actions_pipeline/ CI:true GITEA_ACTIONS:true GITEA_ACTIONS_RUNNER_VERSION:v3.4.1 GITHUB_ACTION:0 GITHUB_ACTIONS:true GITHUB_ACTION_PATH: GITHUB_ACTION_REF: GITHUB_ACTION_REPOSITORY: GITHUB_ACTOR:ThetaDev GITHUB_API_URL:https://code.thetadev.de/api/v1 GITHUB_BASE_REF:main GITHUB_EVENT_NAME:pull_request GITHUB_EVENT_PATH:/var/run/act/workflow/event.json GITHUB_GRAPHQL_URL: GITHUB_HEAD_REF:thetadev-patch-2 GITHUB_JOB:Test GITHUB_REF:refs/pull/3/head GITHUB_REF_NAME:3 GITHUB_REF_TYPE: GITHUB_REPOSITORY:ThetaDev/test-actions GITHUB_REPOSITORY_OWNER:ThetaDev GITHUB_RETENTION_DAYS: GITHUB_RUN_ID:292 GITHUB_RUN_NUMBER:92 GITHUB_SERVER_URL:https://code.thetadev.de GITHUB_SHA:6185409d457e0a7833ee122811b138a950273229 GITHUB_TOKEN:*** GITHUB_WORKFLOW:Rust test GITHUB_WORKSPACE:/workspace/ThetaDev/test-actions ImageOS:cimasterlatest JOB_CONTAINER_NAME:GITEA-ACTIONS-TASK-431_WORKFLOW-Rust-test_JOB-Test RUNNER_PERFLOG:/dev/null RUNNER_TRACKING_ID:]",
|
||||
"timestamp": 1718902105.7244232
|
||||
},
|
||||
{
|
||||
"index": 34,
|
||||
"message": "evaluating expression ''",
|
||||
"timestamp": 1718902105.7316134
|
||||
},
|
||||
{
|
||||
"index": 35,
|
||||
"message": "expression '' evaluated to 'true'",
|
||||
"timestamp": 1718902105.7316737
|
||||
},
|
||||
{
|
||||
"index": 36,
|
||||
"message": "⭐ Run Main Test",
|
||||
"timestamp": 1718902105.7316883
|
||||
},
|
||||
{
|
||||
"index": 37,
|
||||
"message": "Writing entry to tarball workflow/outputcmd.txt len:0",
|
||||
"timestamp": 1718902105.7317095
|
||||
},
|
||||
{
|
||||
"index": 38,
|
||||
"message": "Writing entry to tarball workflow/statecmd.txt len:0",
|
||||
"timestamp": 1718902105.7317333
|
||||
},
|
||||
{
|
||||
"index": 39,
|
||||
"message": "Writing entry to tarball workflow/pathcmd.txt len:0",
|
||||
"timestamp": 1718902105.7317476
|
||||
},
|
||||
{
|
||||
"index": 40,
|
||||
"message": "Writing entry to tarball workflow/envs.txt len:0",
|
||||
"timestamp": 1718902105.7317617
|
||||
},
|
||||
{
|
||||
"index": 41,
|
||||
"message": "Writing entry to tarball workflow/SUMMARY.md len:0",
|
||||
"timestamp": 1718902105.7317736
|
||||
},
|
||||
{
|
||||
"index": 42,
|
||||
"message": "Extracting content to '/var/run/act'",
|
||||
"timestamp": 1718902105.731786
|
||||
},
|
||||
{
|
||||
"index": 43,
|
||||
"message": "expression 'echo \"${{ secrets.FORGEJO_CI_TOKEN }}\"\\n' rewritten to 'format('echo \"{0}\"\\n', secrets.FORGEJO_CI_TOKEN)'",
|
||||
"timestamp": 1718902105.754891
|
||||
},
|
||||
{
|
||||
"index": 44,
|
||||
"message": "evaluating expression 'format('echo \"{0}\"\\n', secrets.FORGEJO_CI_TOKEN)'",
|
||||
"timestamp": 1718902105.7549253
|
||||
},
|
||||
{
|
||||
"index": 45,
|
||||
"message": "expression 'format('echo \"{0}\"\\n', secrets.FORGEJO_CI_TOKEN)' evaluated to '%!t(string=echo \"***\"\\n)'",
|
||||
"timestamp": 1718902105.7549586
|
||||
},
|
||||
{
|
||||
"index": 46,
|
||||
"message": "Wrote command \\n\\necho \"***\"\\n\\n\\n to 'workflow/0'",
|
||||
"timestamp": 1718902105.754978
|
||||
},
|
||||
{
|
||||
"index": 47,
|
||||
"message": "Writing entry to tarball workflow/0 len:50",
|
||||
"timestamp": 1718902105.755002
|
||||
},
|
||||
{
|
||||
"index": 48,
|
||||
"message": "Extracting content to '/var/run/act'",
|
||||
"timestamp": 1718902105.755024
|
||||
},
|
||||
{
|
||||
"index": 49,
|
||||
"message": " 🐳 docker exec cmd=[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/0] user= workdir=",
|
||||
"timestamp": 1718902105.7571557
|
||||
},
|
||||
{
|
||||
"index": 50,
|
||||
"message": "Exec command '[bash --noprofile --norc -e -o pipefail /var/run/act/workflow/0]'",
|
||||
"timestamp": 1718902105.7571852
|
||||
},
|
||||
{
|
||||
"index": 51,
|
||||
"message": "Working directory '/workspace/ThetaDev/test-actions'",
|
||||
"timestamp": 1718902105.7572272
|
||||
}
|
||||
],
|
||||
"started": 1718902104
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
220
tests/testfiles/githubWorkflowRun.json
Normal file
220
tests/testfiles/githubWorkflowRun.json
Normal file
|
@ -0,0 +1,220 @@
|
|||
{
|
||||
"id": 9598566319,
|
||||
"name": "db-tests",
|
||||
"node_id": "WFR_kwLOBFIx288AAAACPB5_rw",
|
||||
"head_branch": "fix-ui-tab",
|
||||
"head_sha": "7ae95457156ea964402747ae263d5a2a7de48883",
|
||||
"path": ".github/workflows/pull-db-tests.yml",
|
||||
"display_title": "WIP: Fix tab performance",
|
||||
"run_number": 20434,
|
||||
"event": "pull_request",
|
||||
"status": "completed",
|
||||
"conclusion": "success",
|
||||
"workflow_id": 56971384,
|
||||
"check_suite_id": 25125296548,
|
||||
"check_suite_node_id": "CS_kwDOBFIx288AAAAF2ZWZpA",
|
||||
"url": "https://api.github.com/repos/go-gitea/gitea/actions/runs/9598566319",
|
||||
"html_url": "https://github.com/go-gitea/gitea/actions/runs/9598566319",
|
||||
"pull_requests": [],
|
||||
"created_at": "2024-06-20T13:41:06Z",
|
||||
"updated_at": "2024-06-20T14:10:02Z",
|
||||
"actor": {
|
||||
"login": "wxiaoguang",
|
||||
"id": 2114189,
|
||||
"node_id": "MDQ6VXNlcjIxMTQxODk=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2114189?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/wxiaoguang",
|
||||
"html_url": "https://github.com/wxiaoguang",
|
||||
"followers_url": "https://api.github.com/users/wxiaoguang/followers",
|
||||
"following_url": "https://api.github.com/users/wxiaoguang/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/wxiaoguang/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/wxiaoguang/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/wxiaoguang/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/wxiaoguang/orgs",
|
||||
"repos_url": "https://api.github.com/users/wxiaoguang/repos",
|
||||
"events_url": "https://api.github.com/users/wxiaoguang/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/wxiaoguang/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"run_attempt": 1,
|
||||
"referenced_workflows": [
|
||||
{
|
||||
"path": "go-gitea/gitea/.github/workflows/files-changed.yml@d8d6749d313098583fc1d527ce8a4aafb81ca12d",
|
||||
"sha": "d8d6749d313098583fc1d527ce8a4aafb81ca12d",
|
||||
"ref": "refs/pull/31437/merge"
|
||||
}
|
||||
],
|
||||
"run_started_at": "2024-06-20T13:41:06Z",
|
||||
"triggering_actor": {
|
||||
"login": "wxiaoguang",
|
||||
"id": 2114189,
|
||||
"node_id": "MDQ6VXNlcjIxMTQxODk=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2114189?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/wxiaoguang",
|
||||
"html_url": "https://github.com/wxiaoguang",
|
||||
"followers_url": "https://api.github.com/users/wxiaoguang/followers",
|
||||
"following_url": "https://api.github.com/users/wxiaoguang/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/wxiaoguang/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/wxiaoguang/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/wxiaoguang/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/wxiaoguang/orgs",
|
||||
"repos_url": "https://api.github.com/users/wxiaoguang/repos",
|
||||
"events_url": "https://api.github.com/users/wxiaoguang/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/wxiaoguang/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"jobs_url": "https://api.github.com/repos/go-gitea/gitea/actions/runs/9598566319/jobs",
|
||||
"logs_url": "https://api.github.com/repos/go-gitea/gitea/actions/runs/9598566319/logs",
|
||||
"check_suite_url": "https://api.github.com/repos/go-gitea/gitea/check-suites/25125296548",
|
||||
"artifacts_url": "https://api.github.com/repos/go-gitea/gitea/actions/runs/9598566319/artifacts",
|
||||
"cancel_url": "https://api.github.com/repos/go-gitea/gitea/actions/runs/9598566319/cancel",
|
||||
"rerun_url": "https://api.github.com/repos/go-gitea/gitea/actions/runs/9598566319/rerun",
|
||||
"previous_attempt_url": null,
|
||||
"workflow_url": "https://api.github.com/repos/go-gitea/gitea/actions/workflows/56971384",
|
||||
"head_commit": {
|
||||
"id": "7ae95457156ea964402747ae263d5a2a7de48883",
|
||||
"tree_id": "edb45bf6711cdcff1ee0347e330a0bd5b89996ec",
|
||||
"message": "fix",
|
||||
"timestamp": "2024-06-20T13:40:55Z",
|
||||
"author": { "name": "wxiaoguang", "email": "wxiaoguang@gmail.com" },
|
||||
"committer": { "name": "wxiaoguang", "email": "wxiaoguang@gmail.com" }
|
||||
},
|
||||
"repository": {
|
||||
"id": 72495579,
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnk3MjQ5NTU3OQ==",
|
||||
"name": "gitea",
|
||||
"full_name": "go-gitea/gitea",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "go-gitea",
|
||||
"id": 12724356,
|
||||
"node_id": "MDEyOk9yZ2FuaXphdGlvbjEyNzI0MzU2",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12724356?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/go-gitea",
|
||||
"html_url": "https://github.com/go-gitea",
|
||||
"followers_url": "https://api.github.com/users/go-gitea/followers",
|
||||
"following_url": "https://api.github.com/users/go-gitea/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/go-gitea/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/go-gitea/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/go-gitea/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/go-gitea/orgs",
|
||||
"repos_url": "https://api.github.com/users/go-gitea/repos",
|
||||
"events_url": "https://api.github.com/users/go-gitea/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/go-gitea/received_events",
|
||||
"type": "Organization",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/go-gitea/gitea",
|
||||
"description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/go-gitea/gitea",
|
||||
"forks_url": "https://api.github.com/repos/go-gitea/gitea/forks",
|
||||
"keys_url": "https://api.github.com/repos/go-gitea/gitea/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/go-gitea/gitea/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/go-gitea/gitea/teams",
|
||||
"hooks_url": "https://api.github.com/repos/go-gitea/gitea/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/go-gitea/gitea/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/go-gitea/gitea/events",
|
||||
"assignees_url": "https://api.github.com/repos/go-gitea/gitea/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/go-gitea/gitea/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/go-gitea/gitea/tags",
|
||||
"blobs_url": "https://api.github.com/repos/go-gitea/gitea/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/go-gitea/gitea/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/go-gitea/gitea/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/go-gitea/gitea/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/go-gitea/gitea/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/go-gitea/gitea/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/go-gitea/gitea/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/go-gitea/gitea/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/go-gitea/gitea/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/go-gitea/gitea/subscription",
|
||||
"commits_url": "https://api.github.com/repos/go-gitea/gitea/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/go-gitea/gitea/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/go-gitea/gitea/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/go-gitea/gitea/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/go-gitea/gitea/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/go-gitea/gitea/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/go-gitea/gitea/merges",
|
||||
"archive_url": "https://api.github.com/repos/go-gitea/gitea/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/go-gitea/gitea/downloads",
|
||||
"issues_url": "https://api.github.com/repos/go-gitea/gitea/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/go-gitea/gitea/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/go-gitea/gitea/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/go-gitea/gitea/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/go-gitea/gitea/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/go-gitea/gitea/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/go-gitea/gitea/deployments"
|
||||
},
|
||||
"head_repository": {
|
||||
"id": 398521154,
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnkzOTg1MjExNTQ=",
|
||||
"name": "gitea",
|
||||
"full_name": "wxiaoguang/gitea",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "wxiaoguang",
|
||||
"id": 2114189,
|
||||
"node_id": "MDQ6VXNlcjIxMTQxODk=",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2114189?v=4",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/wxiaoguang",
|
||||
"html_url": "https://github.com/wxiaoguang",
|
||||
"followers_url": "https://api.github.com/users/wxiaoguang/followers",
|
||||
"following_url": "https://api.github.com/users/wxiaoguang/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/wxiaoguang/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/wxiaoguang/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/wxiaoguang/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/wxiaoguang/orgs",
|
||||
"repos_url": "https://api.github.com/users/wxiaoguang/repos",
|
||||
"events_url": "https://api.github.com/users/wxiaoguang/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/wxiaoguang/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/wxiaoguang/gitea",
|
||||
"description": "Git with a cup of tea, painless self-hosted git service",
|
||||
"fork": true,
|
||||
"url": "https://api.github.com/repos/wxiaoguang/gitea",
|
||||
"forks_url": "https://api.github.com/repos/wxiaoguang/gitea/forks",
|
||||
"keys_url": "https://api.github.com/repos/wxiaoguang/gitea/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/wxiaoguang/gitea/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/wxiaoguang/gitea/teams",
|
||||
"hooks_url": "https://api.github.com/repos/wxiaoguang/gitea/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/wxiaoguang/gitea/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/wxiaoguang/gitea/events",
|
||||
"assignees_url": "https://api.github.com/repos/wxiaoguang/gitea/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/wxiaoguang/gitea/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/wxiaoguang/gitea/tags",
|
||||
"blobs_url": "https://api.github.com/repos/wxiaoguang/gitea/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/wxiaoguang/gitea/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/wxiaoguang/gitea/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/wxiaoguang/gitea/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/wxiaoguang/gitea/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/wxiaoguang/gitea/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/wxiaoguang/gitea/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/wxiaoguang/gitea/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/wxiaoguang/gitea/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/wxiaoguang/gitea/subscription",
|
||||
"commits_url": "https://api.github.com/repos/wxiaoguang/gitea/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/wxiaoguang/gitea/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/wxiaoguang/gitea/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/wxiaoguang/gitea/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/wxiaoguang/gitea/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/wxiaoguang/gitea/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/wxiaoguang/gitea/merges",
|
||||
"archive_url": "https://api.github.com/repos/wxiaoguang/gitea/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/wxiaoguang/gitea/downloads",
|
||||
"issues_url": "https://api.github.com/repos/wxiaoguang/gitea/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/wxiaoguang/gitea/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/wxiaoguang/gitea/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/wxiaoguang/gitea/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/wxiaoguang/gitea/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/wxiaoguang/gitea/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/wxiaoguang/gitea/deployments"
|
||||
}
|
||||
}
|
|
@ -242,8 +242,8 @@ fn parse_listing(doc: &Html) -> Vec<FileEntry> {
|
|||
|
||||
FileEntry {
|
||||
name,
|
||||
size: parts.next().expect("size"),
|
||||
crc32: parts.next().expect("crc32"),
|
||||
size: parts.next().expect("size"),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
|
Loading…
Add table
Reference in a new issue