From c165692fe6f4cfd841c13a6939d00664c499e0df Mon Sep 17 00:00:00 2001 From: Roei Bar Aviv Date: Tue, 20 May 2025 13:28:44 +0200 Subject: [PATCH 1/2] Add code logic and CI --- .github/workflows/validate-workflows.yml | 133 ++++++++++ lib/examples/visualize_example.py | 170 +++++++++++++ lib/n8n_utils/__init__.py | 8 + lib/n8n_utils/ci/__init__.py | 15 ++ lib/n8n_utils/ci/validator.py | 158 ++++++++++++ .../tests/test_workflow_validation.py | 79 ++++++ lib/n8n_utils/visualization/__init__.py | 14 ++ lib/n8n_utils/visualization/visualizer.py | 227 ++++++++++++++++++ lib/requirements.txt | 7 + lib/setup.py | 39 +++ setup-ci.sh | 43 ++++ 11 files changed, 893 insertions(+) create mode 100644 .github/workflows/validate-workflows.yml create mode 100644 lib/examples/visualize_example.py create mode 100644 lib/n8n_utils/__init__.py create mode 100644 lib/n8n_utils/ci/__init__.py create mode 100644 lib/n8n_utils/ci/validator.py create mode 100644 lib/n8n_utils/tests/test_workflow_validation.py create mode 100644 lib/n8n_utils/visualization/__init__.py create mode 100644 lib/n8n_utils/visualization/visualizer.py create mode 100644 lib/requirements.txt create mode 100644 lib/setup.py create mode 100755 setup-ci.sh diff --git a/.github/workflows/validate-workflows.yml b/.github/workflows/validate-workflows.yml new file mode 100644 index 0000000..8da1204 --- /dev/null +++ b/.github/workflows/validate-workflows.yml @@ -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@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + 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@v3 + 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@v6 + 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, + }); + } diff --git a/lib/examples/visualize_example.py b/lib/examples/visualize_example.py new file mode 100644 index 0000000..a43bcb3 --- /dev/null +++ b/lib/examples/visualize_example.py @@ -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() diff --git a/lib/n8n_utils/__init__.py b/lib/n8n_utils/__init__.py new file mode 100644 index 0000000..fd7bc1d --- /dev/null +++ b/lib/n8n_utils/__init__.py @@ -0,0 +1,8 @@ +""" +n8n Utils + +A collection of utilities for working with n8n workflows, +including validation and visualization tools. +""" + +__version__ = "0.1.0" diff --git a/lib/n8n_utils/ci/__init__.py b/lib/n8n_utils/ci/__init__.py new file mode 100644 index 0000000..3e15fa3 --- /dev/null +++ b/lib/n8n_utils/ci/__init__.py @@ -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', +] diff --git a/lib/n8n_utils/ci/validator.py b/lib/n8n_utils/ci/validator.py new file mode 100644 index 0000000..1d868f0 --- /dev/null +++ b/lib/n8n_utils/ci/validator.py @@ -0,0 +1,158 @@ +""" +n8n Workflow Validator + +This module provides functionality to validate n8n workflow JSON files +against a schema to ensure they are properly formatted. +""" +import json +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Union + +import jsonschema +from jsonschema import validate + +# Define the n8n workflow JSON schema +N8N_WORKFLOW_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["nodes", "connections"], + "properties": { + "name": {"type": "string"}, + "nodes": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "name", "type", "typeVersion", "position", "parameters"], + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"}, + "type": {"type": "string"}, + "typeVersion": {"type": ["number", "string"]}, + "position": { + "type": "array", + "items": {"type": "number"}, + "minItems": 2, + "maxItems": 2 + }, + "parameters": {"type": "object"}, + }, + }, + }, + "connections": {"type": "object"}, + }, +} + +class ValidationError(Exception): + """Custom exception for validation errors.""" + pass + +def validate_workflow(workflow_data: Dict) -> List[str]: + """ + Validate an n8n workflow against the schema. + + Args: + workflow_data: The parsed JSON data of the workflow + + Returns: + List of error messages, empty if valid + """ + try: + validate(instance=workflow_data, schema=N8N_WORKFLOW_SCHEMA) + return [] + except jsonschema.exceptions.ValidationError as e: + return [f"Validation error: {e.message} at {'.'.join(map(str, e.path))}"] + +def validate_workflow_file(file_path: Union[str, Path]) -> List[str]: + """ + Validate an n8n workflow file. + + Args: + file_path: Path to the JSON file to validate + + Returns: + List of error messages, empty if valid + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + try: + workflow_data = json.load(f) + except json.JSONDecodeError as e: + return [f"Invalid JSON in {file_path}: {str(e)}"] + + return validate_workflow(workflow_data) + except Exception as e: + return [f"Error reading {file_path}: {str(e)}"] + +def find_workflow_files(directory: Union[str, Path]) -> List[Path]: + """ + Recursively find all JSON files in a directory that might be n8n workflows. + + Args: + directory: Directory to search in + + Returns: + List of Path objects to potential workflow files + """ + directory = Path(directory) + return list(directory.glob("**/*.json")) + +def validate_all_workflows(directory: Union[str, Path]) -> Dict[str, List[str]]: + """ + Validate all JSON files in a directory and its subdirectories. + + Args: + directory: Directory to search for workflow files + + Returns: + Dictionary mapping file paths to lists of error messages + """ + workflow_files = find_workflow_files(directory) + results = {} + + for file_path in workflow_files: + errors = validate_workflow_file(file_path) + if errors: + results[str(file_path)] = errors + + return results + +def main(): + """Command-line interface for the validator.""" + import argparse + + parser = argparse.ArgumentParser(description="Validate n8n workflow files.") + parser.add_argument( + "directory", + nargs="?", + default=".", + help="Directory containing n8n workflow files (default: current directory)", + ) + args = parser.parse_args() + + directory = Path(args.directory).resolve() + if not directory.exists(): + print(f"Error: Directory '{directory}' does not exist") + sys.exit(1) + + print(f"Validating n8n workflows in: {directory}") + results = validate_all_workflows(directory) + + if not results: + print("āœ… All workflow files are valid!") + sys.exit(0) + + # Print errors + error_count = 0 + for file_path, errors in results.items(): + print(f"\nāŒ {file_path}:") + for error in errors: + print(f" - {error}") + error_count += 1 + + print(f"\nFound {error_count} error(s) in {len(results)} file(s)") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/lib/n8n_utils/tests/test_workflow_validation.py b/lib/n8n_utils/tests/test_workflow_validation.py new file mode 100644 index 0000000..64fd9aa --- /dev/null +++ b/lib/n8n_utils/tests/test_workflow_validation.py @@ -0,0 +1,79 @@ +"""Tests for n8n workflow validation.""" +import json +import os +import tempfile +import unittest +from pathlib import Path + +from n8n_utils.ci.validator import validate_workflow, ValidationError + +class TestWorkflowValidation(unittest.TestCase): + """Test cases for workflow validation.""" + + def setUp(self): + """Set up test fixtures.""" + self.valid_workflow = { + "name": "Test 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, 300], + "parameters": { + "url": "https://example.com", + "method": "GET" + } + } + ], + "connections": { + "1": { + "main": [ + [ + { + "node": "2", + "type": "main", + "index": 0 + } + ] + ] + } + } + } + + def test_valid_workflow(self): + """Test validation of a valid workflow.""" + errors = validate_workflow(self.valid_workflow) + self.assertEqual(len(errors), 0) + + def test_missing_required_field(self): + """Test validation of a workflow with a missing required field.""" + # Remove a required field + invalid_workflow = self.valid_workflow.copy() + del invalid_workflow["nodes"][0]["id"] + + errors = validate_workflow(invalid_workflow) + self.assertGreater(len(errors), 0) + self.assertIn("id", errors[0]) + + def test_invalid_node_structure(self): + """Test validation of a workflow with invalid node structure.""" + invalid_workflow = self.valid_workflow.copy() + # Make position an invalid type + invalid_workflow["nodes"][0]["position"] = "not an array" + + errors = validate_workflow(invalid_workflow) + self.assertGreater(len(errors), 0) + self.assertIn("position", errors[0]) + +if __name__ == "__main__": + unittest.main() diff --git a/lib/n8n_utils/visualization/__init__.py b/lib/n8n_utils/visualization/__init__.py new file mode 100644 index 0000000..3541d53 --- /dev/null +++ b/lib/n8n_utils/visualization/__init__.py @@ -0,0 +1,14 @@ +""" +n8n Visualization Utilities + +This package provides tools for visualizing n8n workflows, +including graph generation and export capabilities. +""" + +from .visualizer import WorkflowVisualizer, visualize_workflow, visualize_workflow_file + +__all__ = [ + 'WorkflowVisualizer', + 'visualize_workflow', + 'visualize_workflow_file', +] diff --git a/lib/n8n_utils/visualization/visualizer.py b/lib/n8n_utils/visualization/visualizer.py new file mode 100644 index 0000000..d1ad9df --- /dev/null +++ b/lib/n8n_utils/visualization/visualizer.py @@ -0,0 +1,227 @@ +""" +n8n Workflow Visualizer + +This module provides functionality to visualize n8n workflows as graphs. +""" +import json +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Tuple, Union + +import matplotlib.pyplot as plt +import networkx as nx +from matplotlib.patches import BoxStyle, FancyBboxPatch + +class WorkflowVisualizer: + """Class for visualizing n8n workflows.""" + + def __init__(self, workflow_data: Dict): + """Initialize with workflow data.""" + self.workflow = workflow_data + self.graph = nx.DiGraph() + self.pos = {} + self.node_colors = [] + self.node_sizes = [] + + # Color mapping for different node types + self.type_colors = { + 'n8n-nodes-base.start': '#ff9a9c', + 'n8n-nodes-base.httpRequest': '#a4c2f4', + 'n8n-nodes-base.if': '#ffcc80', + 'n8n-nodes-base.function': '#d5a6bd', + 'n8n-nodes-base.set': '#b4a7d6', + 'n8n-nodes-base.code': '#a2c4c9', + 'n8n-nodes-base.moveBinaryData': '#b6d7a8', + 'n8n-nodes-base.spreadsheetFile': '#f9cb9c', + 'n8n-nodes-base.switch': '#ea9999', + 'n8n-nodes-base.merge': '#9fc5e8', + 'n8n-nodes-base.noOp': '#cccccc', + } + + def parse_workflow(self): + """Parse the workflow data and build the graph.""" + # Add nodes + for node in self.workflow.get('nodes', []): + node_id = node['id'] + node_type = node['type'] + node_name = node.get('name', node_type.split('.')[-1]) + + # Add node to graph + self.graph.add_node(node_id, + label=node_name, + type=node_type, + parameters=node.get('parameters', {})) + + # Store position if available + if 'position' in node and len(node['position']) >= 2: + self.pos[node_id] = (node['position'][0], -node['position'][1]) # Invert y for visualization + + # Add edges based on connections + connections = self.workflow.get('connections', {}) + for source_node_id, source_connections in connections.items(): + for connection_type, connection_list in source_connections.items(): + for connection in connection_list: + target_node_id = connection['node'] + self.graph.add_edge(source_node_id, target_node_id, + connection_type=connection_type) + + def _get_node_color(self, node_type: str) -> str: + """Get color for a node based on its type.""" + return self.type_colors.get(node_type, '#cccccc') + + def _get_node_label(self, node_data: Dict) -> str: + """Generate a label for a node.""" + label = node_data.get('label', '') + # Truncate long labels + if len(label) > 20: + return f"{label[:18]}..." + return label + + def draw(self, output_file: Optional[str] = None, show: bool = True): + """Draw the workflow graph. + + Args: + output_file: If provided, save the figure to this file + show: If True, display the figure + """ + if not self.pos: + # If no positions are defined, use a layout algorithm + self.pos = nx.spring_layout(self.graph, k=0.5, iterations=50) + + plt.figure(figsize=(16, 12)) + + # Draw edges first (behind nodes) + nx.draw_networkx_edges( + self.graph, + self.pos, + edge_color='#888888', + arrows=True, + arrowstyle='-|>', + arrowsize=15, + node_size=2000, + width=1.5, + connectionstyle="arc3,rad=0.1" + ) + + # Draw nodes with colors based on type + for node_id, node_data in self.graph.nodes(data=True): + node_type = node_data.get('type', 'unknown') + color = self._get_node_color(node_type) + + # Draw node + nx.draw_networkx_nodes( + self.graph, + self.pos, + nodelist=[node_id], + node_color=color, + node_size=2000, + edgecolors='#333333', + linewidths=1, + alpha=0.9 + ) + + # Add node type as a small label below the node + plt.text( + self.pos[node_id][0], + self.pos[node_id][1] - 0.07, + node_type.split('.')[-1], + fontsize=6, + ha='center', + va='top', + color='#555555' + ) + + # Draw node labels + labels = {n: d['label'] for n, d in self.graph.nodes(data=True)} + nx.draw_networkx_labels( + self.graph, + self.pos, + labels=labels, + font_size=8, + font_weight='bold', + font_color='#000000', + font_family='sans-serif' + ) + + # Add edge labels (connection types) + edge_labels = {(u, v): d.get('connection_type', '') + for u, v, d in self.graph.edges(data=True)} + nx.draw_networkx_edge_labels( + self.graph, + self.pos, + edge_labels=edge_labels, + font_size=7, + font_color='#666666', + label_pos=0.5 + ) + + plt.title(f"Workflow: {self.workflow.get('name', 'Untitled')}", fontsize=14) + plt.axis('off') + plt.tight_layout() + + if output_file: + plt.savefig(output_file, dpi=150, bbox_inches='tight') + print(f"Workflow visualization saved to {output_file}") + + if show: + plt.show() + else: + plt.close() + +def visualize_workflow(workflow_data: Dict, output_file: Optional[str] = None, show: bool = True): + """Visualize an n8n workflow. + + Args: + workflow_data: The workflow data as a dictionary + output_file: If provided, save the visualization to this file + show: If True, display the visualization + """ + visualizer = WorkflowVisualizer(workflow_data) + visualizer.parse_workflow() + visualizer.draw(output_file=output_file, show=show) + +def visualize_workflow_file(workflow_file: Union[str, Path], output_file: Optional[str] = None, show: bool = True): + """Visualize an n8n workflow from a JSON file. + + Args: + workflow_file: Path to the workflow JSON file + output_file: If provided, save the visualization to this file + show: If True, display the visualization + """ + with open(workflow_file, 'r', encoding='utf-8') as f: + workflow_data = json.load(f) + + if not output_file and not show: + base_name = os.path.splitext(os.path.basename(workflow_file))[0] + output_file = f"{base_name}_visualization.png" + + visualize_workflow(workflow_data, output_file=output_file, show=show) + +def main(): + """Command-line interface for the visualizer.""" + import argparse + + parser = argparse.ArgumentParser(description='Visualize n8n workflows.') + parser.add_argument('workflow_file', help='Path to the n8n workflow JSON file') + parser.add_argument('-o', '--output', help='Output file path for the visualization') + parser.add_argument('--no-show', action='store_true', help='Do not display the visualization') + + args = parser.parse_args() + + if not os.path.exists(args.workflow_file): + print(f"Error: File '{args.workflow_file}' not found") + sys.exit(1) + + try: + visualize_workflow_file( + args.workflow_file, + output_file=args.output, + show=not args.no_show + ) + except Exception as e: + print(f"Error visualizing workflow: {str(e)}") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/lib/requirements.txt b/lib/requirements.txt new file mode 100644 index 0000000..4b0c0e9 --- /dev/null +++ b/lib/requirements.txt @@ -0,0 +1,7 @@ +jsonschema>=4.0.0 +networkx>=2.6.3 +matplotlib>=3.4.3 +pytest>=7.0.0 +pytest-cov>=3.0.0 +click>=8.0.0 +python-dotenv>=0.19.0 diff --git a/lib/setup.py b/lib/setup.py new file mode 100644 index 0000000..eb0b1fe --- /dev/null +++ b/lib/setup.py @@ -0,0 +1,39 @@ +from setuptools import setup, find_packages +import os + +# Get the long description from the README file +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, '..', 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name="n8n_utils", + version="0.1.0", + packages=find_packages(where='.'), + package_dir={'': '.'}, + install_requires=[ + "jsonschema>=4.0.0", + "networkx>=2.6.3", + "matplotlib>=3.4.3", + "click>=8.0.0", + "python-dotenv>=0.19.0", + ], + entry_points={ + 'console_scripts': [ + 'n8n-validate=n8n_utils.ci.validator:main', + 'n8n-visualize=n8n_utils.visualization.visualizer:main', + ], + }, + python_requires='>=3.8', + author="Your Name", + author_email="your.email@example.com", + description="Utilities for n8n workflow validation and visualization", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/yourusername/n8n-free-templates", + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], +) diff --git a/setup-ci.sh b/setup-ci.sh new file mode 100755 index 0000000..96f5cb8 --- /dev/null +++ b/setup-ci.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Setup script for n8n workflow validation and visualization + +set -e + +# Check if Python is installed +if ! command -v python3 &> /dev/null; then + echo "Python 3 is required but not installed. Please install Python 3 and try again." + exit 1 +fi + +# Create and activate a virtual environment +echo "Creating Python virtual environment..." +python3 -m venv venv +source venv/bin/activate + +# Install dependencies +echo "Installing dependencies..." +pip install --upgrade pip +cd lib +pip install -r requirements.txt +pip install -e . +cd .. + +echo "" +echo "āœ… Setup complete!" +echo "To activate the virtual environment, run:" +echo " source venv/bin/activate" +echo "" +echo "To validate all n8n workflows, run:" +echo " cd lib && n8n-validate .. && cd .." +echo "" +echo "To visualize a workflow, run:" +echo " cd lib && n8n-visualize ../path/to/workflow.json && cd .." + +# Run validation if this is a CI environment +if [ "$CI" = "true" ]; then + echo "Running in CI environment. Validating workflows..." + cd lib + n8n-validate .. + echo "āœ… All workflows are valid!" + cd .. +fi From 5a9f76aafa755400b81c59ae7c70426004dd88f9 Mon Sep 17 00:00:00 2001 From: Roei Bar Aviv Date: Tue, 20 May 2025 13:31:41 +0200 Subject: [PATCH 2/2] Update GitHub Actions and improve setup script --- .github/workflows/validate-workflows.yml | 8 ++++---- setup-ci.sh | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate-workflows.yml b/.github/workflows/validate-workflows.yml index 8da1204..497139f 100644 --- a/.github/workflows/validate-workflows.yml +++ b/.github/workflows/validate-workflows.yml @@ -14,10 +14,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' @@ -71,7 +71,7 @@ jobs: - name: Upload workflow visualizations if: always() && steps.validate.outcome == 'success' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: workflow-visualizations path: workflow-visualizations/ @@ -80,7 +80,7 @@ jobs: - name: Comment on PR with validation results if: github.event_name == 'pull_request' && steps.validate.outcome == 'success' - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const fs = require('fs'); diff --git a/setup-ci.sh b/setup-ci.sh index 96f5cb8..c3970fe 100755 --- a/setup-ci.sh +++ b/setup-ci.sh @@ -14,13 +14,19 @@ echo "Creating Python virtual environment..." python3 -m venv venv source venv/bin/activate +# Get the directory of this script +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +LIB_DIR="${SCRIPT_DIR}/lib" + # Install dependencies echo "Installing dependencies..." pip install --upgrade pip -cd lib + +# Change to lib directory and install the package +pushd "$LIB_DIR" > /dev/null pip install -r requirements.txt pip install -e . -cd .. +popd > /dev/null echo "" echo "āœ… Setup complete!" @@ -36,8 +42,11 @@ echo " cd lib && n8n-visualize ../path/to/workflow.json && cd .." # Run validation if this is a CI environment if [ "$CI" = "true" ]; then echo "Running in CI environment. Validating workflows..." - cd lib - n8n-validate .. + pushd "$LIB_DIR" > /dev/null + if ! n8n-validate ..; then + echo "āŒ Workflow validation failed" + exit 1 + fi echo "āœ… All workflows are valid!" - cd .. + popd > /dev/null fi