Skip to content

Commit b3b5136

Browse files
committed
merge: resolve conflicts with origin/main
Take our tracing-based versions over main's reformatted print macros and remove clippy allow attrs that are no longer needed.
2 parents 64ae87b + a008c99 commit b3b5136

204 files changed

Lines changed: 10597 additions & 504 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,56 @@ jobs:
6767
--deselect tests/test_gradle.py \
6868
--deselect tests/test_haskell.py
6969
70+
ts-dsl:
71+
name: harmont-ts (vitest + tsc)
72+
runs-on: ubuntu-latest
73+
steps:
74+
- uses: actions/checkout@v4
75+
76+
- uses: actions/setup-node@v4
77+
with:
78+
node-version: "20"
79+
cache: npm
80+
cache-dependency-path: dsls/harmont-ts/package-lock.json
81+
82+
- name: Install
83+
working-directory: dsls/harmont-ts
84+
run: npm ci
85+
86+
- name: Type check
87+
working-directory: dsls/harmont-ts
88+
run: npx tsc --noEmit
89+
90+
- name: Test
91+
working-directory: dsls/harmont-ts
92+
run: npm test
93+
94+
dogfood:
95+
name: dogfood (hm run ci)
96+
runs-on: ubuntu-latest
97+
timeout-minutes: 30
98+
steps:
99+
- uses: actions/checkout@v4
100+
101+
- uses: dtolnay/rust-toolchain@stable
102+
with:
103+
targets: wasm32-wasip1
104+
105+
- uses: Swatinem/rust-cache@v2
106+
107+
- name: Build hm
108+
run: cargo build -p harmont-cli
109+
110+
- name: Install harmont-py into system Python
111+
run: |
112+
sudo /usr/bin/python3 -m pip install --break-system-packages dsls/harmont-py
113+
/usr/bin/python3 -c "import harmont; print('harmont', harmont.__file__)"
114+
115+
- name: hm run ci
116+
env:
117+
HM_NONINTERACTIVE: '1'
118+
run: ./target/debug/hm run ci
119+
70120
integration:
71121
name: docker-gated integration test
72122
runs-on: ubuntu-latest

.harmont/ci.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Harmont CI pipeline — dogfood."""
2+
from __future__ import annotations
3+
4+
import harmont as hm
5+
from harmont.py.uv import UvProject
6+
from harmont.rust import RustToolchain
7+
8+
9+
@hm.target()
10+
def rust_project() -> RustToolchain:
11+
return hm.rust(path=".")
12+
13+
14+
@hm.target()
15+
def py_project() -> UvProject:
16+
return hm.py.uv(path="dsls/harmont-py")
17+
18+
19+
@hm.pipeline(
20+
"ci",
21+
env={"CI": "true"},
22+
default_image="ubuntu:24.04",
23+
triggers=[
24+
hm.push(branch="main"),
25+
hm.pull_request(branches="main"),
26+
],
27+
)
28+
def ci(
29+
rust_project: hm.Target[RustToolchain],
30+
py_project: hm.Target[UvProject],
31+
) -> tuple[hm.Step, ...]:
32+
return (
33+
rust_project.build(),
34+
rust_project.installed.sh(
35+
". $HOME/.cargo/env && cd . && cargo test --lib",
36+
label=":rust: test",
37+
),
38+
rust_project.clippy(),
39+
rust_project.fmt(),
40+
py_project.lint(),
41+
py_project.fmt(),
42+
py_project.typecheck(paths="harmont"),
43+
py_project.run(
44+
"pytest -v"
45+
" --deselect tests/test_gradle.py"
46+
" --deselect tests/test_haskell.py",
47+
label=":python: test",
48+
),
49+
)

.harmont/ci.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { pipeline, push, pullRequest, type PipelineDefinition } from "harmont";
2+
import { rust, py } from "harmont/toolchains";
3+
4+
const rustProject = rust({ path: "." });
5+
const pyProject = py.uv({ path: "dsls/harmont-py" });
6+
7+
const pipelines: PipelineDefinition[] = [
8+
{
9+
slug: "ci",
10+
triggers: [push({ branch: "main" }), pullRequest({ branches: ["main"] })],
11+
pipeline: pipeline(
12+
rustProject.build(),
13+
rustProject.install().sh(`. $HOME/.cargo/env && cd . && cargo test --lib`, { label: ":rust: test" }),
14+
rustProject.clippy(),
15+
rustProject.fmt(),
16+
pyProject.lint(),
17+
pyProject.fmt(),
18+
pyProject.typecheck({ paths: "harmont" }),
19+
pyProject.run(
20+
"pytest -v --deselect tests/test_gradle.py --deselect tests/test_haskell.py",
21+
{ label: ":python: test" },
22+
),
23+
{ env: { CI: "true" }, defaultImage: "ubuntu:24.04" },
24+
),
25+
},
26+
];
27+
28+
export default pipelines;
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
#![allow(
2+
clippy::cargo_common_metadata,
3+
clippy::multiple_crate_versions,
4+
clippy::unwrap_used,
5+
clippy::expect_used,
6+
clippy::panic
7+
)]
8+
9+
use std::collections::BTreeSet;
10+
use std::fs;
11+
use std::path::PathBuf;
12+
13+
use daggy::petgraph::visit::{EdgeRef, IntoNodeReferences};
14+
use hm_pipeline_ir::{EdgeKind, PipelineGraph};
15+
16+
const SCENARIOS: &[&str] = &[
17+
"monorepo-ci",
18+
"rust-release",
19+
"zig-node-polyglot",
20+
"kitchen-sink",
21+
];
22+
23+
fn fixtures_dir() -> PathBuf {
24+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/e2e/fixtures")
25+
}
26+
27+
fn load_fixture(dsl: &str, scenario: &str) -> PipelineGraph {
28+
let path = fixtures_dir().join(dsl).join(format!("{scenario}.json"));
29+
let bytes = fs::read(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
30+
serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("parse {dsl}/{scenario}: {e}"))
31+
}
32+
33+
fn step_labels(g: &PipelineGraph) -> BTreeSet<String> {
34+
g.dag()
35+
.graph()
36+
.node_references()
37+
.filter_map(|(_, t)| t.step.label.clone())
38+
.collect()
39+
}
40+
41+
fn edge_kinds(g: &PipelineGraph) -> (usize, usize) {
42+
let mut builds_in = 0usize;
43+
let mut depends_on = 0usize;
44+
for e in g.dag().graph().edge_references() {
45+
match e.weight() {
46+
EdgeKind::BuildsIn => builds_in += 1,
47+
EdgeKind::DependsOn => depends_on += 1,
48+
}
49+
}
50+
(builds_in, depends_on)
51+
}
52+
53+
// ---- Python fixtures ----
54+
55+
#[test]
56+
fn python_monorepo_ci() {
57+
let g = load_fixture("python", "monorepo-ci");
58+
assert_eq!(g.default_image(), Some("ubuntu:24.04"));
59+
assert!(g.node_count() >= 15, "nodes: {}", g.node_count());
60+
let labels = step_labels(&g);
61+
assert!(labels.iter().any(|l| l.contains("go")));
62+
assert!(
63+
labels
64+
.iter()
65+
.any(|l| l.contains("python") || l.contains("uv"))
66+
);
67+
assert!(
68+
labels
69+
.iter()
70+
.any(|l| l.contains("node") || l.contains("npm"))
71+
);
72+
}
73+
74+
#[test]
75+
fn python_rust_release() {
76+
let g = load_fixture("python", "rust-release");
77+
assert_eq!(g.default_image(), Some("ubuntu:24.04"));
78+
assert!(g.node_count() >= 5, "nodes: {}", g.node_count());
79+
let labels = step_labels(&g);
80+
assert!(labels.iter().any(|l| l.contains("rust")));
81+
}
82+
83+
#[test]
84+
fn python_zig_node_polyglot() {
85+
let g = load_fixture("python", "zig-node-polyglot");
86+
assert_eq!(g.default_image(), Some("ubuntu:24.04"));
87+
assert!(g.node_count() >= 10, "nodes: {}", g.node_count());
88+
let labels = step_labels(&g);
89+
assert!(labels.iter().any(|l| l.contains("zig")));
90+
assert!(
91+
labels
92+
.iter()
93+
.any(|l| l.contains("node") || l.contains("npm"))
94+
);
95+
}
96+
97+
#[test]
98+
fn python_kitchen_sink() {
99+
let g = load_fixture("python", "kitchen-sink");
100+
assert_eq!(g.default_image(), Some("ubuntu:24.04"));
101+
assert!(g.node_count() >= 12, "nodes: {}", g.node_count());
102+
let labels = step_labels(&g);
103+
assert!(labels.iter().any(|l| l.contains("haskell")));
104+
assert!(
105+
labels
106+
.iter()
107+
.any(|l| l.contains("cmake") || l.contains(":c:"))
108+
);
109+
for (_, t) in g.dag().graph().node_references() {
110+
assert!(
111+
t.env.contains_key("CI"),
112+
"node {} missing CI env",
113+
t.step.key
114+
);
115+
}
116+
}
117+
118+
// ---- TypeScript fixtures ----
119+
120+
#[test]
121+
fn ts_monorepo_ci() {
122+
let g = load_fixture("ts", "monorepo-ci");
123+
assert_eq!(g.default_image(), Some("ubuntu:24.04"));
124+
assert!(g.node_count() >= 15);
125+
}
126+
127+
#[test]
128+
fn ts_rust_release() {
129+
let g = load_fixture("ts", "rust-release");
130+
assert_eq!(g.default_image(), Some("ubuntu:24.04"));
131+
assert!(g.node_count() >= 5);
132+
}
133+
134+
#[test]
135+
fn ts_zig_node_polyglot() {
136+
let g = load_fixture("ts", "zig-node-polyglot");
137+
assert_eq!(g.default_image(), Some("ubuntu:24.04"));
138+
assert!(g.node_count() >= 10);
139+
}
140+
141+
#[test]
142+
fn ts_kitchen_sink() {
143+
let g = load_fixture("ts", "kitchen-sink");
144+
assert_eq!(g.default_image(), Some("ubuntu:24.04"));
145+
assert!(g.node_count() >= 12);
146+
}
147+
148+
// ---- Structural invariants on all fixtures ----
149+
150+
#[test]
151+
fn all_fixtures_have_valid_structure() {
152+
for dsl in ["python", "ts"] {
153+
for scenario in SCENARIOS {
154+
let g = load_fixture(dsl, scenario);
155+
156+
for (_, t) in g.dag().graph().node_references() {
157+
assert!(!t.step.key.is_empty(), "{dsl}/{scenario}: empty key");
158+
assert!(
159+
!t.step.cmd.is_empty(),
160+
"{dsl}/{scenario}: empty cmd for {}",
161+
t.step.key,
162+
);
163+
}
164+
165+
let (bi, dep) = edge_kinds(&g);
166+
assert!(bi + dep > 0, "{dsl}/{scenario}: no edges");
167+
168+
for e in g.dag().graph().edge_references() {
169+
assert_ne!(e.source(), e.target(), "{dsl}/{scenario}: self-loop");
170+
}
171+
}
172+
}
173+
}
174+
175+
// ---- Cross-DSL parity ----
176+
177+
#[test]
178+
fn parity_node_count() {
179+
for scenario in SCENARIOS {
180+
let py = load_fixture("python", scenario);
181+
let ts = load_fixture("ts", scenario);
182+
assert_eq!(
183+
py.node_count(),
184+
ts.node_count(),
185+
"parity/{scenario}: node count (py={}, ts={})",
186+
py.node_count(),
187+
ts.node_count(),
188+
);
189+
}
190+
}
191+
192+
#[test]
193+
fn parity_edge_kinds() {
194+
for scenario in SCENARIOS {
195+
let py = load_fixture("python", scenario);
196+
let ts = load_fixture("ts", scenario);
197+
let py_ek = edge_kinds(&py);
198+
let ts_ek = edge_kinds(&ts);
199+
assert_eq!(
200+
py_ek, ts_ek,
201+
"parity/{scenario}: edge kinds (py={py_ek:?}, ts={ts_ek:?})",
202+
);
203+
}
204+
}
205+
206+
#[test]
207+
fn parity_step_labels() {
208+
for scenario in SCENARIOS {
209+
let py = load_fixture("python", scenario);
210+
let ts = load_fixture("ts", scenario);
211+
let py_labels = step_labels(&py);
212+
let ts_labels = step_labels(&ts);
213+
assert_eq!(
214+
py_labels,
215+
ts_labels,
216+
"parity/{scenario}: labels\npy-only: {:?}\nts-only: {:?}",
217+
py_labels.difference(&ts_labels).collect::<Vec<_>>(),
218+
ts_labels.difference(&py_labels).collect::<Vec<_>>(),
219+
);
220+
}
221+
}
222+
223+
#[test]
224+
fn parity_default_image() {
225+
for scenario in SCENARIOS {
226+
let py = load_fixture("python", scenario);
227+
let ts = load_fixture("ts", scenario);
228+
assert_eq!(
229+
py.default_image(),
230+
ts.default_image(),
231+
"parity/{scenario}: default_image",
232+
);
233+
}
234+
}
235+
236+
#[test]
237+
fn parity_env_keys() {
238+
for scenario in SCENARIOS {
239+
let py = load_fixture("python", scenario);
240+
let ts = load_fixture("ts", scenario);
241+
let py_labels = step_labels(&py);
242+
let ts_labels = step_labels(&ts);
243+
244+
for label in py_labels.intersection(&ts_labels) {
245+
let py_env: BTreeSet<_> = py
246+
.dag()
247+
.graph()
248+
.node_references()
249+
.find(|(_, t)| t.step.label.as_deref() == Some(label))
250+
.map(|(_, t)| t.env.keys().cloned().collect())
251+
.unwrap();
252+
let ts_env: BTreeSet<_> = ts
253+
.dag()
254+
.graph()
255+
.node_references()
256+
.find(|(_, t)| t.step.label.as_deref() == Some(label))
257+
.map(|(_, t)| t.env.keys().cloned().collect())
258+
.unwrap();
259+
assert_eq!(py_env, ts_env, "parity/{scenario}/{label}: env keys");
260+
}
261+
}
262+
}

0 commit comments

Comments
 (0)