Add first-class Actions CLI support
This commit is contained in:
213
crates/gitea/src/actions.rs
Normal file
213
crates/gitea/src/actions.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use reqwest::header::ACCEPT;
|
||||
|
||||
use crate::{Client, Error, Method, RepositoryId, Result, models, positive};
|
||||
use gitea_openapi::apis;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ActionRunDetails {
|
||||
pub run: models::ActionWorkflowRun,
|
||||
pub jobs: Vec<models::ActionWorkflowJob>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ActionJobLog {
|
||||
pub job: models::ActionWorkflowJob,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub async fn action_workflows(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
) -> Result<Vec<models::ActionWorkflow>> {
|
||||
apis::repository_api::actions_list_repository_workflows(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
)
|
||||
.await
|
||||
.map(|response| response.workflows.unwrap_or_default())
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
|
||||
pub async fn dispatch_action_workflow(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
workflow: &str,
|
||||
reference: &str,
|
||||
) -> Result<()> {
|
||||
if workflow.is_empty() || reference.is_empty() {
|
||||
return Err(Error::InvalidInput(
|
||||
"workflow and reference must not be empty".into(),
|
||||
));
|
||||
}
|
||||
apis::repository_api::actions_dispatch_workflow(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
workflow,
|
||||
Some(models::CreateActionWorkflowDispatch::new(reference.into())),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
|
||||
pub async fn action_runs(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
) -> Result<Vec<models::ActionWorkflowRun>> {
|
||||
apis::repository_api::get_workflow_runs(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
Some(100),
|
||||
)
|
||||
.await
|
||||
.map(|response| response.workflow_runs.unwrap_or_default())
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
|
||||
pub async fn action_run_details(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
run: i64,
|
||||
) -> Result<ActionRunDetails> {
|
||||
positive(run, "run ID")?;
|
||||
let run_id = action_run_id(run)?;
|
||||
let (run, jobs) = tokio::try_join!(
|
||||
async {
|
||||
apis::repository_api::get_workflow_run(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
&run.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)
|
||||
},
|
||||
async {
|
||||
apis::repository_api::list_workflow_run_jobs(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
run_id,
|
||||
None,
|
||||
Some(1),
|
||||
Some(100),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)
|
||||
}
|
||||
)?;
|
||||
Ok(ActionRunDetails {
|
||||
run,
|
||||
jobs: jobs.jobs.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn action_run_logs(
|
||||
&self,
|
||||
repository: &RepositoryId,
|
||||
run: i64,
|
||||
) -> Result<Vec<ActionJobLog>> {
|
||||
let run = action_run_id(run)?;
|
||||
let jobs = apis::repository_api::list_workflow_run_jobs(
|
||||
&self.configuration(),
|
||||
&repository.owner,
|
||||
&repository.repository,
|
||||
run,
|
||||
None,
|
||||
Some(1),
|
||||
Some(100),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::generated)?
|
||||
.jobs
|
||||
.unwrap_or_default();
|
||||
let mut logs = Vec::with_capacity(jobs.len());
|
||||
for job in jobs {
|
||||
let id = job
|
||||
.id
|
||||
.ok_or_else(|| Error::Generated("Action job has no ID.".into()))?;
|
||||
let endpoint = format!(
|
||||
"repos/{}/{}/actions/jobs/{id}/logs",
|
||||
apis::urlencode(&repository.owner),
|
||||
apis::urlencode(&repository.repository),
|
||||
);
|
||||
let text = self
|
||||
.execute(
|
||||
self.request(Method::GET, &endpoint)?
|
||||
.header(ACCEPT, "text/plain"),
|
||||
)
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
logs.push(ActionJobLog { job, text });
|
||||
}
|
||||
Ok(logs)
|
||||
}
|
||||
}
|
||||
|
||||
fn action_run_id(run: i64) -> Result<i32> {
|
||||
positive(run, "run ID")?;
|
||||
i32::try_from(run).map_err(|_| Error::InvalidInput("run ID is too large".into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::TcpListener,
|
||||
thread,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::Provider;
|
||||
|
||||
#[tokio::test]
|
||||
async fn gets_every_job_log_for_a_run() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let server = thread::spawn(move || {
|
||||
let mut requests = Vec::new();
|
||||
for (content_type, body) in [
|
||||
(
|
||||
"application/json",
|
||||
r#"{"total_count":1,"jobs":[{"id":139,"name":"audit"}]}"#,
|
||||
),
|
||||
("text/plain", "scanner output\nexit code 1\n"),
|
||||
] {
|
||||
let (mut stream, _) = listener.accept().unwrap();
|
||||
let mut buffer = [0; 4096];
|
||||
let length = stream.read(&mut buffer).unwrap();
|
||||
requests.push(String::from_utf8_lossy(&buffer[..length]).into_owned());
|
||||
write!(
|
||||
stream,
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
|
||||
body.len()
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
requests
|
||||
});
|
||||
|
||||
let client =
|
||||
Client::with_provider(&format!("http://{address}"), None, Provider::Gitea).unwrap();
|
||||
let repository = RepositoryId::new("hugo", "Gotcha").unwrap();
|
||||
let logs = client.action_run_logs(&repository, 95).await.unwrap();
|
||||
|
||||
assert_eq!(logs.len(), 1);
|
||||
assert_eq!(logs[0].job.name.as_deref(), Some("audit"));
|
||||
assert_eq!(logs[0].text, "scanner output\nexit code 1\n");
|
||||
let requests = server.join().unwrap();
|
||||
assert!(requests[0].starts_with("GET /api/v1/repos/hugo/Gotcha/actions/runs/95/jobs?"));
|
||||
assert!(requests[1].starts_with("GET /api/v1/repos/hugo/Gotcha/actions/jobs/139/logs "));
|
||||
assert!(requests[1].contains("accept: text/plain"));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use gitea_openapi::apis;
|
||||
pub use gitea_openapi::models;
|
||||
pub use models::{Repository, ServerVersion, User};
|
||||
|
||||
mod actions;
|
||||
pub mod activity;
|
||||
mod config;
|
||||
pub mod diff;
|
||||
@@ -20,6 +21,7 @@ mod milestones;
|
||||
mod pulls;
|
||||
mod repositories;
|
||||
|
||||
pub use actions::{ActionJobLog, ActionRunDetails};
|
||||
pub use activity::ActivityFilter;
|
||||
pub use config::{Config, Selection, ServerProfile, TuiPreferences, server_url};
|
||||
pub use domain::{
|
||||
|
||||
Reference in New Issue
Block a user