feat: add run inspection commands #5

Manually merged
day01 merged 1 commits from feat/0.6-actions-read into develop 2026-08-30 09:27:21 +00:00
4 changed files with 253 additions and 2 deletions
Showing only changes of commit 109c3743d2 - Show all commits
+6 -2
View File
@@ -1,10 +1,14 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
let protoc = protoc_bin_vendored::protoc_bin_path()?;
let mut config = tonic_prost_build::Config::new();
config.protoc_executable(protoc);
tonic_prost_build::configure()
.build_server(false)
.compile_with_config(config, &["proto/identity.proto"], &["proto"])?;
println!("cargo:rerun-if-changed=proto/identity.proto");
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let protoc = protoc_bin_vendored::protoc_bin_path()?;
let mut config = tonic_prost_build::Config::new();
config.protoc_executable(protoc);
tonic_prost_build::configure()
.build_server(false)
.compile_with_config(
config,
&["proto/identity.proto", "proto/actions.proto"],
&["proto"],
)?;
println!("cargo:rerun-if-changed=proto");
Ok(())
}
+39
View File
File diff suppressed because it is too large Load Diff
+55
View File
@@ -1,0 +1,55 @@
syntax = "proto3";

package syncode.control.v1;

service ActionsRead {
rpc ListRuns(ListRunsRequest) returns (ListRunsResponse);
rpc GetRun(GetRunRequest) returns (ActionRun);
rpc GetJobLogs(GetJobLogsRequest) returns (GetJobLogsResponse);
}

message ListRunsRequest {
string session_token = 1;
string owner = 2;
string repository = 3;
}

message GetRunRequest {
string session_token = 1;
string run_id = 2;
}

message GetJobLogsRequest {
string session_token = 1;
string run_id = 2;
string job_id = 3;
}

message ListRunsResponse {
repeated ActionRun runs = 1;
}

message ActionRun {
string id = 1;
uint64 number = 2;
string repository_id = 3;
string commit = 4;
string reference = 5;
string event = 6;
string workflow = 7;
string state = 8;
string conclusion = 9;
repeated ActionJob jobs = 10;
}

message ActionJob {
string id = 1;
string key = 2;
string state = 3;
string conclusion = 4;
string node_id = 5;
}

message GetJobLogsResponse {
repeated string lines = 1;
}
+153
View File
@@ -1,0 +1,153 @@
use tonic::transport::{Channel, ClientTlsConfig, Endpoint};

use crate::actions_wire::actions_read_client::ActionsReadClient;
use crate::actions_wire::{
ActionJob, ActionRun, GetJobLogsRequest, GetRunRequest, ListRunsRequest,
};
use crate::api::Repository;
use crate::error::{Error, Result};

async fn client(host: &str) -> Result<(ActionsReadClient<Channel>, String)> {
let (host_key, _) = crate::config::normalize_host(host)?;
let config = crate::config::load()?;
let configured = config
.hosts
.get(&host_key)
.ok_or_else(|| Error::Configuration(format!("not logged in to {host_key}")))?;
let channel = Endpoint::from_shared(configured.forge_url.clone())?
.tls_config(ClientTlsConfig::new().with_webpki_roots())?
.connect()
.await?;
Ok((
ActionsReadClient::new(channel),
configured.session_token.clone(),
))
}

pub async fn list(host: &str, repository: &str) -> Result<()> {
let repository = Repository::parse(repository)?;
let (mut client, session_token) = client(host).await?;
let response = client
.list_runs(ListRunsRequest {
session_token,
owner: repository.owner,
repository: repository.name,
})
.await
.map_err(control_error)?
.into_inner();
for run in response.runs {
println!(
"{}\t{}\t{}\t{}\t{}",
run.number,
run.id,
status(&run.state, &run.conclusion),
run.workflow,
short_commit(&run.commit)
);
}
Ok(())
}

pub async fn view(host: &str, run_id: &str) -> Result<()> {
let (mut client, session_token) = client(host).await?;
let run = get_run(&mut client, session_token, run_id).await?;
println!("run\t{}", run.id);
println!("number\t{}", run.number);
println!("status\t{}", status(&run.state, &run.conclusion));
println!("workflow\t{}", run.workflow);
println!("commit\t{}", run.commit);
println!("reference\t{}", run.reference);
println!("event\t{}", run.event);
for job in run.jobs {
print_job(&job);
}
Ok(())
}

pub async fn logs(host: &str, run_id: &str, job_id: Option<&str>) -> Result<()> {
let (mut client, session_token) = client(host).await?;
let job_id = match job_id {
Some(job_id) => job_id.to_owned(),
None => {
let run = get_run(&mut client, session_token.clone(), run_id).await?;
match run.jobs.as_slice() {
[job] => job.id.clone(),
[] => {
return Err(Error::Configuration("run has no jobs".to_owned()));
}
_ => {
return Err(Error::Configuration(
"run has multiple jobs; select one with --job <job-id>".to_owned(),
));
}
}
}
};
let response = client
.get_job_logs(GetJobLogsRequest {
session_token,
run_id: run_id.to_owned(),
job_id,
})
.await
.map_err(control_error)?
.into_inner();
for line in response.lines {
println!("{line}");
}
Ok(())
}

async fn get_run(
client: &mut ActionsReadClient<Channel>,
session_token: String,
run_id: &str,
) -> Result<ActionRun> {
client
.get_run(GetRunRequest {
session_token,
run_id: run_id.to_owned(),
})
.await
.map_err(control_error)
.map(tonic::Response::into_inner)
}

fn print_job(job: &ActionJob) {
println!(
"job\t{}\t{}\t{}",
job.id,
job.key,
status(&job.state, &job.conclusion)
);
}

fn status(state: &str, conclusion: &str) -> String {
if conclusion.is_empty() {
state.to_owned()
} else {
format!("{state}/{conclusion}")
}
}

fn short_commit(commit: &str) -> &str {
commit.get(..commit.len().min(12)).unwrap_or(commit)
}

fn control_error(error: tonic::Status) -> Error {
Error::Command(format!("control rejected the request: {error}"))
}

#[cfg(test)]
mod tests {
use super::{short_commit, status};

#[test]
fn formats_status_and_commit() {
assert_eq!("running", status("running", ""));
assert_eq!("finished/success", status("finished", "success"));
assert_eq!("0123456789ab", short_commit("0123456789abcdef"));
assert_eq!("abc", short_commit("abc"));
}
}