Skip to content

Commit 562f6f7

Browse files
committed
5a6e0c
1 parent 9c63201 commit 562f6f7

4 files changed

Lines changed: 282 additions & 49 deletions

File tree

cgr.py

Lines changed: 147 additions & 24 deletions
Large diffs are not rendered by default.

ide.html

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,13 +357,22 @@
357357
.file-picker-hdr { display:flex; align-items:center; gap:8px; padding:10px 12px; border-bottom:1px solid var(--border); font-family:var(--mono); font-size:12px; color:var(--text2); }
358358
.file-picker-search { margin:12px; margin-bottom:8px; width:calc(100% - 24px); background:var(--bg3); border:1px solid var(--border); border-radius:var(--radius); color:var(--text); font-family:var(--mono); font-size:12px; padding:8px 10px; outline:none; }
359359
.file-picker-search:focus { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
360+
.file-picker-controls { display:flex; align-items:center; gap:10px; padding:0 12px 10px; }
361+
.file-picker-filter { display:flex; align-items:center; gap:8px; font-family:var(--mono); font-size:11px; color:var(--text3); }
362+
.file-picker-filter select { background:var(--bg3); border:1px solid var(--border); border-radius:var(--radius); color:var(--text); font-family:var(--mono); font-size:11px; padding:6px 8px; outline:none; }
363+
.file-picker-filter select:focus { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }
360364
.file-picker-list { padding:0 12px 12px; overflow:auto; }
361365
.file-picker-item { padding:8px 10px; margin:0 0 6px; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg3); cursor:pointer; transition:all .15s; }
362366
.file-picker-item:hover { border-color:var(--accent); background:var(--bg-hover); }
363367
.file-picker-item.active { border-color:var(--accent); box-shadow:0 0 0 1px rgba(88,166,255,.35); }
364368
.file-picker-path { font-family:var(--mono); font-size:12px; color:var(--text); display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
365369
.file-picker-meta { margin-top:2px; font-size:10px; color:var(--text3); }
366370
.file-picker-tag { display:inline-block; padding:1px 6px; border-radius:999px; font-size:9px; font-weight:700; letter-spacing:.03em; text-transform:uppercase; }
371+
.file-picker-tag.cgr { background:rgba(88,166,255,.14); color:var(--accent); }
372+
.file-picker-tag.cg { background:rgba(210,153,34,.14); color:var(--amber); }
373+
.file-picker-tag.root { background:rgba(139,148,158,.18); color:var(--text2); }
374+
.file-picker-tag.example { background:rgba(63,185,80,.14); color:var(--green); }
375+
.file-picker-tag.testing { background:rgba(188,140,255,.14); color:var(--purple); }
367376
.file-picker-tag.template { background:rgba(240,136,62,.15); color:var(--coral); }
368377
.file-picker-empty { padding:14px 6px; color:var(--text3); font-size:12px; text-align:center; }
369378
.file-picker-trigger { display:inline-flex; align-items:center; justify-content:center; width:22px; height:22px; border:1px solid var(--border); border-radius:var(--radius); background:var(--bg3); color:var(--text2); cursor:pointer; font-size:12px; transition:all .15s; }
@@ -510,6 +519,14 @@ <h1>&#9671; CommandGraph</h1>
510519
<button class="btn" onclick="closeFilePicker()" style="font-size:10px;padding:2px 8px">Close</button>
511520
</div>
512521
<input class="file-picker-search" id="filePickerSearch" type="text" placeholder="Filter .cgr / .cg files" oninput="renderFilePicker(this.value)">
522+
<div class="file-picker-controls">
523+
<label class="file-picker-filter" for="filePickerTagFilter">
524+
<span>Show</span>
525+
<select id="filePickerTagFilter" onchange="renderFilePicker(filePickerSearch.value)">
526+
<option value="all">all files</option>
527+
</select>
528+
</label>
529+
</div>
513530
<div class="file-picker-list" id="filePickerList"></div>
514531
</div>
515532
</div>
@@ -539,6 +556,7 @@ <h1>&#9671; CommandGraph</h1>
539556
let applyStopClicks = 0;
540557
let availableGraphFiles = [];
541558
let availableGraphFileEntries = [];
559+
let filePickerTagOptions = [];
542560

543561
const codeEl = document.getElementById('code');
544562
const hlEl = document.getElementById('highlight');
@@ -558,6 +576,7 @@ <h1>&#9671; CommandGraph</h1>
558576
const rerunBtn = document.getElementById('rerunBtn');
559577
const filePickerBackdrop = document.getElementById('filePickerBackdrop');
560578
const filePickerSearch = document.getElementById('filePickerSearch');
579+
const filePickerTagFilter = document.getElementById('filePickerTagFilter');
561580
const filePickerList = document.getElementById('filePickerList');
562581
const filePickerBtn = document.getElementById('filePickerBtn');
563582
let execInputWrap = null;
@@ -632,7 +651,13 @@ <h1>&#9671; CommandGraph</h1>
632651

633652
function renderFilePicker(filter = '') {
634653
const q = String(filter || '').toLowerCase().trim();
635-
const files = availableGraphFileEntries.filter(f => !q || f.name.toLowerCase().includes(q));
654+
const activeTag = filePickerTagFilter ? filePickerTagFilter.value : 'all';
655+
const files = availableGraphFileEntries.filter(f => {
656+
const matchesSearch = !q || f.name.toLowerCase().includes(q);
657+
const tags = Array.isArray(f.tags) ? f.tags : [];
658+
const matchesTag = activeTag === 'all' || tags.includes(activeTag);
659+
return matchesSearch && matchesTag;
660+
});
636661
filePickerList.innerHTML = '';
637662
if (!files.length) {
638663
filePickerList.innerHTML = '<div class="file-picker-empty">No matching graph files</div>';
@@ -642,14 +667,47 @@ <h1>&#9671; CommandGraph</h1>
642667
const name = file.name;
643668
const isActive = currentFile === name;
644669
const label = name.split('/').pop();
670+
const tags = Array.isArray(file.tags) ? file.tags : [];
671+
const tagsHtml = tags.map(tag => `<span class="file-picker-tag ${esc(tag)}">${esc(tag)}</span>`).join('');
645672
const item = document.createElement('div');
646673
item.className = 'file-picker-item' + (isActive ? ' active' : '');
647-
item.innerHTML = `<div class="file-picker-path"><span>${esc(label)}</span>${file.is_template ? '<span class="file-picker-tag template">template</span>' : ''}</div><div class="file-picker-meta">${esc(name)}${isActive ? ' · current file' : ''}</div>`;
674+
item.innerHTML = `<div class="file-picker-path"><span>${esc(label)}</span>${tagsHtml}</div><div class="file-picker-meta">${esc(name)}${isActive ? ' · current file' : ''}</div>`;
648675
item.addEventListener('click', () => { void chooseFileFromPicker(name); });
649676
filePickerList.appendChild(item);
650677
});
651678
}
652679

680+
function filePickerTagLabel(tag) {
681+
const labels = {
682+
all: 'all files',
683+
root: 'root files',
684+
example: 'examples',
685+
testing: 'testing',
686+
template: 'templates',
687+
cgr: '.cgr',
688+
cg: '.cg',
689+
};
690+
return labels[tag] || tag;
691+
}
692+
693+
function refreshFilePickerTagOptions() {
694+
if (!filePickerTagFilter) return;
695+
const current = filePickerTagFilter.value || 'all';
696+
const preferredOrder = ['root', 'example', 'testing', 'template', 'cgr', 'cg'];
697+
const available = new Set();
698+
availableGraphFileEntries.forEach(file => {
699+
(file.tags || []).forEach(tag => available.add(tag));
700+
});
701+
filePickerTagOptions = preferredOrder.filter(tag => available.has(tag));
702+
available.forEach(tag => {
703+
if (!filePickerTagOptions.includes(tag)) filePickerTagOptions.push(tag);
704+
});
705+
const nextValue = current === 'all' || available.has(current) ? current : 'all';
706+
filePickerTagFilter.innerHTML = `<option value="all">${esc(filePickerTagLabel('all'))}</option>`
707+
+ filePickerTagOptions.map(tag => `<option value="${esc(tag)}">${esc(filePickerTagLabel(tag))}</option>`).join('');
708+
filePickerTagFilter.value = nextValue;
709+
}
710+
653711
function openFilePicker() {
654712
if (!availableGraphFiles.length) return;
655713
filePickerBackdrop.style.display = 'flex';
@@ -2956,7 +3014,8 @@ <h1>&#9671; CommandGraph</h1>
29563014
try {
29573015
const info = await api('GET', '/api/info');
29583016
availableGraphFiles = info.files || [];
2959-
availableGraphFileEntries = info.file_entries || availableGraphFiles.map(name => ({ name, is_template: false }));
3017+
availableGraphFileEntries = info.file_entries || availableGraphFiles.map(name => ({ name, is_template: false, tags: [] }));
3018+
refreshFilePickerTagOptions();
29603019
filenameEl.title = availableGraphFiles.length ? 'Click to choose file' : 'No graph files available';
29613020
filePickerBtn.classList.toggle('disabled', !availableGraphFiles.length);
29623021
filePickerBtn.title = availableGraphFiles.length ? 'Open graph file (Ctrl+O)' : 'No graph files available';

parallel_test.cg

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,86 @@
1-
# Parallelism test — .cg brace-delimited format
2-
# Equivalent to parallel_test.cgr
3-
41
var servers = "web-1,web-2,web-3,web-4"
52
var version = "2.4.1"
63

74
node "deploy" {
85
via local
96

107
resource build_release {
8+
description "build release"
119
check `test -f /tmp/cg_par_test/release.tar.gz`
12-
run `mkdir -p /tmp/cg_par_test && echo "built" > /tmp/cg_par_test/release.tar.gz`
10+
run `mkdir -p /tmp/cg_par_test && echo "built" > /tmp/cg_par_test/release.tar.gz`
1311
}
14-
15-
# ── Test 1: parallel block ──────────────────────────
1612
resource configure_everything {
13+
description "configure everything"
1714
needs build_release
18-
19-
parallel 2, if_one_fails wait {
15+
run `echo "all configs written"`
16+
parallel 2 {
2017
resource write_config_a {
18+
description "write config a"
2119
run `echo "config_a" > /tmp/cg_par_test/a.conf`
2220
}
2321
resource write_config_b {
22+
description "write config b"
2423
run `echo "config_b" > /tmp/cg_par_test/b.conf`
2524
}
2625
resource write_config_c {
26+
description "write config c"
2727
run `echo "config_c" > /tmp/cg_par_test/c.conf`
2828
}
2929
}
30-
31-
run `echo "all configs written"`
3230
}
33-
34-
# ── Test 2: race block ──────────────────────────────
3531
resource fetch_package {
32+
description "fetch package"
3633
needs build_release
37-
3834
race {
3935
resource try_local_cache {
36+
description "try local cache"
4037
run `test -f /tmp/cg_par_test/release.tar.gz && echo "cache hit"`
4138
}
4239
resource try_remote_mirror {
40+
description "try remote mirror"
4341
run `echo "downloaded from mirror"`
4442
}
4543
}
4644
}
47-
48-
# ── Test 3: each loop ───────────────────────────────
4945
resource distribute_to_fleet {
46+
description "distribute to fleet"
5047
needs build_release
51-
5248
each server in "${servers}", 2 {
5349
resource upload_to_server {
50+
description "upload to ${server}"
5451
run `echo "uploading to ${server}" > /tmp/cg_par_test/${server}.uploaded`
5552
}
5653
}
5754
}
58-
59-
# ── Test 4: stage with phases ───────────────────────
6055
resource rolling_deploy {
56+
description "rolling deploy"
6157
needs distribute_to_fleet, configure_everything, fetch_package
62-
6358
stage "rollout" {
6459
phase "canary" 1 from "${servers}" {
6560
resource deploy_to_server {
61+
description "deploy to ${server}"
6662
run `echo "deployed ${version} to ${server}" > /tmp/cg_par_test/${server}.deployed`
6763
}
6864
verify "canary healthy" {
6965
run `test -f /tmp/cg_par_test/web-1.deployed`
70-
retry 1
66+
timeout 30
67+
on_fail warn
7168
}
7269
}
7370
phase "remaining" rest from "${servers}" {
7471
each server, 2 {
7572
resource deploy_to_server {
73+
description "deploy to ${server}"
7674
run `echo "deployed ${version} to ${server}" > /tmp/cg_par_test/${server}.deployed`
7775
}
7876
}
7977
}
8078
}
8179
}
82-
8380
verify "all servers deployed" {
8481
needs rolling_deploy
8582
run `ls /tmp/cg_par_test/*.deployed | wc -l | grep -q 4`
86-
retry 2
83+
timeout 30
84+
on_fail warn
8785
}
8886
}

test_commandgraph.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5660,6 +5660,59 @@ def test_on_success_failure_combined_with_collect(self):
56605660
assert result.var_bindings.get("action") == "compress"
56615661
assert result.var_bindings.get("do_compress") == "yes"
56625662

5663+
def test_runtime_binding_expands_in_later_run_command(self):
5664+
src = textwrap.dedent("""\
5665+
set has_python = ""
5666+
target "t" local:
5667+
[detect]:
5668+
run $ command -v definitely_not_a_real_binary_for_commandgraph_test
5669+
on success: set has_python = "yes"
5670+
on failure: set has_python = "no"
5671+
if fails ignore
5672+
5673+
[report]:
5674+
first [detect]
5675+
run $ echo "${has_python}"
5676+
""")
5677+
g = _resolve_cgr(src)
5678+
node = cg.HostNode(name="t", via_method="local", via_props={}, resources={})
5679+
5680+
detect = cg.exec_resource(g.all_resources["t.detect"], node, variables=g.variables)
5681+
g.variables.update(detect.var_bindings)
5682+
report = cg.exec_resource(g.all_resources["t.report"], node, variables=g.variables)
5683+
5684+
assert detect.var_bindings.get("has_python") == "no"
5685+
assert report.stdout.strip() == "no"
5686+
5687+
def test_runtime_bindings_replay_from_state_on_resume(self, tmp_path):
5688+
graph_file = tmp_path / "resume_vars.cgr"
5689+
out_file = tmp_path / "report.txt"
5690+
graph_file.write_text(textwrap.dedent(f"""\
5691+
set has_python = ""
5692+
target "t" local:
5693+
[detect]:
5694+
run $ command -v definitely_not_a_real_binary_for_commandgraph_test
5695+
on success: set has_python = "yes"
5696+
on failure: set has_python = "no"
5697+
if fails ignore
5698+
5699+
[report]:
5700+
first [detect]
5701+
run $ echo "${{has_python}}" > {out_file}
5702+
"""))
5703+
5704+
graph = cg._load(str(graph_file), raise_on_error=True)
5705+
cg.cmd_apply_stateful(graph, str(graph_file), max_parallel=1)
5706+
assert out_file.read_text().strip() == "no"
5707+
5708+
out_file.unlink()
5709+
state = cg.StateFile(cg._state_path(str(graph_file)))
5710+
state.drop("t.report")
5711+
5712+
graph = cg._load(str(graph_file), raise_on_error=True)
5713+
cg.cmd_apply_stateful(graph, str(graph_file), max_parallel=1)
5714+
assert out_file.read_text().strip() == "no"
5715+
56635716

56645717
# ══════════════════════════════════════════════════════════════════════════════
56655718
# Gap 3: reduce "key" — aggregate collected outputs

0 commit comments

Comments
 (0)