Skip to content

Commit 478ce42

Browse files
committed
749be7
1 parent acc9026 commit 478ce42

6 files changed

Lines changed: 103 additions & 28 deletions

File tree

COMMANDGRAPH_SPEC.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ A `.cgr` file describes a **directed acyclic graph (DAG)** of shell commands to
2323

2424
## 2. File Structure
2525

26-
A `.cgr` file has four top-level constructs, all optional except `target`:
26+
A `.cgr` file has top-level constructs, all optional except `target`:
2727

2828
```
2929
--- Title --- # optional, decorative only
3030
3131
set VAR = "value" # 0+ variable declarations
3232
set VAR2 = "value2"
3333
34+
gather facts # optional controller-local fact variables
35+
3436
using category/template_name # 0+ template imports
3537
3638
target "node-name" ssh user@host: # 1+ target blocks
@@ -67,6 +69,32 @@ set servers = "web-1,web-2,web-3"
6769
- Variables are expanded at resolve time, not parse time.
6870
- There is no variable interpolation nesting (`${${var}}` is not supported).
6971

72+
#### 2.2.1 Facts (`gather facts`)
73+
74+
```
75+
gather facts
76+
```
77+
78+
`gather facts` populates controller-local variables from the machine running `cgr`. These facts have the lowest variable precedence and can be overridden by inventory variables, graph `set` statements, vars files, secrets, and CLI `--set` values.
79+
80+
Available fact variables:
81+
82+
```
83+
os_name
84+
os_release
85+
os_machine
86+
hostname
87+
arch
88+
os_family
89+
os_pretty
90+
os_version
91+
os_id
92+
cpu_count
93+
memory_mb
94+
python_version
95+
current_user
96+
```
97+
7098
### 2.3 Template Imports (`using`)
7199

72100
```
@@ -743,12 +771,14 @@ The `skip if` command is the key to idempotency. Here are reliable patterns:
743771
## 10. Formal Grammar (PEG-style)
744772

745773
```
746-
program = (title / set_stmt / using_stmt / template_block / target_block)*
774+
program = (title / set_stmt / gather_facts_stmt / using_stmt / template_block / target_block)*
747775
748776
title = "---" TEXT "---"
749777
750778
set_stmt = "set" IDENT "=" QUOTED
751779
780+
gather_facts_stmt = "gather" SPACE "facts"
781+
752782
using_stmt = "using" import_path ("," import_path)*
753783
import_path = IDENT ("/" IDENT)*
754784

MANUAL.md

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ set domain = "example.com"
111111
set port = "443"
112112
```
113113

114+
**Facts** — populate controller-local system variables:
115+
```
116+
gather facts
117+
```
118+
114119
**Imports** — load templates from the repository:
115120
```
116121
using apt/install_package, tls/certbot, nginx/vhost
@@ -595,10 +600,41 @@ cgr apply audit.cgr -i staging.ini
595600

596601
**Variable precedence** (lowest to highest):
597602

598-
1. Inventory file (`[group:vars]`)
599-
2. Graph file (`set name = "value"`)
600-
3. Vars file (`--vars-file`)
601-
4. CLI flags (`--set KEY=VALUE`)
603+
1. Gathered facts (`gather facts`)
604+
2. Inventory file (`[group:vars]`)
605+
3. Graph file (`set name = "value"`)
606+
4. Vars file (`--vars-file`)
607+
5. Secrets file (`secrets "vault.enc"`)
608+
6. CLI flags (`--set KEY=VALUE`)
609+
610+
### Facts
611+
612+
`gather facts` collects facts from the machine running `cgr` and exposes them as normal variables. These are controller-local facts, not per-SSH-target facts. Override them with inventory, `set`, `--vars-file`, secrets, or `--set` when a graph needs target-specific values.
613+
614+
```cgr
615+
gather facts
616+
617+
target "local" local:
618+
[install packages (apt)]:
619+
when os_family == "debian"
620+
run $ apt-get install -y nginx
621+
```
622+
623+
| Variable | Meaning |
624+
|----------|---------|
625+
| `os_name` | Lowercase `uname` system name, such as `linux`, `darwin`, or `freebsd`. |
626+
| `os_release` | OS/kernel release from `uname`. |
627+
| `os_machine` | Machine type from `uname`, such as `x86_64`, `aarch64`, or `arm64`. |
628+
| `hostname` | Local hostname from `uname`. |
629+
| `arch` | CPU architecture from Python `platform.machine()`. |
630+
| `os_family` | Normalized OS family: `debian`, `redhat`, `suse`, `arch`, `alpine`, `darwin`, `unknown`, or an `/etc/os-release` fallback. |
631+
| `os_pretty` | `/etc/os-release` `PRETTY_NAME`, when present. |
632+
| `os_version` | `/etc/os-release` `VERSION_ID`, falling back to Python `platform.version()`. |
633+
| `os_id` | `/etc/os-release` `ID`, falling back to the lowercase `uname` system name. |
634+
| `cpu_count` | CPU count from Python `multiprocessing.cpu_count()`. |
635+
| `memory_mb` | Total memory in MiB from Linux `/proc/meminfo`; `0` when unavailable. |
636+
| `python_version` | Python runtime version used by `cgr`. |
637+
| `current_user` | `USER` or `LOGNAME` from the environment, falling back to `unknown`. |
602638

603639
### How it works
604640

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,12 @@ Categories include: `apt`, `dnf`, `nginx`, `tls`, `firewall`, `systemd`, `servic
488488

489489
## Cross-distro, conditionals, and runtime detection
490490

491+
Use `gather facts` when you want controller-local runtime facts such as `os_family`, `arch`, and `memory_mb` available as variables:
492+
493+
```python
494+
gather facts
495+
```
496+
491497
```python
492498
[install packages (apt)]:
493499
when os_family == "debian"
@@ -512,6 +518,8 @@ Categories include: `apt`, `dnf`, `nginx`, `tls`, `firewall`, `systemd`, `servic
512518

513519
Override anything at runtime: `cgr apply --set os_family=redhat --set version=2.5.0`
514520

521+
Facts are gathered on the machine running `cgr`, not on each SSH target. Available fact variables include `os_name`, `os_release`, `os_machine`, `hostname`, `arch`, `os_family`, `os_pretty`, `os_version`, `os_id`, `cpu_count`, `memory_mb`, `python_version`, and `current_user`. See `MANUAL.md` for the full facts reference and precedence rules.
522+
515523
---
516524

517525
## Encrypted secrets

docs/visualize-demo/ide.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3011,9 +3011,9 @@ <h1>&#9671; CommandGraph</h1>
30113011

30123012

30133013
// ── Static GitHub Pages demo shim ──────────────────────
3014-
const STATIC_DEMO_SOURCE = "--- README-style web app rollout ---\n\ntarget \"demo\" local:\n\n [install nginx]:\n skip if $ test -x /tmp/cgr-pages-demo/bin/nginx\n run $ mkdir -p /tmp/cgr-pages-demo/bin && printf '#!/bin/sh\\nexit 0\\n' > /tmp/cgr-pages-demo/bin/nginx && chmod +x /tmp/cgr-pages-demo/bin/nginx\n\n [write site config]:\n first [install nginx]\n content > /tmp/cgr-pages-demo/nginx.conf:\n server {\n listen 8080;\n server_name demo.local;\n root /tmp/cgr-pages-demo/www;\n index index.html;\n location / { try_files $uri $uri/ =404; }\n }\n validate $ /tmp/cgr-pages-demo/bin/nginx -t\n\n [enable site]:\n first [write site config]\n skip if $ test -L /tmp/cgr-pages-demo/sites-enabled/myapp\n run $ mkdir -p /tmp/cgr-pages-demo/sites-enabled && ln -sf /tmp/cgr-pages-demo/nginx.conf /tmp/cgr-pages-demo/sites-enabled/myapp\n\n [deploy code]:\n first [install nginx]\n skip if $ test -f /tmp/cgr-pages-demo/www/index.html\n run $ mkdir -p /tmp/cgr-pages-demo/www && printf '<h1>Hello from CommandGraph</h1>\\n' > /tmp/cgr-pages-demo/www/index.html\n\n [start nginx]:\n first [enable site], [deploy code]\n run $ printf 'nginx would reload here\\n'\n\n verify \"site is live\":\n first [start nginx]\n run $ test -f /tmp/cgr-pages-demo/www/index.html\n retry 3x wait 2s\n";
3014+
const STATIC_DEMO_SOURCE = "--- Install nginx and basic app ---\n\ntarget \"web\" ssh deploy@10.0.1.5:\n\n [install nginx] as root:\n skip if $ command -v nginx\n run $ apt-get install -y nginx\n\n [write site config] as root:\n first [install nginx]\n content > /etc/nginx/sites-available/myapp:\n server {\n listen 80;\n server_name myapp.example.com;\n root /var/www/myapp;\n index index.html;\n location / { try_files $uri $uri/ =404; }\n }\n validate $ nginx -t\n\n [enable site] as root:\n first [write site config]\n skip if $ test -L /etc/nginx/sites-enabled/myapp\n run $ ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp\n\n [deploy code]:\n first [install nginx]\n skip if $ test -f /var/www/myapp/index.html\n run $ mkdir -p /var/www/myapp && echo \"<h1>Hello World</h1>\" > /var/www/myapp/index.html\n\n [start nginx] as root:\n first [enable site], [deploy code]\n skip if $ systemctl is-active nginx\n run $ systemctl reload-or-restart nginx\n\n verify \"site is live\":\n first [start nginx]\n run $ curl -sf http://localhost/\n retry 3x wait 2s\n";
30153015
const STATIC_DEMO_FILE = "readme-style-webapp.cgr";
3016-
const STATIC_DEMO_RESOLVE = {"graph": {"resources": [{"id": "demo.install_nginx", "short_name": "install_nginx", "node_name": "demo", "description": "install nginx", "needs": [], "check": "test -x /tmp/cgr-pages-demo/bin/nginx", "run": "mkdir -p /tmp/cgr-pages-demo/bin && printf '#!/bin/sh\\nexit 0\\n' > /tmp/cgr-pages-demo/bin/nginx && chmod +x /tmp/cgr-pages-demo/bin/nginx", "script_path": "", "run_as": "", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "4170b424c04edf1a", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 5, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "demo.write_site_config", "short_name": "write_site_config", "node_name": "demo", "description": "write site config", "needs": ["demo.install_nginx"], "check": "", "run": "", "script_path": "", "run_as": "", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "35e3b81f35b769e3", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 9, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "demo.enable_site", "short_name": "enable_site", "node_name": "demo", "description": "enable site", "needs": ["demo.write_site_config"], "check": "test -L /tmp/cgr-pages-demo/sites-enabled/myapp", "run": "mkdir -p /tmp/cgr-pages-demo/sites-enabled && ln -sf /tmp/cgr-pages-demo/nginx.conf /tmp/cgr-pages-demo/sites-enabled/myapp", "script_path": "", "run_as": "", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "6700a9afbc43ce5f", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 21, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "demo.deploy_code", "short_name": "deploy_code", "node_name": "demo", "description": "deploy code", "needs": ["demo.install_nginx"], "check": "test -f /tmp/cgr-pages-demo/www/index.html", "run": "mkdir -p /tmp/cgr-pages-demo/www && printf '<h1>Hello from CommandGraph</h1>\\n' > /tmp/cgr-pages-demo/www/index.html", "script_path": "", "run_as": "", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "12d49ea429d6890e", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 26, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "demo.start_nginx", "short_name": "start_nginx", "node_name": "demo", "description": "start nginx", "needs": ["demo.enable_site", "demo.deploy_code"], "check": "", "run": "printf 'nginx would reload here\\n'", "script_path": "", "run_as": "", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "fe6f71951dad307b", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 31, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "demo.__verify_35", "short_name": "__verify_35", "node_name": "demo", "description": "site is live", "needs": ["demo.start_nginx"], "check": "", "run": "test -f /tmp/cgr-pages-demo/www/index.html", "script_path": "", "run_as": "", "timeout": 30, "timeout_reset_on_output": false, "retries": 3, "retry_delay": 2, "on_fail": "warn", "is_verify": true, "provenance": null, "identity_hash": "48a0a8e3130f2cf5", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 35, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}], "waves": [["demo.install_nginx"], ["demo.deploy_code", "demo.write_site_config"], ["demo.enable_site"], ["demo.start_nginx"], ["demo.__verify_35"]], "nodes": {"demo": {"via": "local", "host": "", "user": "", "port": "22"}}, "provenance": [], "dedup": {}, "variables": {}}, "resources": 6, "waves_count": 5, "waves": 5, "variables": {}, "external_files": []};
3016+
const STATIC_DEMO_RESOLVE = {"graph": {"resources": [{"id": "web.install_nginx", "short_name": "install_nginx", "node_name": "web", "description": "install nginx", "needs": [], "check": "command -v nginx", "run": "apt-get install -y nginx", "script_path": "", "run_as": "root", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "b2db03777c118394", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 5, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "web.write_site_config", "short_name": "write_site_config", "node_name": "web", "description": "write site config", "needs": ["web.install_nginx"], "check": "", "run": "", "script_path": "", "run_as": "root", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "6457dfb1d3c21840", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 9, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "web.enable_site", "short_name": "enable_site", "node_name": "web", "description": "enable site", "needs": ["web.write_site_config"], "check": "test -L /etc/nginx/sites-enabled/myapp", "run": "ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp", "script_path": "", "run_as": "root", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "c84bb034f5144b10", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 21, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "web.deploy_code", "short_name": "deploy_code", "node_name": "web", "description": "deploy code", "needs": ["web.install_nginx"], "check": "test -f /var/www/myapp/index.html", "run": "mkdir -p /var/www/myapp && echo \"<h1>Hello World</h1>\" > /var/www/myapp/index.html", "script_path": "", "run_as": "", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "943bc08c2dd5c6b6", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 26, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "web.start_nginx", "short_name": "start_nginx", "node_name": "web", "description": "start nginx", "needs": ["web.enable_site", "web.deploy_code"], "check": "systemctl is-active nginx", "run": "systemctl reload-or-restart nginx", "script_path": "", "run_as": "root", "timeout": 300, "timeout_reset_on_output": false, "retries": 0, "retry_delay": 5, "on_fail": "stop", "is_verify": false, "provenance": null, "identity_hash": "11e0e7270224bd64", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 31, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}, {"id": "web.__verify_36", "short_name": "__verify_36", "node_name": "web", "description": "site is live", "needs": ["web.start_nginx"], "check": "", "run": "curl -sf http://localhost/", "script_path": "", "run_as": "", "timeout": 30, "timeout_reset_on_output": false, "retries": 3, "retry_delay": 2, "on_fail": "warn", "is_verify": true, "provenance": null, "identity_hash": "40aaecd80f533a1c", "parallel_group": null, "concurrency_limit": null, "is_race": false, "race_into": null, "is_barrier": false, "source_line": 36, "collect_key": null, "http_method": null, "wait_kind": null, "subgraph_path": null}], "waves": [["web.install_nginx"], ["web.deploy_code", "web.write_site_config"], ["web.enable_site"], ["web.start_nginx"], ["web.__verify_36"]], "nodes": {"web": {"via": "ssh", "host": "10.0.1.5", "user": "deploy", "port": "22"}}, "provenance": [], "dedup": {}, "variables": {}}, "resources": 6, "waves_count": 5, "waves": 5, "variables": {}, "external_files": []};
30173017

30183018
function staticDemoNotice() {
30193019
const changed = codeEl.value !== STATIC_DEMO_SOURCE;

0 commit comments

Comments
 (0)