Skip to content
Open
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
36 changes: 23 additions & 13 deletions crates/hm-cloud/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ use harmont_cloud::{HarmontClient, HarmontError};
use hm_common::url_nonce::UrlNonce;
use hm_core::{app_ctx::AppCtx, config::ResolvedCloudConfig};
use secrecy::ExposeSecret as _;
use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, net::TcpListener};
use thiserror::Error;
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
net::TcpListener,
};
use tracing::{info, instrument, warn};
use url::Url;

Expand All @@ -33,10 +36,13 @@ impl BrowserAuth {
/// spawn the task that serves the redirect.
#[instrument]
async fn open(app: Url, nonce: &UrlNonce) -> Result<(), BrowserAuthError> {
let listener = TcpListener::bind("127.0.0.1:0").await
let listener = TcpListener::bind("127.0.0.1:0")
.await
.map_err(BrowserAuthError::CouldNotCreateListener)?;
let port = listener.local_addr()
.map_err(BrowserAuthError::CouldNotDeduceAddress)?.port();
let port = listener
.local_addr()
.map_err(BrowserAuthError::CouldNotDeduceAddress)?
.port();

let mut url = app;
url.set_path("/cli-login");
Expand Down Expand Up @@ -117,14 +123,16 @@ impl PasteTokenAuth {

/// Prompt for a login code, re-prompting until a non-empty code is entered.
async fn read_code() -> Result<String, PasteAuthError> {
tokio::task::spawn_blocking(|| loop {
let raw = dialoguer::Input::<String>::new()
.with_prompt("code")
.interact()
.map_err(|e| PasteAuthError::Prompt(e.to_string()))?;
let code = raw.trim().to_string();
if !code.is_empty() {
return Ok(code);
tokio::task::spawn_blocking(|| {
loop {
let raw = dialoguer::Input::<String>::new()
.with_prompt("code")
.interact()
.map_err(|e| PasteAuthError::Prompt(e.to_string()))?;
let code = raw.trim().to_string();
if !code.is_empty() {
return Ok(code);
}
}
})
.await
Expand Down Expand Up @@ -166,7 +174,9 @@ impl<'client> ClaimPoller<'client> {
loop {
match self.client.claim_token(&nonce).await {
Ok(token) => return Ok(token),
Err(HarmontError::Api { status: 400, code, .. }) if code == "cli_code_invalid" => {
Err(HarmontError::Api {
status: 400, code, ..
}) if code == "cli_code_invalid" => {
if Instant::now() >= deadline {
return Err(ClaimError::TimedOut);
}
Expand Down
14 changes: 11 additions & 3 deletions crates/hm-common/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ pub struct GitBranch<'r, 'g, 'bin> {
name: BString,
}

impl<'r, 'g, 'bin> GitBranch<'r, 'g, 'bin> {
impl GitBranch<'_, '_, '_> {
/// The branch name (e.g. `main`, or `HEAD` when detached).
#[must_use]
pub fn name(&self) -> &BStr {
Expand All @@ -181,7 +181,12 @@ impl<'r, 'g, 'bin> GitBranch<'r, 'g, 'bin> {
#[tracing::instrument(skip(self))]
pub fn head_commit(&self) -> Option<GitSha> {
let name = self.name.to_str().ok()?;
self.repo.run(&["rev-parse", name])?.to_str().ok()?.parse().ok()
self.repo
.run(&["rev-parse", name])?
.to_str()
.ok()?
.parse()
.ok()
}
}

Expand Down Expand Up @@ -279,7 +284,10 @@ mod tests {
#[case::just_over(41)]
#[case::sha256_width(64)]
fn rejects_wrong_length(#[case] len: usize) {
assert_eq!("a".repeat(len).parse::<GitSha>(), Err(GitShaError::BadLength(len)));
assert_eq!(
"a".repeat(len).parse::<GitSha>(),
Err(GitShaError::BadLength(len))
);
}

#[rstest]
Expand Down
16 changes: 9 additions & 7 deletions crates/hm-core/src/exec/local/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! Cache keys are computed by `harmont.keygen` at plan time and ride
//! along the JSON in `cache.key`.

use hm_plugin_protocol::CommandStep;
use hm_plugin_protocol::Step;

fn sanitize_for_tag(s: &str) -> String {
s.chars()
Expand All @@ -25,7 +25,7 @@ fn sanitize_for_tag(s: &str) -> String {
/// Returns `None` when the step has no cache, a `"none"` policy, or no
/// cache key.
#[must_use]
pub(crate) fn stable_cache_tag(step: &CommandStep) -> Option<String> {
pub(crate) fn stable_cache_tag(step: &Step) -> Option<String> {
let cache = step.cache.as_ref()?;
if cache.policy == "none" {
return None;
Expand All @@ -45,16 +45,18 @@ pub(crate) fn stable_cache_tag(step: &CommandStep) -> Option<String> {
)]
mod tests {
use super::*;
use hm_plugin_protocol::Cache;
use hm_plugin_protocol::{Cache, Step, StepAction};
use rstest::rstest;

fn step(cache: Option<Cache>) -> CommandStep {
CommandStep {
fn step(cache: Option<Cache>) -> Step {
Step {
key: "build".into(),
action: StepAction::Command {
cmd: "true".into(),
env: None,
},
label: None,
cmd: "true".into(),
image: None,
env: None,
timeout_seconds: None,
cache,
runner: None,
Expand Down
153 changes: 78 additions & 75 deletions crates/hm-core/src/exec/local/runner/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::sync::Arc;

use anyhow::{Context, Result};
use hm_plugin_protocol::{
BuildEvent, CacheDecision, ExecutorInput, SnapshotRef, StdStream, StepResult,
BuildEvent, CacheDecision, ExecutorInput, SnapshotRef, StdStream, StepAction, StepResult,
};
use hm_vm::types::OutputSink;
use hm_vm::{Action, CachingPolicy, HmVm, ImageSource, SnapshotId};
Expand Down Expand Up @@ -68,13 +68,6 @@ impl StepRunner for VmRunner {

#[tracing::instrument(skip(vm, ctx), fields(step_key = %input.step.key))]
async fn run_step_vm(vm: &HmVm, ctx: &StepContext, input: ExecutorInput) -> Result<StepResult> {
let policy = match &input.cache_lookup {
CacheDecision::Hit { tag } | CacheDecision::MissBuildAs { tag } => {
CachingPolicy::Cache { key: tag.0.clone() }
}
CacheDecision::MissNoCommit => CachingPolicy::None,
};

let source = if let Some(ref snap) = input.parent_snapshot {
ImageSource::Snapshot(SnapshotId::new(snap.0.clone()))
} else {
Expand All @@ -90,74 +83,84 @@ async fn run_step_vm(vm: &HmVm, ctx: &StepContext, input: ExecutorInput) -> Resu
)
};

// Inject the current workspace on every executing step, overlaying it
// onto the system state inherited from the parent snapshot (apt packages,
// installed runtimes, `node_modules`, …). Injecting only at the chain root
// is wrong: root steps such as `apt_base` are `CacheForever`, so their
// snapshots freeze the source tree captured at first build and every COW
// descendant inherits that stale tree — source edits never reach leaf
// steps. A true cache hit short-circuits inside `HmVm::execute` before
// inject runs, so this overlay only happens when a step actually executes;
// the overlay (Docker PUT-archive) adds/overwrites files without deleting
// the inherited system state.
let (inject, _temp_guard) = {
let archive_bytes = ctx
.archives
.get_bytes(input.workspace_archive_id)
.ok_or_else(|| anyhow::anyhow!("source archive not found"))?;
let dir =
extract_archive_to_tempdir(&archive_bytes).context("extracting workspace archive")?;
let path = dir.path().to_path_buf();
(Some(path), Some(dir))
};

// Baseline env for shell operation inside VMs.
let mut env: Vec<(String, String)> = vec![
("HOME".into(), "/root".into()),
(
"PATH".into(),
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into(),
),
];
env.extend(input.env);

let action = Action {
source,
cmd: input.step.cmd.clone(),
env,
working_dir: input.workdir.clone(),
timeout: None,
inject,
};

let sink = EventBusSink {
step_id: input.step_id,
bus: Arc::clone(&ctx.event_bus),
};

let result = tokio::select! {
r = vm.execute(action, policy, &sink) => r,
() = ctx.cancel.cancelled() => {
anyhow::bail!("step cancelled (build timeout or sibling failure)")
let result = match &input.step.action {
StepAction::Command { cmd, .. } => {
let policy = match &input.cache_lookup {
CacheDecision::Hit { tag } | CacheDecision::MissBuildAs { tag } => {
CachingPolicy::Cache { key: tag.0.clone() }
}
CacheDecision::MissNoCommit => CachingPolicy::None,
};

// Inject the current workspace on every executing step, overlaying it
// onto the system state inherited from the parent snapshot (apt packages,
// installed runtimes, `node_modules`, …). Injecting only at the chain root
// is wrong: root steps such as `apt_base` are `CacheForever`, so their
// snapshots freeze the source tree captured at first build and every COW
// descendant inherits that stale tree — source edits never reach leaf
// steps. A true cache hit short-circuits inside `HmVm::execute` before
// inject runs, so this overlay only happens when a step actually executes;
// the overlay (Docker PUT-archive) adds/overwrites files without deleting
// the inherited system state.
let (inject, _temp_guard) = {
let archive_bytes = ctx
.archives
.get_bytes(input.workspace_archive_id)
.ok_or_else(|| anyhow::anyhow!("source archive not found"))?;
let dir = extract_archive_to_tempdir(&archive_bytes)
.context("extracting workspace archive")?;
let path = dir.path().to_path_buf();
(Some(path), Some(dir))
};

// Baseline env for shell operation inside VMs.
let mut env: Vec<(String, String)> = vec![
("HOME".into(), "/root".into()),
(
"PATH".into(),
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into(),
),
];
env.extend(input.env);

let action = Action {
source,
cmd: cmd.clone(),
env,
working_dir: input.workdir.clone(),
timeout: None,
inject,
};

let sink = EventBusSink {
step_id: input.step_id,
bus: Arc::clone(&ctx.event_bus),
};

let result = tokio::select! {
r = vm.execute(action, policy, &sink) => r,
() = ctx.cancel.cancelled() => {
anyhow::bail!("step cancelled (build timeout or sibling failure)")
}
}
.context("vm execute failed")?;

if result.cached {
ctx.event_bus.emit(BuildEvent::StepCacheHit {
step_id: input.step_id,
key: input.step.key.clone(),
tag: result
.snapshot
.as_ref()
.map_or_else(String::new, ToString::to_string),
});
}
result
}
}
.context("vm execute failed")?;

if result.cached {
ctx.event_bus.emit(BuildEvent::StepCacheHit {
step_id: input.step_id,
key: input
.step
.cache
.as_ref()
.and_then(|c| c.key.clone())
.unwrap_or_default(),
tag: result
.snapshot
.as_ref()
.map_or_else(String::new, ToString::to_string),
});
}
StepAction::Mount { from, to } => {
vm.mount_into_vm(from, to, &input.workdir, &source).await?
}
};

Ok(StepResult {
exit_code: result.exit_code,
Expand Down
10 changes: 6 additions & 4 deletions crates/hm-core/src/exec/local/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use hm_plugin_protocol::{
};
use uuid::Uuid;

use hm_pipeline_ir::{DurationMs, EdgeKind, PipelineGraph, Transition};
use hm_pipeline_ir::{DurationMs, EdgeKind, PipelineGraph, StepAction, Transition};

use crate::exec::local::runner::{RunnerRegistry, StepContext};
use crate::exec::local::source::build_archive_bytes;
Expand Down Expand Up @@ -391,9 +391,11 @@ async fn execute_step(
let step_wire = transition.step;
let step_key = step_wire.key.clone();
let display_name = step_wire.label.clone().unwrap_or_else(|| {
step_wire
.cmd
.trim()
let feat = match &step_wire.action {
StepAction::Command { cmd, .. } => cmd.clone(),
StepAction::Mount { from, .. } => from.clone(),
};
feat.trim()
.ellipsize(Measure::Columns(40), Pos::End, Indicator::UNICODE)
.to_string()
});
Expand Down
8 changes: 4 additions & 4 deletions crates/hm-core/src/exec/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ mod tests {
"default_image": "ubuntu:24.04",
"graph": {
"nodes": [
{"step": {"key": "a", "cmd": "echo a", "image": "ubuntu:24.04"}, "env": {}},
{"step": {"key": "b", "cmd": "echo b"}, "env": {}}
{"step": {"key": "a", "action": {"cmd": "echo a"}, "image": "ubuntu:24.04"}, "env": {}},
{"step": {"key": "b", "action": {"cmd": "echo b"}}, "env": {}}
],
"node_holes": [],
"edge_property": "directed",
Expand All @@ -139,8 +139,8 @@ mod tests {
"version": "0",
"graph": {
"nodes": [
{"step": {"key": "a", "cmd": "echo a", "image": "ubuntu:24.04"}, "env": {}},
{"step": {"key": "b", "cmd": "echo b", "image": "ubuntu:24.04"}, "env": {}}
{"step": {"key": "a", "action": {"cmd": "echo a"}, "image": "ubuntu:24.04"}, "env": {}},
{"step": {"key": "b", "action": {"cmd": "echo b"}, "image": "ubuntu:24.04"}, "env": {}}
],
"node_holes": [],
"edge_property": "directed",
Expand Down
Loading
Loading