Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/hm-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,18 @@ pub fn app_url(api: &str, override_url: Option<&str>) -> String {
pub struct CloudConfig {
pub org: Option<String>,
pub api_url: String,
/// Org-global pipeline slug to submit builds to directly (set by `hm run`
/// after registering a remoteless directory). When present, cloud runs
/// submit by this slug instead of resolving by git-repo identity.
pub pipeline: Option<String>,
}

impl Default for CloudConfig {
fn default() -> Self {
Self {
org: None,
api_url: DEFAULT_API_URL.to_owned(),
pipeline: None,
}
}
}
Expand Down Expand Up @@ -270,6 +275,7 @@ mod tests {
assert_eq!(cfg.backend, Backend::Docker);
assert_eq!(cfg.cloud.api_url, DEFAULT_API_URL);
assert!(cfg.cloud.org.is_none());
assert!(cfg.cloud.pipeline.is_none());
assert_eq!(cfg.preferences.format, "human");
assert!(!cfg.preferences.auto_watch);
}
Expand All @@ -280,6 +286,7 @@ mod tests {
[cloud]
org = "acme"
api_url = "https://custom.api"
pipeline = "acme/web"

[preferences]
format = "json"
Expand All @@ -288,6 +295,7 @@ auto_watch = true
let cfg: Config = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.cloud.org.as_deref(), Some("acme"));
assert_eq!(cfg.cloud.api_url, "https://custom.api");
assert_eq!(cfg.cloud.pipeline.as_deref(), Some("acme/web"));
assert_eq!(cfg.preferences.format, "json");
assert!(cfg.preferences.auto_watch);
}
Expand Down Expand Up @@ -388,6 +396,7 @@ org = "project-org"
let cfg = Config {
cloud: CloudConfig {
org: Some("saved-org".into()),
pipeline: Some("saved-org/web".into()),
..CloudConfig::default()
},
..Config::default()
Expand All @@ -396,6 +405,7 @@ org = "project-org"

let loaded = Config::load_from_paths(Some(&path), None).unwrap();
assert_eq!(loaded.cloud.org.as_deref(), Some("saved-org"));
assert_eq!(loaded.cloud.pipeline.as_deref(), Some("saved-org/web"));
assert_eq!(loaded.cloud.api_url, DEFAULT_API_URL);
assert_eq!(loaded.preferences.format, "human");
}
Expand Down
78 changes: 51 additions & 27 deletions crates/hm-exec/src/cloud/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
//! to Harmont Cloud, and watch it to completion. The server schedules and runs;
//! this backend is an *observer* (see [`Capabilities::cloud`]).

use harmont_cloud::{HarmontClient, HarmontError, builds::NewRepoBuild};
use harmont_cloud::{
HarmontClient, HarmontError,
builds::{NewBuild, NewRepoBuild},
};
use hm_plugin_protocol::events::{BuildEvent, BuildRef};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
Expand Down Expand Up @@ -74,6 +77,10 @@ impl ExecutionBackend for CloudBackend {
Capabilities::cloud()
}

// The two submit branches (by-slug vs. repo-identity) plus the
// watch/no-watch handling push this just over the 100-line lint; splitting
// it would only scatter the linear submit-then-watch flow.
#[allow(clippy::too_many_lines)]
async fn start(&self, req: RunRequest) -> Result<BackendHandle> {
// Archive the worktree (fail fast as a setup error).
let source_tgz = crate::local::build_archive_bytes(&req.repo_root)
Expand All @@ -84,32 +91,49 @@ impl ExecutionBackend for CloudBackend {
// the size so the upload isn't a silent gulf of evaluation.
guard_archive_size(source_tgz.len(), &req.repo_root)?;

// Resolve the repo's `owner/repo`; cloud runs address the pipeline by
// (repo_name, source_slug) — the bare DSL slug is not the server's
// global slug. A worktree with no remote can't be matched to a pipeline.
let repo_name = req.source.repo_name.clone().ok_or_else(|| {
BackendError::Local(
"cloud runs need a git remote to identify the pipeline — add an \
`origin` remote, or run from a cloned repo"
.into(),
)
})?;

let build = self
.client
.submit_repo_build(NewRepoBuild {
org: self.org.clone(),
repo_name,
source_slug: req.pipeline_slug.clone(),
branch: req.source.branch.clone(),
commit: req.source.commit.clone(),
message: req.source.message.clone(),
pipeline_ir: req.plan.ir_json.clone(), // verbatim
source_tgz,
env: req.env.clone().into_iter().collect(),
})
.await
.map_err(map_harmont_err)?;
// Submit the build. Normally we address the pipeline by repo identity
// (`submit_repo_build` resolves `(repo_name, source_slug)` to the
// server's global slug). But when the driver has already resolved or
// created the pipeline — for a repo the server hasn't discovered — it
// passes the global slug directly, and we submit by slug
// (`submit_build`), bypassing repo-identity resolution.
let build = if let Some(slug) = req.cloud_pipeline_slug.clone() {
self.client
.submit_build(NewBuild {
org: self.org.clone(),
pipeline: slug,
branch: req.source.branch.clone(),
commit: req.source.commit.clone(),
message: req.source.message.clone(),
pipeline_ir: req.plan.ir_json.clone(), // verbatim
source_tgz,
env: req.env.clone().into_iter().collect(),
})
.await
.map_err(map_harmont_err)?
} else {
let repo_name = req.source.repo_name.clone().ok_or_else(|| {
BackendError::Local(
"cloud runs need a git remote to identify the pipeline — add an \
`origin` remote, or run from a cloned repo"
.into(),
)
})?;
self.client
.submit_repo_build(NewRepoBuild {
org: self.org.clone(),
repo_name,
source_slug: req.pipeline_slug.clone(),
branch: req.source.branch.clone(),
commit: req.source.commit.clone(),
message: req.source.message.clone(),
pipeline_ir: req.plan.ir_json.clone(), // verbatim
source_tgz,
env: req.env.clone().into_iter().collect(),
})
.await
.map_err(map_harmont_err)?
};

// The server resolved (and returns) the global slug; watch/cancel/log
// endpoints are addressed by it, NOT by the source slug we submitted.
Expand Down
7 changes: 7 additions & 0 deletions crates/hm-exec/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ pub struct RunRequest {
pub env: BTreeMap<String, String>,
pub source: SourceMeta,
pub options: RunOptions,
/// When `Some`, the cloud backend submits the build directly to this
/// already-resolved org-global pipeline slug (via `submit_build`) instead
/// of resolving by repo identity (`submit_repo_build`). Set by the `hm run`
/// driver after it has created or looked up the pipeline for a repo the
/// server hasn't "discovered" (connected/pushed). `None` for the normal
/// repo-identity path. Ignored by non-cloud backends.
pub cloud_pipeline_slug: Option<String>,
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions crates/hm-exec/tests/backend_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,6 @@ fn fake_request() -> RunRequest {
watch: true,
..Default::default()
},
cloud_pipeline_slug: None,
}
}
Loading
Loading