feat: show CI output in Forgejo UI via PR comments and status descriptions
- Add target_url to StatusUpdate for linking commit statuses to PR comments - StatusReporter can now find open PRs, post markdown comments with job output, and re-post pipeline status with target_url pointing to the comment - On job failure, commit status description includes last ~10 output lines - Pipeline completion triggers a PR comment with <details> per job showing up to 100 lines of output - Add ureq json feature for Forgejo API response parsing Authored by Claude, lovingly guided by Zachery Aaron Shores-Chmielewski
This commit is contained in:
parent
d75076e5ff
commit
825a8e658f
7 changed files with 346 additions and 4 deletions
|
|
@ -11,7 +11,7 @@ serde_json = "1"
|
||||||
|
|
||||||
# Local runner library dependencies
|
# Local runner library dependencies
|
||||||
tiny_http = { version = "0.12", optional = true }
|
tiny_http = { version = "0.12", optional = true }
|
||||||
ureq = { version = "2", optional = true }
|
ureq = { version = "2", features = ["json"], optional = true }
|
||||||
hmac = { version = "0.12", optional = true }
|
hmac = { version = "0.12", optional = true }
|
||||||
sha2 = { version = "0.10", optional = true }
|
sha2 = { version = "0.10", optional = true }
|
||||||
hex = { version = "0.4", optional = true }
|
hex = { version = "0.4", optional = true }
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ impl Coordinator {
|
||||||
state: "pending".into(),
|
state: "pending".into(),
|
||||||
context: format!("ci/{pipeline_name}"),
|
context: format!("ci/{pipeline_name}"),
|
||||||
description: format!("Pipeline '{pipeline_name}' is pending"),
|
description: format!("Pipeline '{pipeline_name}' is pending"),
|
||||||
|
target_url: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
self.pipelines.insert(pipeline_id, pipeline);
|
self.pipelines.insert(pipeline_id, pipeline);
|
||||||
|
|
@ -148,6 +149,7 @@ impl Coordinator {
|
||||||
pipeline.pipeline_name,
|
pipeline.pipeline_name,
|
||||||
pipeline.status.forgejo_state()
|
pipeline.status.forgejo_state()
|
||||||
),
|
),
|
||||||
|
target_url: None,
|
||||||
};
|
};
|
||||||
self.emit_status_update(update);
|
self.emit_status_update(update);
|
||||||
return;
|
return;
|
||||||
|
|
@ -209,6 +211,7 @@ impl Coordinator {
|
||||||
state: "pending".into(),
|
state: "pending".into(),
|
||||||
context: format!("ci/{job_name}"),
|
context: format!("ci/{job_name}"),
|
||||||
description: format!("Job '{job_name}' is provisioning"),
|
description: format!("Job '{job_name}' is provisioning"),
|
||||||
|
target_url: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,6 +335,7 @@ impl Coordinator {
|
||||||
},
|
},
|
||||||
context: format!("ci/{job_name}"),
|
context: format!("ci/{job_name}"),
|
||||||
description: format!("Job '{job_name}' completed"),
|
description: format!("Job '{job_name}' completed"),
|
||||||
|
target_url: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(pipeline) = self.pipelines.get_mut(&pipeline_id) {
|
if let Some(pipeline) = self.pipelines.get_mut(&pipeline_id) {
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,7 @@ pub struct StatusUpdate {
|
||||||
pub state: String,
|
pub state: String,
|
||||||
pub context: String,
|
pub context: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
pub target_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Coordinator Config ─────────────────────────────────────────────────────
|
// ─── Coordinator Config ─────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use std::sync::{Arc, Mutex};
|
||||||
use swactor::actor::{ActorAddress, ActorInterface, Ctx};
|
use swactor::actor::{ActorAddress, ActorInterface, Ctx};
|
||||||
|
|
||||||
use crate::pipeline::PipelineExecution;
|
use crate::pipeline::PipelineExecution;
|
||||||
use crate::status_reporter::StatusReporterMsg;
|
use crate::status_reporter::{JobOutput, StatusReporterMsg};
|
||||||
use crate::yaml::{self, CiYaml};
|
use crate::yaml::{self, CiYaml};
|
||||||
use crate::{
|
use crate::{
|
||||||
JobComplete, JobId, JobProgress, JobStatus, LocalCiConfig, LocalStartJob, PipelineId,
|
JobComplete, JobId, JobProgress, JobStatus, LocalCiConfig, LocalStartJob, PipelineId,
|
||||||
|
|
@ -147,6 +147,7 @@ impl LocalCoordinator {
|
||||||
state: "pending".into(),
|
state: "pending".into(),
|
||||||
context: format!("ci/{pipeline_name}"),
|
context: format!("ci/{pipeline_name}"),
|
||||||
description: format!("Pipeline '{pipeline_name}' is pending"),
|
description: format!("Pipeline '{pipeline_name}' is pending"),
|
||||||
|
target_url: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -226,8 +227,10 @@ impl LocalCoordinator {
|
||||||
pipeline.pipeline_name,
|
pipeline.pipeline_name,
|
||||||
pipeline.status.forgejo_state()
|
pipeline.status.forgejo_state()
|
||||||
),
|
),
|
||||||
|
target_url: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
self.emit_pipeline_comment(ctx, &pipeline);
|
||||||
self.archive_pipeline(pipeline);
|
self.archive_pipeline(pipeline);
|
||||||
self.active_pipeline = None;
|
self.active_pipeline = None;
|
||||||
// Recurse to pick next from queue.
|
// Recurse to pick next from queue.
|
||||||
|
|
@ -307,6 +310,7 @@ impl LocalCoordinator {
|
||||||
state: "pending".into(),
|
state: "pending".into(),
|
||||||
context: format!("ci/{job_name}"),
|
context: format!("ci/{job_name}"),
|
||||||
description: format!("Job '{job_name}' is running"),
|
description: format!("Job '{job_name}' is running"),
|
||||||
|
target_url: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -383,6 +387,41 @@ impl LocalCoordinator {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Build a description with output tail for failures.
|
||||||
|
let description = match &status {
|
||||||
|
JobStatus::Passed => format!("Job '{job_name}' passed"),
|
||||||
|
JobStatus::Failed { reason } => {
|
||||||
|
let output_tail = self
|
||||||
|
.pipelines
|
||||||
|
.get(&pipeline_id)
|
||||||
|
.and_then(|p| p.jobs.get(&job_name))
|
||||||
|
.map(|j| {
|
||||||
|
let lines: Vec<&str> = j
|
||||||
|
.output_lines
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.take(10)
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect();
|
||||||
|
lines.into_iter().rev().collect::<Vec<_>>().join("\n")
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut desc = format!("Job '{job_name}' failed: {reason}");
|
||||||
|
if !output_tail.is_empty() {
|
||||||
|
desc.push_str("\n");
|
||||||
|
desc.push_str(&output_tail);
|
||||||
|
}
|
||||||
|
// Cap at ~250 chars for the status description field.
|
||||||
|
if desc.len() > 250 {
|
||||||
|
desc.truncate(247);
|
||||||
|
desc.push_str("...");
|
||||||
|
}
|
||||||
|
desc
|
||||||
|
}
|
||||||
|
_ => format!("Job '{job_name}' completed"),
|
||||||
|
};
|
||||||
|
|
||||||
// Emit per-job final status.
|
// Emit per-job final status.
|
||||||
if let Some(pipeline) = self.pipelines.get(&pipeline_id) {
|
if let Some(pipeline) = self.pipelines.get(&pipeline_id) {
|
||||||
self.emit_status(
|
self.emit_status(
|
||||||
|
|
@ -396,7 +435,8 @@ impl LocalCoordinator {
|
||||||
_ => "failure".into(),
|
_ => "failure".into(),
|
||||||
},
|
},
|
||||||
context: format!("ci/{job_name}"),
|
context: format!("ci/{job_name}"),
|
||||||
description: format!("Job '{job_name}' completed"),
|
description,
|
||||||
|
target_url: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -425,6 +465,46 @@ impl LocalCoordinator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn emit_pipeline_comment(&self, ctx: &Ctx, pipeline: &crate::pipeline::PipelineExecution) {
|
||||||
|
let reporter_addr = match self.status_reporter_addr {
|
||||||
|
Some(addr) => addr,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let job_outputs: Vec<JobOutput> = pipeline
|
||||||
|
.jobs
|
||||||
|
.values()
|
||||||
|
.map(|job| {
|
||||||
|
let passed = job.status == JobStatus::Passed;
|
||||||
|
let failure_reason = match &job.status {
|
||||||
|
JobStatus::Failed { reason } => Some(reason.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
JobOutput {
|
||||||
|
job_name: job.definition.name.clone(),
|
||||||
|
passed,
|
||||||
|
failure_reason,
|
||||||
|
output_lines: job.output_lines.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let _ = ctx.send(
|
||||||
|
reporter_addr,
|
||||||
|
StatusReporterMsg::PostPipelineComment {
|
||||||
|
repo_owner: pipeline.repo_owner.clone(),
|
||||||
|
repo_name: pipeline.repo_name.clone(),
|
||||||
|
commit_sha: pipeline.commit_sha.clone(),
|
||||||
|
branch: pipeline.branch.clone(),
|
||||||
|
pipeline_name: pipeline.pipeline_name.clone(),
|
||||||
|
pipeline_state: pipeline.status.forgejo_state().to_string(),
|
||||||
|
job_outputs,
|
||||||
|
forgejo_url: self.config.ci.forgejo_url.clone(),
|
||||||
|
forgejo_token: self.config.ci.forgejo_token.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn archive_pipeline(&mut self, pipeline: PipelineExecution) {
|
fn archive_pipeline(&mut self, pipeline: PipelineExecution) {
|
||||||
self.completed.push_back(pipeline);
|
self.completed.push_back(pipeline);
|
||||||
if self.completed.len() > 50 {
|
if self.completed.len() > 50 {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,21 @@
|
||||||
//! StatusReporter actor: fire-and-forget Forgejo commit status updates.
|
//! StatusReporter actor: fire-and-forget Forgejo commit status updates.
|
||||||
//!
|
//!
|
||||||
//! Receives status update messages and POSTs them to the Forgejo API.
|
//! Receives status update messages and POSTs them to the Forgejo API.
|
||||||
|
//! Can also post pipeline summary comments to PRs.
|
||||||
|
|
||||||
use swactor::actor::{ActorInterface, Ctx};
|
use swactor::actor::{ActorInterface, Ctx};
|
||||||
|
|
||||||
use crate::StatusUpdate;
|
use crate::StatusUpdate;
|
||||||
|
|
||||||
|
/// Captured output for a single job, used to build PR comments.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct JobOutput {
|
||||||
|
pub job_name: String,
|
||||||
|
pub passed: bool,
|
||||||
|
pub failure_reason: Option<String>,
|
||||||
|
pub output_lines: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Messages the StatusReporter can receive.
|
/// Messages the StatusReporter can receive.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum StatusReporterMsg {
|
pub enum StatusReporterMsg {
|
||||||
|
|
@ -14,6 +24,17 @@ pub enum StatusReporterMsg {
|
||||||
forgejo_url: String,
|
forgejo_url: String,
|
||||||
forgejo_token: String,
|
forgejo_token: String,
|
||||||
},
|
},
|
||||||
|
PostPipelineComment {
|
||||||
|
repo_owner: String,
|
||||||
|
repo_name: String,
|
||||||
|
commit_sha: String,
|
||||||
|
branch: String,
|
||||||
|
pipeline_name: String,
|
||||||
|
pipeline_state: String,
|
||||||
|
job_outputs: Vec<JobOutput>,
|
||||||
|
forgejo_url: String,
|
||||||
|
forgejo_token: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// StatusReporter actor state.
|
/// StatusReporter actor state.
|
||||||
|
|
@ -34,12 +55,16 @@ impl StatusReporter {
|
||||||
update.commit_sha,
|
update.commit_sha,
|
||||||
);
|
);
|
||||||
|
|
||||||
let body = serde_json::json!({
|
let mut body = serde_json::json!({
|
||||||
"state": update.state,
|
"state": update.state,
|
||||||
"context": update.context,
|
"context": update.context,
|
||||||
"description": update.description,
|
"description": update.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(ref target_url) = update.target_url {
|
||||||
|
body["target_url"] = serde_json::Value::String(target_url.clone());
|
||||||
|
}
|
||||||
|
|
||||||
let result = ureq::post(&url)
|
let result = ureq::post(&url)
|
||||||
.set("Authorization", &format!("token {forgejo_token}"))
|
.set("Authorization", &format!("token {forgejo_token}"))
|
||||||
.set("Content-Type", "application/json")
|
.set("Content-Type", "application/json")
|
||||||
|
|
@ -49,6 +74,194 @@ impl StatusReporter {
|
||||||
eprintln!("StatusReporter: failed to post status to {url}: {e}");
|
eprintln!("StatusReporter: failed to post status to {url}: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "local")]
|
||||||
|
fn find_pr_for_branch(
|
||||||
|
forgejo_url: &str,
|
||||||
|
forgejo_token: &str,
|
||||||
|
repo_owner: &str,
|
||||||
|
repo_name: &str,
|
||||||
|
branch: &str,
|
||||||
|
) -> Option<u64> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/v1/repos/{}/{}/pulls?state=open&limit=50",
|
||||||
|
forgejo_url.trim_end_matches('/'),
|
||||||
|
repo_owner,
|
||||||
|
repo_name,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = ureq::get(&url)
|
||||||
|
.set("Authorization", &format!("token {forgejo_token}"))
|
||||||
|
.call();
|
||||||
|
|
||||||
|
let response = match response {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("StatusReporter: failed to list PRs: {e}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let body: serde_json::Value = match response.into_json() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("StatusReporter: failed to parse PR list: {e}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let prs = body.as_array()?;
|
||||||
|
for pr in prs {
|
||||||
|
let head_ref = pr.get("head")?.get("ref")?.as_str()?;
|
||||||
|
if head_ref == branch {
|
||||||
|
return pr.get("number")?.as_u64();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "local")]
|
||||||
|
fn post_pr_comment(
|
||||||
|
forgejo_url: &str,
|
||||||
|
forgejo_token: &str,
|
||||||
|
repo_owner: &str,
|
||||||
|
repo_name: &str,
|
||||||
|
pr_number: u64,
|
||||||
|
body_text: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/v1/repos/{}/{}/issues/{}/comments",
|
||||||
|
forgejo_url.trim_end_matches('/'),
|
||||||
|
repo_owner,
|
||||||
|
repo_name,
|
||||||
|
pr_number,
|
||||||
|
);
|
||||||
|
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"body": body_text,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = ureq::post(&url)
|
||||||
|
.set("Authorization", &format!("token {forgejo_token}"))
|
||||||
|
.set("Content-Type", "application/json")
|
||||||
|
.send_string(&body.to_string());
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(response) => {
|
||||||
|
let json: serde_json::Value = response.into_json().ok()?;
|
||||||
|
json.get("html_url")?.as_str().map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("StatusReporter: failed to post PR comment: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "local")]
|
||||||
|
fn build_pipeline_comment(
|
||||||
|
pipeline_name: &str,
|
||||||
|
pipeline_state: &str,
|
||||||
|
commit_sha: &str,
|
||||||
|
job_outputs: &[JobOutput],
|
||||||
|
) -> String {
|
||||||
|
let mut md = format!("## Pipeline `{pipeline_name}` — {pipeline_state}\n\n");
|
||||||
|
let short_sha = if commit_sha.len() > 7 {
|
||||||
|
&commit_sha[..7]
|
||||||
|
} else {
|
||||||
|
commit_sha
|
||||||
|
};
|
||||||
|
md.push_str(&format!("Commit: `{short_sha}`\n\n"));
|
||||||
|
|
||||||
|
for job in job_outputs {
|
||||||
|
let status_label = if job.passed {
|
||||||
|
"passed".to_string()
|
||||||
|
} else {
|
||||||
|
match &job.failure_reason {
|
||||||
|
Some(reason) => format!("failed: {reason}"),
|
||||||
|
None => "failed".to_string(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
md.push_str(&format!(
|
||||||
|
"<details>\n<summary>{} — {}</summary>\n\n",
|
||||||
|
job.job_name, status_label
|
||||||
|
));
|
||||||
|
|
||||||
|
let max_lines = 100;
|
||||||
|
let total = job.output_lines.len();
|
||||||
|
let lines: &[String] = if total > max_lines {
|
||||||
|
md.push_str(&format!("_Showing last {max_lines} of {total} lines_\n\n"));
|
||||||
|
&job.output_lines[total - max_lines..]
|
||||||
|
} else {
|
||||||
|
&job.output_lines
|
||||||
|
};
|
||||||
|
|
||||||
|
md.push_str("```\n");
|
||||||
|
for line in lines {
|
||||||
|
md.push_str(line);
|
||||||
|
md.push('\n');
|
||||||
|
}
|
||||||
|
md.push_str("```\n\n</details>\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
md
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "local")]
|
||||||
|
fn handle_pipeline_comment(
|
||||||
|
repo_owner: &str,
|
||||||
|
repo_name: &str,
|
||||||
|
commit_sha: &str,
|
||||||
|
branch: &str,
|
||||||
|
pipeline_name: &str,
|
||||||
|
pipeline_state: &str,
|
||||||
|
job_outputs: &[JobOutput],
|
||||||
|
forgejo_url: &str,
|
||||||
|
forgejo_token: &str,
|
||||||
|
) {
|
||||||
|
let pr_number = match Self::find_pr_for_branch(
|
||||||
|
forgejo_url,
|
||||||
|
forgejo_token,
|
||||||
|
repo_owner,
|
||||||
|
repo_name,
|
||||||
|
branch,
|
||||||
|
) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
eprintln!(
|
||||||
|
"StatusReporter: no open PR for branch '{branch}', skipping comment"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let comment_body =
|
||||||
|
Self::build_pipeline_comment(pipeline_name, pipeline_state, commit_sha, job_outputs);
|
||||||
|
|
||||||
|
let comment_url = Self::post_pr_comment(
|
||||||
|
forgejo_url,
|
||||||
|
forgejo_token,
|
||||||
|
repo_owner,
|
||||||
|
repo_name,
|
||||||
|
pr_number,
|
||||||
|
&comment_body,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-post pipeline status with target_url pointing to the comment.
|
||||||
|
if let Some(ref url) = comment_url {
|
||||||
|
let update = StatusUpdate {
|
||||||
|
repo_owner: repo_owner.to_string(),
|
||||||
|
repo_name: repo_name.to_string(),
|
||||||
|
commit_sha: commit_sha.to_string(),
|
||||||
|
state: pipeline_state.to_string(),
|
||||||
|
context: format!("ci/{pipeline_name}"),
|
||||||
|
description: format!("Pipeline '{pipeline_name}' {pipeline_state}"),
|
||||||
|
target_url: Some(url.clone()),
|
||||||
|
};
|
||||||
|
Self::post_status(&update, forgejo_url, forgejo_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActorInterface for StatusReporter {
|
impl ActorInterface for StatusReporter {
|
||||||
|
|
@ -70,6 +283,45 @@ impl ActorInterface for StatusReporter {
|
||||||
let _ = (update, forgejo_url, forgejo_token);
|
let _ = (update, forgejo_url, forgejo_token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
StatusReporterMsg::PostPipelineComment {
|
||||||
|
repo_owner,
|
||||||
|
repo_name,
|
||||||
|
commit_sha,
|
||||||
|
branch,
|
||||||
|
pipeline_name,
|
||||||
|
pipeline_state,
|
||||||
|
job_outputs,
|
||||||
|
forgejo_url,
|
||||||
|
forgejo_token,
|
||||||
|
} => {
|
||||||
|
#[cfg(feature = "local")]
|
||||||
|
Self::handle_pipeline_comment(
|
||||||
|
&repo_owner,
|
||||||
|
&repo_name,
|
||||||
|
&commit_sha,
|
||||||
|
&branch,
|
||||||
|
&pipeline_name,
|
||||||
|
&pipeline_state,
|
||||||
|
&job_outputs,
|
||||||
|
&forgejo_url,
|
||||||
|
&forgejo_token,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "local"))]
|
||||||
|
{
|
||||||
|
let _ = (
|
||||||
|
repo_owner,
|
||||||
|
repo_name,
|
||||||
|
commit_sha,
|
||||||
|
branch,
|
||||||
|
pipeline_name,
|
||||||
|
pipeline_state,
|
||||||
|
job_outputs,
|
||||||
|
forgejo_url,
|
||||||
|
forgejo_token,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ pub fn run_simulation(config: LocalSimConfig) -> LocalSimTrace {
|
||||||
state: "pending".into(),
|
state: "pending".into(),
|
||||||
context: format!("ci/{pipeline_name}"),
|
context: format!("ci/{pipeline_name}"),
|
||||||
description: format!("Pipeline '{pipeline_name}' is pending"),
|
description: format!("Pipeline '{pipeline_name}' is pending"),
|
||||||
|
target_url: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
pipelines.insert(pipeline_id, pipeline);
|
pipelines.insert(pipeline_id, pipeline);
|
||||||
|
|
@ -182,6 +183,7 @@ pub fn run_simulation(config: LocalSimConfig) -> LocalSimTrace {
|
||||||
state: "error".into(),
|
state: "error".into(),
|
||||||
context: format!("ci/{}", old_pipeline.pipeline_name),
|
context: format!("ci/{}", old_pipeline.pipeline_name),
|
||||||
description: "superseded".into(),
|
description: "superseded".into(),
|
||||||
|
target_url: None,
|
||||||
});
|
});
|
||||||
completed_pipelines.push(old_pipeline);
|
completed_pipelines.push(old_pipeline);
|
||||||
}
|
}
|
||||||
|
|
@ -342,6 +344,7 @@ fn schedule_next(
|
||||||
pipeline.pipeline_name,
|
pipeline.pipeline_name,
|
||||||
pipeline.status.forgejo_state()
|
pipeline.status.forgejo_state()
|
||||||
),
|
),
|
||||||
|
target_url: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
events.push((
|
events.push((
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,7 @@ pub fn run_simulation(config: CiSimConfig) -> CiSimTrace {
|
||||||
state: "pending".into(),
|
state: "pending".into(),
|
||||||
context: format!("ci/{pipeline_name}"),
|
context: format!("ci/{pipeline_name}"),
|
||||||
description: format!("Pipeline '{pipeline_name}' is pending"),
|
description: format!("Pipeline '{pipeline_name}' is pending"),
|
||||||
|
target_url: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
pipelines.insert(pipeline_id, pipeline);
|
pipelines.insert(pipeline_id, pipeline);
|
||||||
|
|
@ -481,6 +482,7 @@ pub fn run_simulation(config: CiSimConfig) -> CiSimTrace {
|
||||||
pipeline.pipeline_name,
|
pipeline.pipeline_name,
|
||||||
pipeline.status.forgejo_state()
|
pipeline.status.forgejo_state()
|
||||||
),
|
),
|
||||||
|
target_url: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit per-job skipped events.
|
// Emit per-job skipped events.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue