Skip to content

Commit beb7086

Browse files
remove COW (#29)
* remove COW * better readme * commit clean.rs * remove decorative section-divider comments * fix clippy strict warnings in scheduler, cache, and clean * fix: cargo fmt formatting
1 parent 2cd4334 commit beb7086

15 files changed

Lines changed: 253 additions & 362 deletions

File tree

README.md

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -75,56 +75,6 @@ Save this as `.harmont/pipeline.py` (or `.harmont/pipeline.ts`):
7575
<details open>
7676
<summary><b>Python</b></summary>
7777

78-
```python
79-
import harmont as hm
80-
81-
@hm.pipeline("ci")
82-
def ci() -> hm.Step:
83-
return (
84-
hm.sh("echo 'hello from harmont'", label="hello")
85-
.sh("uname -a", label="env")
86-
)
87-
```
88-
89-
</details>
90-
91-
<details>
92-
<summary><b>TypeScript</b></summary>
93-
94-
```typescript
95-
import { sh, pipeline, type PipelineDefinition } from "harmont";
96-
97-
const pipelines: PipelineDefinition[] = [
98-
{
99-
slug: "ci",
100-
pipeline: pipeline(
101-
sh("echo 'hello from harmont'", { label: "hello" })
102-
.sh("uname -a", { label: "env" }),
103-
),
104-
},
105-
];
106-
107-
export default pipelines;
108-
```
109-
110-
</details>
111-
112-
### 2. Run it
113-
114-
```sh
115-
hm run ci
116-
```
117-
118-
If the repo declares only one pipeline, the slug is optional - just `hm run`.
119-
120-
### Real-world example
121-
122-
For production pipelines, use typed toolchains - they generate test, lint, and
123-
format steps from your project layout:
124-
125-
<details open>
126-
<summary><b>Python</b></summary>
127-
12878
```python
12979
import harmont as hm
13080
from harmont.python import PythonToolchain
@@ -177,6 +127,14 @@ export default pipelines;
177127

178128
</details>
179129

130+
### 2. Run it
131+
132+
```sh
133+
hm run ci
134+
```
135+
136+
If the repo declares only one pipeline, the slug is optional - just `hm run`.
137+
180138
Browse the [example projects](./examples) for idiomatic pipelines in Rust,
181139
Go, Python, Java, C++, React, Next.js, and more.
182140

crates/hm-pipeline-ir/tests/e2e_fixtures.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ fn edge_kinds(g: &PipelineGraph) -> (usize, usize) {
5050
(builds_in, depends_on)
5151
}
5252

53-
// ---- Python fixtures ----
54-
5553
#[test]
5654
fn python_monorepo_ci() {
5755
let g = load_fixture("python", "monorepo-ci");
@@ -115,8 +113,6 @@ fn python_kitchen_sink() {
115113
}
116114
}
117115

118-
// ---- TypeScript fixtures ----
119-
120116
#[test]
121117
fn ts_monorepo_ci() {
122118
let g = load_fixture("ts", "monorepo-ci");
@@ -145,8 +141,6 @@ fn ts_kitchen_sink() {
145141
assert!(g.node_count() >= 12);
146142
}
147143

148-
// ---- Structural invariants on all fixtures ----
149-
150144
#[test]
151145
fn all_fixtures_have_valid_structure() {
152146
for dsl in ["python", "ts"] {
@@ -172,8 +166,6 @@ fn all_fixtures_have_valid_structure() {
172166
}
173167
}
174168

175-
// ---- Cross-DSL parity ----
176-
177169
#[test]
178170
fn parity_node_count() {
179171
for scenario in SCENARIOS {

crates/hm/src/cli/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ pub enum CacheCommand {
6767
Save(CacheSaveArgs),
6868
/// Restore harmont Docker images from a cache directory.
6969
Restore(CacheRestoreArgs),
70+
/// Remove all cached workspaces and Docker images.
71+
Clean,
7072
}
7173

7274
#[derive(Debug, Clone, clap::Args)]
@@ -92,6 +94,7 @@ pub async fn dispatch(command: Command, ctx: RunContext) -> Result<i32> {
9294
Command::Cache(cmd) => match cmd {
9395
CacheCommand::Save(args) => crate::commands::cache::handle_save(&args.dir).await,
9496
CacheCommand::Restore(args) => crate::commands::cache::handle_restore(&args.dir).await,
97+
CacheCommand::Clean => crate::commands::cache::handle_clean().await,
9598
},
9699
Command::Version => version::run().await.map(|()| 0),
97100
Command::Plugin(cmd) => plugin::run(cmd).await.map(|()| 0),
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use anyhow::Result;
2+
3+
/// # Errors
4+
/// Returns an error if workspace cache removal or Docker image listing fails.
5+
pub async fn handle_clean() -> Result<i32> {
6+
let mut cleaned = if let Some(ws_cache) = hm_util::dirs::harmont_workspace_cache_dir()
7+
&& ws_cache.exists()
8+
{
9+
let size = dir_size(&ws_cache);
10+
std::fs::remove_dir_all(&ws_cache)?;
11+
tracing::info!(
12+
path = %ws_cache.display(),
13+
"removed workspace cache ({})",
14+
human_bytes(size),
15+
);
16+
true
17+
} else {
18+
false
19+
};
20+
21+
let docker = match crate::orchestrator::docker_client::DockerClient::connect() {
22+
Ok(d) => match d.ping().await {
23+
Ok(()) => Some(d),
24+
Err(e) => {
25+
tracing::warn!(%e, "Docker daemon unreachable — skipping image cleanup");
26+
None
27+
}
28+
},
29+
Err(e) => {
30+
tracing::warn!(%e, "cannot connect to Docker — skipping image cleanup");
31+
None
32+
}
33+
};
34+
35+
if let Some(docker) = &docker {
36+
let cache_images = docker.list_images_by_prefix("harmont-cache/").await?;
37+
for tag in &cache_images {
38+
if let Err(e) = docker.remove_image(tag).await {
39+
tracing::warn!(image = %tag, %e, "failed to remove cached image");
40+
} else {
41+
tracing::info!(image = %tag, "removed cached Docker image");
42+
cleaned = true;
43+
}
44+
}
45+
46+
let ephemeral_images = docker
47+
.list_images_by_prefix("harmont-local-ephemeral/")
48+
.await?;
49+
for tag in &ephemeral_images {
50+
if let Err(e) = docker.remove_image(tag).await {
51+
tracing::warn!(image = %tag, %e, "failed to remove ephemeral image");
52+
} else {
53+
tracing::info!(image = %tag, "removed ephemeral Docker image");
54+
cleaned = true;
55+
}
56+
}
57+
}
58+
59+
if !cleaned {
60+
tracing::info!("nothing to clean");
61+
}
62+
63+
Ok(0)
64+
}
65+
66+
fn dir_size(path: &std::path::Path) -> u64 {
67+
fn walk(p: &std::path::Path) -> u64 {
68+
std::fs::read_dir(p)
69+
.into_iter()
70+
.flatten()
71+
.filter_map(std::result::Result::ok)
72+
.map(|e| {
73+
let path = e.path();
74+
if path.is_dir() {
75+
walk(&path)
76+
} else {
77+
e.metadata().map_or(0, |m| m.len())
78+
}
79+
})
80+
.sum()
81+
}
82+
walk(path)
83+
}
84+
85+
#[allow(
86+
clippy::cast_precision_loss,
87+
reason = "human-readable display; sub-byte precision irrelevant"
88+
)]
89+
fn human_bytes(bytes: u64) -> String {
90+
let b = bytes as f64;
91+
if bytes < 1024 {
92+
format!("{bytes}B")
93+
} else if bytes < 1024 * 1024 {
94+
format!("{:.1}KB", b / 1024.0)
95+
} else if bytes < 1024 * 1024 * 1024 {
96+
format!("{:.1}MB", b / (1024.0 * 1024.0))
97+
} else {
98+
format!("{:.1}GB", b / (1024.0 * 1024.0 * 1024.0))
99+
}
100+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
mod clean;
12
pub mod manifest;
23
mod restore;
34
mod save;
45

6+
pub use clean::handle_clean;
57
pub use restore::handle_restore;
68
pub use save::handle_save;

crates/hm/src/orchestrator/archive.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ impl ArchiveStore {
4242
.unwrap_or(0)
4343
}
4444

45+
/// Return a clone of the full archive bytes, or `None` if unknown.
46+
#[must_use]
47+
pub fn get_bytes(&self, id: ArchiveId) -> Option<Vec<u8>> {
48+
self.archives.lock().ok()?.get(&id).cloned()
49+
}
50+
4551
/// Read up to `max` bytes from offset `offset`. Returns empty
4652
/// when offset is beyond end, or when the archive is unknown.
4753
#[must_use]

0 commit comments

Comments
 (0)