Add repository owner/name parsing and integrate with commit status updates

This commit introduces:
- A utility function to parse repository owner and name from URLs, supporting HTTPS, SSH, and Git formats.
- Enhancements to job messages and results with optional `repo_owner` and `repo_name` fields for downstream integrations.
- Updated orchestrator and forge-integration workflows to leverage parsed repository details for status updates and accurate routing.
This commit is contained in:
Till Wegmueller 2025-11-03 23:36:25 +01:00
parent c00ce54112
commit 06ae079b14
No known key found for this signature in database
4 changed files with 87 additions and 14 deletions

View file

@ -1,6 +1,6 @@
- Return status to codeberg: Extract repo from Webhook and keep in messages
- Make RabbitMQ Messages Print nicely - Make RabbitMQ Messages Print nicely
- move runner logs to debug level so they can be logged in the CI job but don't spam the deployed version - move runner logs to debug level so they can be logged in the CI job but don't spam the deployed version
- Make Orchestrator serve the runner binaries so no external server is needed - Make Orchestrator serve the runner binaries so no external server is needed
- Make orchestrator detect the address it will be reachable by checking the libvirt config or on illumos use it's external IP - Make orchestrator detect the address it will be reachable by checking the libvirt config or on illumos use it's external IP
- Make VM reachable IP of the orchestrator configurable in case the setup on illumos gets more complicated (via config file) - Make VM reachable IP of the orchestrator configurable in case the setup on illumos gets more complicated (via config file)
- Make the forge-integration task use fnox secrets

View file

@ -15,6 +15,12 @@ pub struct JobRequest {
pub source: SourceSystem, pub source: SourceSystem,
/// Repository clone URL (SSH or HTTPS). /// Repository clone URL (SSH or HTTPS).
pub repo_url: String, pub repo_url: String,
/// Repository owner (parsed from webhook or URL); optional for backward-compat.
#[serde(default)]
pub repo_owner: Option<String>,
/// Repository name (parsed from webhook or URL); optional for backward-compat.
#[serde(default)]
pub repo_name: Option<String>,
/// Commit SHA to check out. /// Commit SHA to check out.
pub commit_sha: String, pub commit_sha: String,
/// Optional path to the workflow file within the repo (KDL). /// Optional path to the workflow file within the repo (KDL).
@ -50,6 +56,8 @@ impl JobRequest {
request_id: Uuid::new_v4(), request_id: Uuid::new_v4(),
source, source,
repo_url: repo_url.into(), repo_url: repo_url.into(),
repo_owner: None,
repo_name: None,
commit_sha: commit_sha.into(), commit_sha: commit_sha.into(),
workflow_path: None, workflow_path: None,
workflow_job_id: None, workflow_job_id: None,
@ -69,6 +77,12 @@ pub struct JobResult {
pub request_id: Uuid, pub request_id: Uuid,
/// Repository and commit info (for convenience in consumers) /// Repository and commit info (for convenience in consumers)
pub repo_url: String, pub repo_url: String,
/// Repository owner (when known). Optional for backward-compat.
#[serde(default)]
pub repo_owner: Option<String>,
/// Repository name (when known). Optional for backward-compat.
#[serde(default)]
pub repo_name: Option<String>,
pub commit_sha: String, pub commit_sha: String,
/// Outcome info /// Outcome info
pub success: bool, pub success: bool,
@ -96,6 +110,8 @@ impl JobResult {
schema_version: default_jobresult_schema(), schema_version: default_jobresult_schema(),
request_id, request_id,
repo_url, repo_url,
repo_owner: None,
repo_name: None,
commit_sha, commit_sha,
success, success,
exit_code, exit_code,

View file

@ -221,6 +221,20 @@ async fn post_commit_status(
) -> Result<()> { ) -> Result<()> {
// Extract owner/repo from repo_url (supports https://.../owner/repo.git and ssh://git@host/owner/repo.git) // Extract owner/repo from repo_url (supports https://.../owner/repo.git and ssh://git@host/owner/repo.git)
let (owner, repo) = parse_owner_repo(repo_url).ok_or_else(|| miette::miette!("cannot parse owner/repo from repo_url: {repo_url}"))?; let (owner, repo) = parse_owner_repo(repo_url).ok_or_else(|| miette::miette!("cannot parse owner/repo from repo_url: {repo_url}"))?;
post_commit_status_owner(base, token, &owner, &repo, sha, context, state, target_url, description).await
}
async fn post_commit_status_owner(
base: &str,
token: &str,
owner: &str,
repo: &str,
sha: &str,
context: &str,
state: &str,
target_url: Option<&str>,
description: Option<&str>,
) -> Result<()> {
let api = format!("{}/repos/{}/{}/statuses/{}", base.trim_end_matches('/'), owner, repo, sha); let api = format!("{}/repos/{}/{}/statuses/{}", base.trim_end_matches('/'), owner, repo, sha);
let mut body = serde_json::json!({ let mut body = serde_json::json!({
"state": state, "state": state,
@ -377,6 +391,20 @@ async fn handle_job_result(state: &AppState, jobres: &common::messages::JobResul
let state_str = if jobres.success { "success" } else { "failure" }; let state_str = if jobres.success { "success" } else { "failure" };
if let (Some(base), Some(token)) = (state.forgejo_base.as_ref(), state.forgejo_token.as_ref()) { if let (Some(base), Some(token)) = (state.forgejo_base.as_ref(), state.forgejo_token.as_ref()) {
let desc = if jobres.success { Some("Job succeeded") } else { Some("Job failed") }; let desc = if jobres.success { Some("Job succeeded") } else { Some("Job failed") };
// Prefer explicit owner/repo from JobResult when available
if let (Some(owner), Some(repo)) = (jobres.repo_owner.as_ref(), jobres.repo_name.as_ref()) {
let _ = post_commit_status_owner(
base,
token,
owner,
repo,
&jobres.commit_sha,
&state.forge_context,
state_str,
target_url.as_deref(),
desc,
).await;
} else {
let _ = post_commit_status( let _ = post_commit_status(
base, base,
token, token,
@ -386,8 +414,8 @@ async fn handle_job_result(state: &AppState, jobres: &common::messages::JobResul
state_str, state_str,
target_url.as_deref(), target_url.as_deref(),
desc, desc,
) ).await;
.await; }
} }
Ok(()) Ok(())
@ -679,6 +707,11 @@ async fn enqueue_job(state: &Arc<AppState>, repo_url: String, commit_sha: String
miette::bail!("missing repo_url in webhook payload"); miette::bail!("missing repo_url in webhook payload");
} }
let mut jr = common::JobRequest::new(common::SourceSystem::Forgejo, repo_url, commit_sha); let mut jr = common::JobRequest::new(common::SourceSystem::Forgejo, repo_url, commit_sha);
// Try to populate repo_owner/repo_name from URL for accurate status routing
if let Some((owner, name)) = parse_owner_repo(&jr.repo_url) {
jr.repo_owner = Some(owner);
jr.repo_name = Some(name);
}
// Infer runs_on from repo map, labels, or default // Infer runs_on from repo map, labels, or default
jr.runs_on = infer_runs_on(state, &jr.repo_url, labels.as_ref().map(|v| v.as_slice())); jr.runs_on = infer_runs_on(state, &jr.repo_url, labels.as_ref().map(|v| v.as_slice()));
common::publish_job(&state.mq_cfg, &jr).await?; common::publish_job(&state.mq_cfg, &jr).await?;

View file

@ -13,6 +13,25 @@ use std::sync::Arc;
use crate::persist::Persist; use crate::persist::Persist;
fn parse_owner_repo(repo_url: &str) -> Option<(String, String)> {
let url = repo_url.trim_end_matches(".git");
if let Some(rest) = url.strip_prefix("https://").or_else(|| url.strip_prefix("http://")) {
let parts: Vec<&str> = rest.split('/').collect();
if parts.len() >= 3 { return Some((parts[1].to_string(), parts[2].to_string())); }
} else if let Some(rest) = url.strip_prefix("ssh://") {
// ssh://git@host/owner/repo
let after_host = rest.splitn(2, '/').nth(1)?;
let parts: Vec<&str> = after_host.split('/').collect();
if parts.len() >= 2 { return Some((parts[0].to_string(), parts[1].to_string())); }
} else if let Some(idx) = url.find(':') {
// git@host:owner/repo
let after = &url[idx+1..];
let parts: Vec<&str> = after.split('/').collect();
if parts.len() >= 2 { return Some((parts[0].to_string(), parts[1].to_string())); }
}
None
}
pub struct RunnerSvc { pub struct RunnerSvc {
mq_cfg: MqConfig, mq_cfg: MqConfig,
persist: Arc<Persist>, persist: Arc<Persist>,
@ -95,7 +114,7 @@ impl Runner for RunnerSvc {
if let (Some(id), Some(repo), Some(sha)) = if let (Some(id), Some(repo), Some(sha)) =
(req_id.as_ref(), repo_url.as_ref(), commit_sha.as_ref()) (req_id.as_ref(), repo_url.as_ref(), commit_sha.as_ref())
{ {
let result = common::messages::JobResult::new( let mut result = common::messages::JobResult::new(
id.clone(), id.clone(),
repo.clone(), repo.clone(),
sha.clone(), sha.clone(),
@ -103,6 +122,11 @@ impl Runner for RunnerSvc {
exit_code, exit_code,
None, None,
); );
// Try to parse owner/repo for downstream integrations to avoid reparsing URLs
if let Some((owner, name)) = parse_owner_repo(&repo) {
result.repo_owner = Some(owner);
result.repo_name = Some(name);
}
if let Err(e) = publish_job_result(&self.mq_cfg, &result).await { if let Err(e) = publish_job_result(&self.mq_cfg, &result).await {
error!(error = %e, request_id = %id, "failed to publish JobResult"); error!(error = %e, request_id = %id, "failed to publish JobResult");
} }