Compare commits
12 commits
dba33f86f3
...
9ce25467f1
Author | SHA1 | Date | |
---|---|---|---|
9ce25467f1 | |||
|
c16bc2fb84 | ||
|
60f3bb405f | ||
|
ea32909621 | ||
f0c7881d57 | |||
39f0019455 | |||
0516abb8fd | |||
3690b0244c | |||
bdc847f5d0 | |||
d8c3ab4f36 | |||
09e7c1d8bd | |||
1e36edf499 |
8 changed files with 75 additions and 74 deletions
|
@ -30,6 +30,9 @@ jobs:
|
||||||
- name: 🔗 Artifactview PR comment
|
- name: 🔗 Artifactview PR comment
|
||||||
if: ${{ always() && github.event_name == 'pull_request' }}
|
if: ${{ always() && github.event_name == 'pull_request' }}
|
||||||
run: |
|
run: |
|
||||||
echo "Run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_NUMBER"
|
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 }}"
|
echo "Pull: ${{ github.event.number }}"
|
||||||
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/$GITHUB_RUN_NUMBER\", \"pr\": ${{ github.event.number }}, \"artifact_titles\": {\"test\":\"🧪 Test report\"}, \"artifact_paths\": {\"test\":\"/junit.xml?viewer=1\"}}"
|
|
||||||
|
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"}}'
|
||||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -3,6 +3,27 @@
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
## [v0.4.1](https://codeberg.org/ThetaDev/artifactview/compare/v0.4.0..v0.4.1) - 2024-06-22
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|
44
Cargo.lock
generated
44
Cargo.lock
generated
|
@ -141,7 +141,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "artifactview"
|
name = "artifactview"
|
||||||
version = "0.4.1"
|
version = "0.4.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async_zip",
|
"async_zip",
|
||||||
"axum",
|
"axum",
|
||||||
|
@ -187,7 +187,6 @@ dependencies = [
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"unic-emoji-char",
|
|
||||||
"url",
|
"url",
|
||||||
"yarte",
|
"yarte",
|
||||||
"yarte_helpers",
|
"yarte_helpers",
|
||||||
|
@ -3145,47 +3144,6 @@ version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unic-char-property"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
|
|
||||||
dependencies = [
|
|
||||||
"unic-char-range",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unic-char-range"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unic-common"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unic-emoji-char"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d"
|
|
||||||
dependencies = [
|
|
||||||
"unic-char-property",
|
|
||||||
"unic-char-range",
|
|
||||||
"unic-ucd-version",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unic-ucd-version"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
|
|
||||||
dependencies = [
|
|
||||||
"unic-common",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "artifactview"
|
name = "artifactview"
|
||||||
version = "0.4.1"
|
version = "0.4.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["ThetaDev <thetadev@magenta.de>"]
|
authors = ["ThetaDev <thetadev@magenta.de>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -73,7 +73,6 @@ tokio-util = { version = "0.7.11", features = ["io"] }
|
||||||
tower-http = { version = "0.5.2", features = ["trace", "set-header"] }
|
tower-http = { version = "0.5.2", features = ["trace", "set-header"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = "0.3.18"
|
tracing-subscriber = "0.3.18"
|
||||||
unic-emoji-char = "0.9.0"
|
|
||||||
url = "2.5.0"
|
url = "2.5.0"
|
||||||
yarte = { version = "0.15.7", features = ["json"] }
|
yarte = { version = "0.15.7", features = ["json"] }
|
||||||
|
|
||||||
|
|
14
README.md
14
README.md
|
@ -1,5 +1,7 @@
|
||||||
# Artifactview
|
# Artifactview
|
||||||
|
|
||||||
|
**This is a test PR**: 4
|
||||||
|
|
||||||
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
|
Forgejo and GitHub's CI systems allow you to upload files and directories as
|
||||||
|
@ -82,7 +84,8 @@ artifacts).
|
||||||
- name: 🔗 Artifactview PR comment
|
- name: 🔗 Artifactview PR comment
|
||||||
if: ${{ always() && github.event_name == 'pull_request' }}
|
if: ${{ always() && github.event_name == 'pull_request' }}
|
||||||
run: |
|
run: |
|
||||||
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/$GITHUB_RUN_NUMBER\", \"pr\": ${{ github.event.number }}}"
|
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
|
## API
|
||||||
|
@ -254,11 +257,12 @@ Example list: `foo;bar`, example map: `foo=>f1;bar=>b1`
|
||||||
### Access tokens
|
### Access tokens
|
||||||
|
|
||||||
GitHub does not allow downloading artifacts for public repositories for unauthenticated
|
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. These are
|
users. So you need to setup an access token to use Artifactview with GitHub.
|
||||||
the permissions that need to be enabled:
|
|
||||||
|
|
||||||
- Repository access: All repositories
|
If you are not using the `prComment` feature, you can use a fine-grained access token
|
||||||
- Repository permissions: Pull requests (Read and write)
|
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
|
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,
|
you only need to create a token if you want to use the `prComment`-API. In this case,
|
||||||
|
|
18
src/app.rs
18
src/app.rs
|
@ -1,5 +1,4 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
net::{IpAddr, SocketAddr},
|
net::{IpAddr, SocketAddr},
|
||||||
|
@ -673,7 +672,7 @@ impl App {
|
||||||
.extract::<Json<PrCommentReq>, _>()
|
.extract::<Json<PrCommentReq>, _>()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::BadRequest(e.body_text().into()))?;
|
.map_err(|e| Error::BadRequest(e.body_text().into()))?;
|
||||||
let query = RunQuery::from_forge_url(&req.url)?;
|
let query = RunQuery::from_forge_url_alias(&req.url, &state.i.cfg.load().site_aliases)?;
|
||||||
|
|
||||||
if let Some(limiter) = &state.i.lim_pr_comment {
|
if let Some(limiter) = &state.i.lim_pr_comment {
|
||||||
limiter.check_key(&ip).map_err(Error::from)?;
|
limiter.check_key(&ip).map_err(Error::from)?;
|
||||||
|
@ -866,20 +865,7 @@ fn pr_comment_text(p: PrCommentTextParams) -> String {
|
||||||
};
|
};
|
||||||
|
|
||||||
let write_link_icon = |s: &mut String, title: &str, href: &str| {
|
let write_link_icon = |s: &mut String, title: &str, href: &str| {
|
||||||
// Move leading emoji into a prefix variable since including them in the link does not look good
|
let (title_pfx, title) = util::split_icon_prefix(title);
|
||||||
let mut title_pfx = String::new();
|
|
||||||
let mut title = Cow::Borrowed(title);
|
|
||||||
if let Some((i, c)) = title
|
|
||||||
.char_indices()
|
|
||||||
// Some emoji use variation selectors that are not included in is_emoji
|
|
||||||
.find(|(_, c)| !unic_emoji_char::is_emoji(*c) && !('\u{fe00}'..='\u{fe0f}').contains(c))
|
|
||||||
{
|
|
||||||
if i > 0 && c == ' ' {
|
|
||||||
title[..i + 1].clone_into(&mut title_pfx);
|
|
||||||
title = title[i + 1..].to_owned().into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = write!(
|
_ = write!(
|
||||||
s,
|
s,
|
||||||
r#"{title_pfx}<a href="{href}" target="_blank" rel="noopener noreferrer">{title}</a>"#,
|
r#"{title_pfx}<a href="{href}" target="_blank" rel="noopener noreferrer">{title}</a>"#,
|
||||||
|
|
|
@ -269,6 +269,7 @@ impl ArtifactApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "error", skip_all)]
|
||||||
pub async fn list(&self, query: &RunQuery, cached: bool) -> Result<Vec<Artifact>> {
|
pub async fn list(&self, query: &RunQuery, cached: bool) -> Result<Vec<Artifact>> {
|
||||||
let cache_key = query.cache_key();
|
let cache_key = query.cache_key();
|
||||||
let fut = async {
|
let fut = async {
|
||||||
|
@ -290,6 +291,7 @@ impl ArtifactApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "error", skip_all)]
|
||||||
pub async fn fetch(&self, query: &ArtifactQuery) -> Result<Artifact> {
|
pub async fn fetch(&self, query: &ArtifactQuery) -> Result<Artifact> {
|
||||||
if query.is_github() {
|
if query.is_github() {
|
||||||
self.fetch_github(query).await
|
self.fetch_github(query).await
|
||||||
|
@ -305,6 +307,7 @@ impl ArtifactApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "error", skip_all)]
|
||||||
pub async fn download(&self, artifact: &Artifact, path: &Path) -> Result<()> {
|
pub async fn download(&self, artifact: &Artifact, path: &Path) -> Result<()> {
|
||||||
if artifact.expired {
|
if artifact.expired {
|
||||||
return Err(Error::Expired);
|
return Err(Error::Expired);
|
||||||
|
@ -416,10 +419,9 @@ impl ArtifactApi {
|
||||||
if let Err(e) = resp.error_for_status_ref() {
|
if let Err(e) = resp.error_for_status_ref() {
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
let msg = resp.json::<ApiError>().await.ok();
|
let msg = resp.json::<ApiError>().await.ok();
|
||||||
Err(Error::HttpClient(
|
let msg_str = msg.map(|msg| msg.message).unwrap_or(e.to_string()).into();
|
||||||
msg.map(|msg| msg.message).unwrap_or(e.to_string()).into(),
|
tracing::error!("API error: {msg_str}");
|
||||||
status,
|
Err(Error::HttpClient(msg_str, status))
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
@ -492,6 +494,7 @@ impl ArtifactApi {
|
||||||
.header(header::AUTHORIZATION, format!("token {token}")))
|
.header(header::AUTHORIZATION, format!("token {token}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "error", skip_all)]
|
||||||
pub async fn workflow_run(&self, query: &RunQuery) -> Result<WorkflowRun> {
|
pub async fn workflow_run(&self, query: &RunQuery) -> Result<WorkflowRun> {
|
||||||
if query.is_github() {
|
if query.is_github() {
|
||||||
self.workflow_run_github(query).await
|
self.workflow_run_github(query).await
|
||||||
|
@ -554,6 +557,7 @@ impl ArtifactApi {
|
||||||
Ok(run.into())
|
Ok(run.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "error", skip_all)]
|
||||||
pub async fn add_comment(
|
pub async fn add_comment(
|
||||||
&self,
|
&self,
|
||||||
query: QueryRef<'_>,
|
query: QueryRef<'_>,
|
||||||
|
@ -621,8 +625,8 @@ impl ArtifactApi {
|
||||||
) -> Result<u64> {
|
) -> Result<u64> {
|
||||||
if let Some(old_comment_id) = old_comment_id {
|
if let Some(old_comment_id) = old_comment_id {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"https://api.github.com/repos/{}/{}/issues/{}/comments/{}",
|
"https://api.github.com/repos/{}/{}/issues/comments/{}",
|
||||||
query.user, query.repo, issue_id, old_comment_id
|
query.user, query.repo, old_comment_id
|
||||||
);
|
);
|
||||||
if recreate {
|
if recreate {
|
||||||
Self::send_api_req_empty(self.req_github(Method::DELETE, url)?).await?;
|
Self::send_api_req_empty(self.req_github(Method::DELETE, url)?).await?;
|
||||||
|
@ -650,6 +654,7 @@ impl ArtifactApi {
|
||||||
Ok(new_c.id)
|
Ok(new_c.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "error", skip_all)]
|
||||||
pub async fn find_comment(
|
pub async fn find_comment(
|
||||||
&self,
|
&self,
|
||||||
query: QueryRef<'_>,
|
query: QueryRef<'_>,
|
||||||
|
@ -702,6 +707,7 @@ impl ArtifactApi {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "error", skip_all)]
|
||||||
pub async fn get_pr(&self, query: QueryRef<'_>, pr_id: u64) -> Result<PullRequest> {
|
pub async fn get_pr(&self, query: QueryRef<'_>, pr_id: u64) -> Result<PullRequest> {
|
||||||
let req = if query.is_github() {
|
let req = if query.is_github() {
|
||||||
self.get_github(format!(
|
self.get_github(format!(
|
||||||
|
|
24
src/util.rs
24
src/util.rs
|
@ -302,6 +302,18 @@ pub fn extract_delim<'a>(s: &'a str, start: &str, end: &str) -> Option<&'a str>
|
||||||
None
|
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)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -390,4 +402,16 @@ pub(crate) mod tests {
|
||||||
let res = super::filename_ext(filename);
|
let res = super::filename_ext(filename);
|
||||||
assert_eq!(res, expect);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue