Skip to content

Commit 9e284c4

Browse files
authored
Adding initial version of Cirro validation to CI/CD (#276)
* Adding initial version of Cirro validation to CI/CD * Adding Cirro validation to CONTRIBUTING.md * Adding permissions to linting.yml
1 parent 724fe90 commit 9e284c4

File tree

4 files changed

+199
-1
lines changed

4 files changed

+199
-1
lines changed

.github/CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ Pipelines may include optional platform-specific configuration directories for e
175175

176176
Platform configurations are entirely optional and should not be required to run the pipeline with standard WDL executors (Cromwell, miniWDL, Sprocket).
177177

178+
**Cirro Configuration Validation**: Pipelines with `.cirro/` directories are automatically validated in CI. The validation checks that all required files are present (`preprocess.py`, `process-form.json`, `process-input.json`, `process-output.json`, `process-compute.config`), JSON files are valid, and `preprocess.py` has no syntax errors. You can run this locally with `make lint_cirro`.
179+
178180
## Testing Requirements
179181

180182
### Local Tests
@@ -256,6 +258,7 @@ All contributions must pass our automated testing pipeline which executes on a P
256258
- **Container verification**: All Docker images must be accessible and functional
257259
- **Syntax validation**: WDL syntax and structure validation
258260
- **Integration testing**: Cross-module compatibility testing
261+
- **Cirro validation**: Validates `.cirro/` configurations for pipelines that include them
259262

260263
## Documentation Website
261264

.github/scripts/validate_cirro.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Validate .cirro configurations in WILDS pipelines.
4+
5+
Checks that .cirro directories contain the required files,
6+
JSON files are valid, and preprocess.py has no syntax errors.
7+
"""
8+
9+
import json
10+
import sys
11+
from pathlib import Path
12+
13+
REQUIRED_FILES = [
14+
"preprocess.py",
15+
"process-form.json",
16+
"process-input.json",
17+
"process-output.json",
18+
"process-compute.config",
19+
]
20+
21+
def validate_json_file(filepath):
22+
"""Validate that a file contains valid JSON. Returns list of error strings."""
23+
errors = []
24+
try:
25+
with open(filepath) as f:
26+
data = json.load(f)
27+
except json.JSONDecodeError as e:
28+
errors.append(f" Invalid JSON in {filepath.name}: {e}")
29+
return errors, None
30+
return errors, data
31+
32+
33+
def validate_form(filepath):
34+
"""Validate process-form.json has expected structure."""
35+
errors, data = validate_json_file(filepath)
36+
if data is None:
37+
return errors
38+
39+
if not isinstance(data, dict):
40+
errors.append(f" {filepath.name}: expected a JSON object at top level")
41+
return errors
42+
43+
if "form" not in data:
44+
errors.append(f" {filepath.name}: missing top-level 'form' key")
45+
return errors
46+
47+
form = data["form"]
48+
if not isinstance(form, dict):
49+
errors.append(f" {filepath.name}: 'form' should be an object")
50+
return errors
51+
52+
if "properties" not in form:
53+
errors.append(f" {filepath.name}: 'form' missing 'properties' key")
54+
55+
if "required" in form and not isinstance(form["required"], list):
56+
errors.append(f" {filepath.name}: 'required' should be a list")
57+
58+
return errors
59+
60+
61+
def validate_input(filepath):
62+
"""Validate process-input.json has JSON path mappings."""
63+
errors, data = validate_json_file(filepath)
64+
if data is None:
65+
return errors
66+
67+
if not isinstance(data, dict):
68+
errors.append(f" {filepath.name}: expected a JSON object")
69+
return errors
70+
71+
for key, value in data.items():
72+
if not isinstance(value, str):
73+
errors.append(f" {filepath.name}: value for '{key}' should be a string, got {type(value).__name__}")
74+
elif not value.startswith("$."):
75+
errors.append(f" {filepath.name}: value for '{key}' should be a JSON path (start with '$.')")
76+
77+
return errors
78+
79+
80+
def validate_output(filepath):
81+
"""Validate process-output.json is valid JSON."""
82+
errors, _ = validate_json_file(filepath)
83+
return errors
84+
85+
86+
def validate_preprocess(filepath):
87+
"""Validate preprocess.py has no syntax errors."""
88+
errors = []
89+
try:
90+
source = filepath.read_text()
91+
compile(source, str(filepath), "exec")
92+
except SyntaxError as e:
93+
errors.append(f" {filepath.name}: Python syntax error: {e}")
94+
return errors
95+
96+
97+
def validate_cirro_dir(cirro_dir):
98+
"""Validate a single .cirro directory. Returns list of error strings."""
99+
errors = []
100+
101+
# Check required files
102+
for filename in REQUIRED_FILES:
103+
if not (cirro_dir / filename).exists():
104+
errors.append(f" Missing required file: {filename}")
105+
106+
# Validate individual files
107+
form_path = cirro_dir / "process-form.json"
108+
if form_path.exists():
109+
errors.extend(validate_form(form_path))
110+
111+
input_path = cirro_dir / "process-input.json"
112+
if input_path.exists():
113+
errors.extend(validate_input(input_path))
114+
115+
output_path = cirro_dir / "process-output.json"
116+
if output_path.exists():
117+
errors.extend(validate_output(output_path))
118+
119+
preprocess_path = cirro_dir / "preprocess.py"
120+
if preprocess_path.exists():
121+
errors.extend(validate_preprocess(preprocess_path))
122+
123+
return errors
124+
125+
126+
def main():
127+
pipelines_dir = Path("pipelines")
128+
if not pipelines_dir.exists():
129+
print("No pipelines directory found")
130+
return 0
131+
132+
found_any = False
133+
all_errors = {}
134+
135+
for pipeline_dir in sorted(pipelines_dir.iterdir()):
136+
if not pipeline_dir.is_dir():
137+
continue
138+
139+
cirro_dir = pipeline_dir / ".cirro"
140+
if not cirro_dir.is_dir():
141+
print(f"Skipping {pipeline_dir.name} (no .cirro directory)")
142+
continue
143+
144+
found_any = True
145+
print(f"Validating {pipeline_dir.name}/.cirro/ ...")
146+
errors = validate_cirro_dir(cirro_dir)
147+
148+
if errors:
149+
all_errors[pipeline_dir.name] = errors
150+
print(f" FAIL ({len(errors)} issue(s))")
151+
else:
152+
print(f" OK")
153+
154+
if not found_any:
155+
print("No .cirro directories found in any pipeline")
156+
return 0
157+
158+
if all_errors:
159+
print(f"\n{'='*50}")
160+
print(f"Cirro validation failed for {len(all_errors)} pipeline(s):\n")
161+
for pipeline, errors in all_errors.items():
162+
print(f"{pipeline}:")
163+
for error in errors:
164+
print(error)
165+
print()
166+
return 1
167+
168+
print(f"\nAll Cirro configurations valid!")
169+
return 0
170+
171+
172+
if __name__ == "__main__":
173+
sys.exit(main())

.github/workflows/linting.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ on:
88
pull_request:
99
types: [opened, reopened, synchronize]
1010

11+
permissions:
12+
contents: read
13+
1114
jobs:
1215
miniwdl_check:
1316
runs-on: ubuntu-latest
@@ -80,3 +83,18 @@ jobs:
8083
uses: stjude-rust-labs/sprocket-action@v0.10.0
8184
with:
8285
action: lint
86+
87+
cirro_validation:
88+
runs-on: ubuntu-latest
89+
steps:
90+
-
91+
name: Checkout
92+
uses: actions/checkout@v4
93+
-
94+
name: Set up Python
95+
uses: actions/setup-python@v5
96+
with:
97+
python-version: 3.13
98+
-
99+
name: Validate Cirro Configurations
100+
run: python .github/scripts/validate_cirro.py

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ lint_womtool: check_java check_womtool check_name ## Run WOMtool validate on mod
146146
done
147147

148148

149-
lint: lint_sprocket lint_miniwdl lint_womtool ## Run all linting checks
149+
lint_cirro: ## Validate .cirro configurations in pipelines
150+
@echo "Validating Cirro configurations..."
151+
@python3 .github/scripts/validate_cirro.py
152+
153+
lint: lint_sprocket lint_miniwdl lint_womtool lint_cirro ## Run all linting checks
150154

151155
##@ Run
152156

0 commit comments

Comments
 (0)