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
58 changes: 8 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,56 +75,6 @@ Save this as `.harmont/pipeline.py` (or `.harmont/pipeline.ts`):
<details open>
<summary><b>Python</b></summary>

```python
import harmont as hm

@hm.pipeline("ci")
def ci() -> hm.Step:
return (
hm.sh("echo 'hello from harmont'", label="hello")
.sh("uname -a", label="env")
)
```

</details>

<details>
<summary><b>TypeScript</b></summary>

```typescript
import { sh, pipeline, type PipelineDefinition } from "harmont";

const pipelines: PipelineDefinition[] = [
{
slug: "ci",
pipeline: pipeline(
sh("echo 'hello from harmont'", { label: "hello" })
.sh("uname -a", { label: "env" }),
),
},
];

export default pipelines;
```

</details>

### 2. Run it

```sh
hm run ci
```

If the repo declares only one pipeline, the slug is optional - just `hm run`.

### Real-world example

For production pipelines, use typed toolchains - they generate test, lint, and
format steps from your project layout:

<details open>
<summary><b>Python</b></summary>

```python
import harmont as hm
from harmont.python import PythonToolchain
Expand Down Expand Up @@ -177,6 +127,14 @@ export default pipelines;

</details>

### 2. Run it

```sh
hm run ci
```

If the repo declares only one pipeline, the slug is optional - just `hm run`.

Browse the [example projects](./examples) for idiomatic pipelines in Rust,
Go, Python, Java, C++, React, Next.js, and more.

Expand Down
8 changes: 0 additions & 8 deletions crates/hm-pipeline-ir/tests/e2e_fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ fn edge_kinds(g: &PipelineGraph) -> (usize, usize) {
(builds_in, depends_on)
}

// ---- Python fixtures ----

#[test]
fn python_monorepo_ci() {
let g = load_fixture("python", "monorepo-ci");
Expand Down Expand Up @@ -115,8 +113,6 @@ fn python_kitchen_sink() {
}
}

// ---- TypeScript fixtures ----

#[test]
fn ts_monorepo_ci() {
let g = load_fixture("ts", "monorepo-ci");
Expand Down Expand Up @@ -145,8 +141,6 @@ fn ts_kitchen_sink() {
assert!(g.node_count() >= 12);
}

// ---- Structural invariants on all fixtures ----

#[test]
fn all_fixtures_have_valid_structure() {
for dsl in ["python", "ts"] {
Expand All @@ -172,8 +166,6 @@ fn all_fixtures_have_valid_structure() {
}
}

// ---- Cross-DSL parity ----

#[test]
fn parity_node_count() {
for scenario in SCENARIOS {
Expand Down
3 changes: 3 additions & 0 deletions crates/hm/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ pub enum CacheCommand {
Save(CacheSaveArgs),
/// Restore harmont Docker images from a cache directory.
Restore(CacheRestoreArgs),
/// Remove all cached workspaces and Docker images.
Clean,
}

#[derive(Debug, Clone, clap::Args)]
Expand All @@ -92,6 +94,7 @@ pub async fn dispatch(command: Command, ctx: RunContext) -> Result<i32> {
Command::Cache(cmd) => match cmd {
CacheCommand::Save(args) => crate::commands::cache::handle_save(&args.dir).await,
CacheCommand::Restore(args) => crate::commands::cache::handle_restore(&args.dir).await,
CacheCommand::Clean => crate::commands::cache::handle_clean().await,
},
Command::Version => version::run().await.map(|()| 0),
Command::Plugin(cmd) => plugin::run(cmd).await.map(|()| 0),
Expand Down
100 changes: 100 additions & 0 deletions crates/hm/src/commands/cache/clean.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use anyhow::Result;

/// # Errors
/// Returns an error if workspace cache removal or Docker image listing fails.
pub async fn handle_clean() -> Result<i32> {
let mut cleaned = if let Some(ws_cache) = hm_util::dirs::harmont_workspace_cache_dir()
&& ws_cache.exists()
{
let size = dir_size(&ws_cache);
std::fs::remove_dir_all(&ws_cache)?;
tracing::info!(
path = %ws_cache.display(),
"removed workspace cache ({})",
human_bytes(size),
);
true
} else {
false
};

let docker = match crate::orchestrator::docker_client::DockerClient::connect() {
Ok(d) => match d.ping().await {
Ok(()) => Some(d),
Err(e) => {
tracing::warn!(%e, "Docker daemon unreachable — skipping image cleanup");
None
}
},
Err(e) => {
tracing::warn!(%e, "cannot connect to Docker — skipping image cleanup");
None
}
};

if let Some(docker) = &docker {
let cache_images = docker.list_images_by_prefix("harmont-cache/").await?;
for tag in &cache_images {
if let Err(e) = docker.remove_image(tag).await {
tracing::warn!(image = %tag, %e, "failed to remove cached image");
} else {
tracing::info!(image = %tag, "removed cached Docker image");
cleaned = true;
}
}

let ephemeral_images = docker
.list_images_by_prefix("harmont-local-ephemeral/")
.await?;
for tag in &ephemeral_images {
if let Err(e) = docker.remove_image(tag).await {
tracing::warn!(image = %tag, %e, "failed to remove ephemeral image");
} else {
tracing::info!(image = %tag, "removed ephemeral Docker image");
cleaned = true;
}
}
}

if !cleaned {
tracing::info!("nothing to clean");
}

Ok(0)
}

fn dir_size(path: &std::path::Path) -> u64 {
fn walk(p: &std::path::Path) -> u64 {
std::fs::read_dir(p)
.into_iter()
.flatten()
.filter_map(std::result::Result::ok)
.map(|e| {
let path = e.path();
if path.is_dir() {
walk(&path)
} else {
e.metadata().map_or(0, |m| m.len())
}
})
.sum()
}
walk(path)
}

#[allow(
clippy::cast_precision_loss,
reason = "human-readable display; sub-byte precision irrelevant"
)]
fn human_bytes(bytes: u64) -> String {
let b = bytes as f64;
if bytes < 1024 {
format!("{bytes}B")
} else if bytes < 1024 * 1024 {
format!("{:.1}KB", b / 1024.0)
} else if bytes < 1024 * 1024 * 1024 {
format!("{:.1}MB", b / (1024.0 * 1024.0))
} else {
format!("{:.1}GB", b / (1024.0 * 1024.0 * 1024.0))
}
}
2 changes: 2 additions & 0 deletions crates/hm/src/commands/cache/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod clean;
pub mod manifest;
mod restore;
mod save;

pub use clean::handle_clean;
pub use restore::handle_restore;
pub use save::handle_save;
6 changes: 6 additions & 0 deletions crates/hm/src/orchestrator/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ impl ArchiveStore {
.unwrap_or(0)
}

/// Return a clone of the full archive bytes, or `None` if unknown.
#[must_use]
pub fn get_bytes(&self, id: ArchiveId) -> Option<Vec<u8>> {
self.archives.lock().ok()?.get(&id).cloned()
}

/// Read up to `max` bytes from offset `offset`. Returns empty
/// when offset is beyond end, or when the archive is unknown.
#[must_use]
Expand Down
Loading
Loading