Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions .github/workflows/validate-workflows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: Validate n8n Workflows

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch: # Allow manual triggering

jobs:
validate-workflows:
name: Validate n8n Workflows
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install dependencies
working-directory: ./lib
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .

- name: Run workflow validation
id: validate
working-directory: ./lib
run: |
# Run the validator on all JSON files in the repository
# This will fail if any workflow is invalid
echo "Validating all n8n workflows..."
if ! n8n-validate ..; then
echo "::error::One or more workflow validations failed"
exit 1
fi
echo "All workflows are valid!"

- name: Create visualization artifacts
if: always() # Run this step even if validation fails
working-directory: ./lib
run: |
echo "Creating visualizations for all workflows..."
mkdir -p ../workflow-visualizations

# Find all JSON files that might be n8n workflows
find .. -type f -name "*.json" -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/workflow-visualizations/*" | while read -r file; do
# Try to validate the file first
if n8n-validate "$file" 2>/dev/null; then
# If validation passes, create a visualization
echo "Creating visualization for $file"
filename=$(basename "$file" .json)
output_file="../workflow-visualizations/${filename}.png"
if ! n8n-visualize "$file" -o "$output_file" --no-show 2>/dev/null; then
echo "::warning::Failed to create visualization for $file"
fi
fi
done

# Count the number of visualizations created
VIS_COUNT=$(find ../workflow-visualizations -type f -name "*.png" | wc -l)
echo "Created $VIS_COUNT workflow visualizations"

# Set an output with the visualization count
echo "visualization_count=$VIS_COUNT" >> $GITHUB_OUTPUT

- name: Upload workflow visualizations
if: always() && steps.validate.outcome == 'success'
uses: actions/upload-artifact@v4
with:
name: workflow-visualizations
path: workflow-visualizations/
if-no-files-found: ignore
retention-days: 7

- name: Comment on PR with validation results
if: github.event_name == 'pull_request' && steps.validate.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const { execSync } = require('child_process');

// Get the list of workflow files that were validated
const workflowFiles = execSync('find .. -type f -name "*.json" -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/workflow-visualizations/*"')
.toString()
.split('\n')
.filter(Boolean);

// Count visualizations
let visCount = 0;
try {
visCount = fs.readdirSync('../workflow-visualizations').length;
} catch (e) {
// Directory might not exist if no visualizations were created
}

// Create a comment
const comment = `βœ… All ${workflowFiles.length} n8n workflow files are valid!\n` +
`πŸ“Š ${visCount} workflow visualizations were generated and attached as artifacts.`;

// Add a comment to the PR
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const botComment = comments.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('n8n workflow')
);

if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment,
});
}
170 changes: 170 additions & 0 deletions lib/examples/visualize_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""
Example script demonstrating how to use the n8n_utils visualization.
"""
import json
import os
import sys
from pathlib import Path

# Add the lib directory to the path so we can import n8n_utils
sys.path.insert(0, str(Path(__file__).parent.parent))

from n8n_utils.visualization import visualize_workflow

def create_sample_workflow():
"""Create a sample n8n workflow for demonstration."""
return {
"name": "Sample Workflow",
"nodes": [
{
"id": "1",
"name": "Start",
"type": "n8n-nodes-base.start",
"typeVersion": 1,
"position": [250, 300],
"parameters": {}
},
{
"id": "2",
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [450, 200],
"parameters": {
"url": "https://api.example.com/data",
"method": "GET"
}
},
{
"id": "3",
"name": "Process Data",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [650, 300],
"parameters": {
"functionCode": "// Process the data here\nreturn items;"
}
},
{
"id": "4",
"name": "Condition",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [850, 300],
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.someField }}",
"operation": "exists"
}
]
}
}
},
{
"id": "5",
"name": "Send Email",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 1,
"position": [1050, 200],
"parameters": {
"to": "user@example.com",
"subject": "Processing Complete",
"text": "The data has been processed successfully."
}
},
{
"id": "6",
"name": "Log Error",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [1050, 400],
"parameters": {
"functionCode": "console.log('Error processing data:', items);\nreturn items;"
}
}
],
"connections": {
"1": {
"main": [
[
{
"node": "2",
"type": "main",
"index": 0
}
]
]
},
"2": {
"main": [
[
{
"node": "3",
"type": "main",
"index": 0
}
]
]
},
"3": {
"main": [
[
{
"node": "4",
"type": "main",
"index": 0
}
]
]
},
"4": {
"main": [
[
{
"node": "5",
"type": "main",
"index": 0
}
]
]
},
"4-1": {
"main": [
[
{
"node": "6",
"type": "main",
"index": 0
}
]
]
}
}
}

def main():
"""Run the example."""
# Create output directory if it doesn't exist
output_dir = Path(__file__).parent / "output"
output_dir.mkdir(exist_ok=True)

# Create a sample workflow
workflow = create_sample_workflow()

# Save the workflow as JSON
workflow_file = output_dir / "sample_workflow.json"
with open(workflow_file, 'w', encoding='utf-8') as f:
json.dump(workflow, f, indent=2)

print(f"Created sample workflow at: {workflow_file}")

# Visualize the workflow
output_image = output_dir / "workflow_visualization.png"
from n8n_utils.visualization.visualizer import visualize_workflow
visualize_workflow(workflow, output_file=str(output_image), show=True)
print(f"Workflow visualization saved to: {output_image}")

if __name__ == "__main__":
main()
8 changes: 8 additions & 0 deletions lib/n8n_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
n8n Utils

A collection of utilities for working with n8n workflows,
including validation and visualization tools.
"""

__version__ = "0.1.0"
15 changes: 15 additions & 0 deletions lib/n8n_utils/ci/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
n8n CI Utilities

This package provides tools for CI/CD integration with n8n workflows,
including validation and testing utilities.
"""

from .validator import validate_workflow, validate_workflow_file, validate_all_workflows, ValidationError

__all__ = [
'validate_workflow',
'validate_workflow_file',
'validate_all_workflows',
'ValidationError',
]
Loading