nautobot-workflow-engine 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. nautobot_workflow_engine-1.0.1/PKG-INFO +88 -0
  2. nautobot_workflow_engine-1.0.1/README.md +67 -0
  3. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/__init__.py +35 -0
  4. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/app-config-schema.json +31 -0
  5. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/datasources.py +257 -0
  6. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/executors.py +1138 -0
  7. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/filters.py +23 -0
  8. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/forms.py +745 -0
  9. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/jobs.py +211 -0
  10. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/migrations/0001_initial.py +152 -0
  11. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/migrations/__init__.py +0 -0
  12. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/models.py +271 -0
  13. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/navigation.py +36 -0
  14. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/scheduling.py +184 -0
  15. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/servicenow.py +348 -0
  16. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/static/nautobot_workflow_engine/js/workflow_list.js +116 -0
  17. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tables.py +22 -0
  18. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tasks.py +135 -0
  19. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/partials/workflow_list_grouped.html +92 -0
  20. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/partials/workflow_tiles.html +129 -0
  21. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_launch.html +2972 -0
  22. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_list.html +144 -0
  23. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_report.html +560 -0
  24. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_run.html +535 -0
  25. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templates/nautobot_workflow_engine/workflow_run_list.html +331 -0
  26. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templatetags/__init__.py +2 -0
  27. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/templatetags/workflow_extras.py +38 -0
  28. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/__init__.py +1 -0
  29. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_action_ordering.py +102 -0
  30. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_basic.py +30 -0
  31. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_executors.py +137 -0
  32. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_forms_listdict.py +110 -0
  33. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_repo_filters.py +143 -0
  34. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/tests/test_servicenow_scheduling.py +291 -0
  35. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/urls.py +27 -0
  36. nautobot_workflow_engine-1.0.1/nautobot_workflow_engine/views.py +1381 -0
  37. nautobot_workflow_engine-1.0.1/pyproject.toml +269 -0
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: nautobot-workflow-engine
3
+ Version: 1.0.1
4
+ Summary: Workflow Engine
5
+ Keywords: nautobot,nautobot-app,nautobot-plugin
6
+ Author: Eric Fetty
7
+ Requires-Python: >=3.10,<3.14
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Provides-Extra: all
16
+ Requires-Dist: jinja2 (>=3.1)
17
+ Requires-Dist: nautobot (>=3.0.0,<4.0.0)
18
+ Requires-Dist: pyyaml (>=6.0)
19
+ Requires-Dist: requests (>=2.31)
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Nautobot Workflow Engine
23
+
24
+ Define automation in YAML, synchronize it through Nautobot's repository integration,
25
+ and launch it from dynamic forms. Execute Python scripts in Nautobot or job templates
26
+ in AWX, with ordered actions, reusable results, scheduling, and run history.
27
+
28
+ ## Capabilities
29
+
30
+ - Typed inputs, Nautobot object lookups, dependent choices, conditional visibility,
31
+ validation, and tabular CSV input.
32
+ - Pre-actions, main actions, and post-actions in definition order.
33
+ - Reusable Python helpers and repository-backed Jinja filters.
34
+ - Immediate or scheduled runs, including optional ServiceNow change windows.
35
+ - Execution logs, AWX job monitoring, reporting, and estimated time savings.
36
+
37
+ ## Get Started
38
+
39
+ The current package requires **Nautobot 3.x** and **Python 3.10 through 3.13**. The
40
+ selected Nautobot release must also support that Python version. These are package
41
+ requirements, not a claim that every combination has been tested.
42
+
43
+ 1. Follow [installation and configuration](docs/admin/install.md) using an approved
44
+ package in your Nautobot deployment.
45
+ 2. Configure a trusted workflow source and synchronize its Workflow Engine content.
46
+ 3. Run the non-destructive [hostname tutorial](docs/user/app_getting_started.md).
47
+ 4. Use the [authoring reference](docs/user/app_use_cases.md) to add inputs and actions.
48
+
49
+ AWX and ServiceNow are optional. Individual workflows may require additional apps,
50
+ Python packages, inventory, or service permissions. Examples are starting points for
51
+ review and adaptation, not production-ready deployment definitions.
52
+
53
+ ## Documentation
54
+
55
+ - [Overview](docs/user/app_overview.md) and [documentation home](docs/index.md).
56
+ - [Workflow authoring](docs/user/app_use_cases.md) and [custom filters](docs/dev/extending.md).
57
+ - [AWX and ServiceNow](docs/user/external_interactions.md).
58
+ - [Compatibility](docs/admin/compatibility_matrix.md), [upgrades](docs/admin/upgrade.md),
59
+ and [release notes](docs/admin/release_notes/index.md).
60
+ - [Security and data handling](docs/admin/security.md).
61
+
62
+ ## Examples
63
+
64
+ The [custom-filter workflow](examples/workflows/custom-filter-demo.yaml) uses
65
+ [an echo script](examples/scripts/echo_hostname.py) and shared helpers without changing
66
+ inventory. Other examples illustrate inventory operations and optional vendor
67
+ integrations. Review their model, script, and service dependencies before use.
68
+
69
+ ## Trust and Execution Logs
70
+
71
+ Workflow scripts and filters are trusted code running inside Nautobot, not sandboxed
72
+ submissions. Limit who can modify their source and who can launch or inspect runs.
73
+ Detailed script execution logging is intentional. Treat inputs, results, output,
74
+ diagnostics, and backups as operational data; do not submit or print credentials.
75
+
76
+ ## Contributing
77
+
78
+ Read the [contribution guide](docs/dev/contributing.md),
79
+ [development environment guide](docs/dev/dev_environment.md), and
80
+ [architecture overview](docs/dev/arch_decision.md). Keep changes focused, use synthetic
81
+ fixtures, preserve migrations, and include regression coverage for behavioral changes.
82
+
83
+ ## Publication Status
84
+
85
+ Open-source preparation is in progress. Apache-2.0 is the intended license, subject
86
+ to copyright-owner approval and a provenance review. No project-wide license grant
87
+ is asserted here until those steps are complete. A private vulnerability-reporting
88
+ contact and final artifact validation are also required before public release.
@@ -0,0 +1,67 @@
1
+ # Nautobot Workflow Engine
2
+
3
+ Define automation in YAML, synchronize it through Nautobot's repository integration,
4
+ and launch it from dynamic forms. Execute Python scripts in Nautobot or job templates
5
+ in AWX, with ordered actions, reusable results, scheduling, and run history.
6
+
7
+ ## Capabilities
8
+
9
+ - Typed inputs, Nautobot object lookups, dependent choices, conditional visibility,
10
+ validation, and tabular CSV input.
11
+ - Pre-actions, main actions, and post-actions in definition order.
12
+ - Reusable Python helpers and repository-backed Jinja filters.
13
+ - Immediate or scheduled runs, including optional ServiceNow change windows.
14
+ - Execution logs, AWX job monitoring, reporting, and estimated time savings.
15
+
16
+ ## Get Started
17
+
18
+ The current package requires **Nautobot 3.x** and **Python 3.10 through 3.13**. The
19
+ selected Nautobot release must also support that Python version. These are package
20
+ requirements, not a claim that every combination has been tested.
21
+
22
+ 1. Follow [installation and configuration](docs/admin/install.md) using an approved
23
+ package in your Nautobot deployment.
24
+ 2. Configure a trusted workflow source and synchronize its Workflow Engine content.
25
+ 3. Run the non-destructive [hostname tutorial](docs/user/app_getting_started.md).
26
+ 4. Use the [authoring reference](docs/user/app_use_cases.md) to add inputs and actions.
27
+
28
+ AWX and ServiceNow are optional. Individual workflows may require additional apps,
29
+ Python packages, inventory, or service permissions. Examples are starting points for
30
+ review and adaptation, not production-ready deployment definitions.
31
+
32
+ ## Documentation
33
+
34
+ - [Overview](docs/user/app_overview.md) and [documentation home](docs/index.md).
35
+ - [Workflow authoring](docs/user/app_use_cases.md) and [custom filters](docs/dev/extending.md).
36
+ - [AWX and ServiceNow](docs/user/external_interactions.md).
37
+ - [Compatibility](docs/admin/compatibility_matrix.md), [upgrades](docs/admin/upgrade.md),
38
+ and [release notes](docs/admin/release_notes/index.md).
39
+ - [Security and data handling](docs/admin/security.md).
40
+
41
+ ## Examples
42
+
43
+ The [custom-filter workflow](examples/workflows/custom-filter-demo.yaml) uses
44
+ [an echo script](examples/scripts/echo_hostname.py) and shared helpers without changing
45
+ inventory. Other examples illustrate inventory operations and optional vendor
46
+ integrations. Review their model, script, and service dependencies before use.
47
+
48
+ ## Trust and Execution Logs
49
+
50
+ Workflow scripts and filters are trusted code running inside Nautobot, not sandboxed
51
+ submissions. Limit who can modify their source and who can launch or inspect runs.
52
+ Detailed script execution logging is intentional. Treat inputs, results, output,
53
+ diagnostics, and backups as operational data; do not submit or print credentials.
54
+
55
+ ## Contributing
56
+
57
+ Read the [contribution guide](docs/dev/contributing.md),
58
+ [development environment guide](docs/dev/dev_environment.md), and
59
+ [architecture overview](docs/dev/arch_decision.md). Keep changes focused, use synthetic
60
+ fixtures, preserve migrations, and include regression coverage for behavioral changes.
61
+
62
+ ## Publication Status
63
+
64
+ Open-source preparation is in progress. Apache-2.0 is the intended license, subject
65
+ to copyright-owner approval and a provenance review. No project-wide license grant
66
+ is asserted here until those steps are complete. A private vulnerability-reporting
67
+ contact and final artifact validation are also required before public release.
@@ -0,0 +1,35 @@
1
+ """App declaration for nautobot_workflow_engine."""
2
+
3
+ # Metadata is inherited from Nautobot. If not including Nautobot in the environment, this should be added
4
+ import os
5
+ from importlib import metadata
6
+
7
+ from nautobot.apps import NautobotAppConfig
8
+
9
+ __version__ = metadata.version(__name__)
10
+
11
+
12
+ class WorkflowEngineConfig(NautobotAppConfig):
13
+ """App configuration for the nautobot_workflow_engine app."""
14
+
15
+ name = "nautobot_workflow_engine"
16
+ verbose_name = "Workflow Engine"
17
+ version = __version__
18
+ author = "Eric Fetty"
19
+ description = "General-purpose workflow engine for Nautobot"
20
+ base_url = "workflow-engine"
21
+ required_settings = []
22
+ default_settings = {
23
+ "awx_external_integration_name": "AWX",
24
+ "workflow_dir": "workflows",
25
+ "filters_dir": "filters",
26
+ "awx_poll_seconds": 5,
27
+ "show_disabled": False,
28
+ "show_add_new_item": os.getenv("SHOW_ADD_NEW_ITEM", "").split(","),
29
+ }
30
+ caching_config = {}
31
+ docs_view_name = "plugins:nautobot_workflow_engine:docs"
32
+ searchable_models = ["workflow"]
33
+
34
+
35
+ config = WorkflowEngineConfig # pylint:disable=invalid-name
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "awx_external_integration_name": {
6
+ "type": "string",
7
+ "default": "AWX"
8
+ },
9
+ "workflow_dir": {
10
+ "type": "string",
11
+ "default": "workflows"
12
+ },
13
+ "filters_dir": {
14
+ "type": "string",
15
+ "default": "filters"
16
+ },
17
+ "awx_poll_seconds": {
18
+ "type": "integer",
19
+ "default": 5
20
+ },
21
+ "show_disabled": {
22
+ "type": "boolean",
23
+ "default": false
24
+ },
25
+ "show_add_new_item": {
26
+ "type": "array",
27
+ "default": []
28
+ }
29
+ },
30
+ "additionalProperties": false
31
+ }
@@ -0,0 +1,257 @@
1
+ """Git repository sync functionality for workflows."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ import yaml
7
+ from django.conf import settings
8
+ from django.contrib.contenttypes.models import ContentType
9
+ from nautobot.apps.datasources import DatasourceContent
10
+ from nautobot.extras.models import GitRepository
11
+
12
+ from .executors import clear_repo_script_cache, validate_repo_filter_modules
13
+ from .models import Workflow, WorkflowAction, WorkflowFilterModule, WorkflowHelper, WorkflowInput
14
+
15
+ PLUGIN_CFG = settings.PLUGINS_CONFIG.get("nautobot_workflow_engine", {})
16
+
17
+
18
+ def _process_includes(job_result, file_handler):
19
+ """Process any includes in the content."""
20
+ content = file_handler.read_text(encoding="utf-8")
21
+ include_pattern = re.compile(r"""^\s*{%\s*include\s+[\"'](?P<path>[\w\/\.\-]+)[\"']\s*%}""")
22
+ for line in content.splitlines():
23
+ match = include_pattern.search(line)
24
+ if match:
25
+ # Load the template path from the match
26
+ template_path = match.group("path")
27
+ template_file = file_handler.parent / template_path
28
+ if template_file.exists():
29
+ # If the template file exists, read its content and replace the include
30
+ template_content = template_file.read_text(encoding="utf-8")
31
+ content = content.replace(line, template_content)
32
+ else:
33
+ # If the template file doesn't exist, log a warning
34
+ job_result.log(f"Template file not found: {template_file}")
35
+
36
+ return content
37
+
38
+
39
+ def _get_action_list(data, *keys):
40
+ """Return first non-empty action list for any provided YAML key alias."""
41
+ for key in keys:
42
+ value = data.get(key)
43
+ if value is None:
44
+ continue
45
+ if isinstance(value, list):
46
+ return value
47
+ # Be forgiving if user accidentally provides an empty scalar/object.
48
+ return []
49
+ return []
50
+
51
+
52
+ def _is_filter_module_path(relative_path):
53
+ """Return True when the repository-relative path is inside the configured filters directory."""
54
+ filters_dir = Path(PLUGIN_CFG.get("filters_dir", "filters"))
55
+ filters_parts = filters_dir.parts
56
+ rel_parts = relative_path.parts
57
+ return len(rel_parts) >= len(filters_parts) and rel_parts[: len(filters_parts)] == filters_parts
58
+
59
+
60
+ def _sync_repo_python_modules(repository_record, repo_path, job_result):
61
+ """Store repository Python modules in helper or filter-module models."""
62
+ WorkflowHelper.objects.filter(source_repo=repository_record).delete()
63
+ WorkflowFilterModule.objects.filter(source_repo=repository_record).delete()
64
+
65
+ for py_file in repo_path.rglob("*.py"):
66
+ if "__pycache__" in str(py_file) or py_file.name.startswith("."):
67
+ continue
68
+
69
+ relative_path = py_file.relative_to(repo_path)
70
+ model = WorkflowFilterModule if _is_filter_module_path(relative_path) else WorkflowHelper
71
+ model.objects.create(
72
+ source_repo=repository_record,
73
+ file_path=str(relative_path),
74
+ content=py_file.read_text(encoding="utf-8"),
75
+ )
76
+
77
+ if model is WorkflowFilterModule:
78
+ job_result.log(message=f"Stored filter module: {relative_path}")
79
+ else:
80
+ job_result.log(message=f"Stored Python file: {relative_path}")
81
+
82
+
83
+ def sync_workflows_from_repo(repository_record: GitRepository, job_result, delete=False):
84
+ """Sync workflow definitions and helper files from a Git repository."""
85
+ repo_path = Path(repository_record.filesystem_path)
86
+ wf_dir = PLUGIN_CFG["workflow_dir"]
87
+ workflows_path = repo_path / wf_dir
88
+ if not workflows_path.exists():
89
+ clear_repo_script_cache(repository_record)
90
+ return
91
+
92
+ # Track workflows found in this sync
93
+ synced_workflow_keys = set()
94
+
95
+ _sync_repo_python_modules(repository_record, repo_path, job_result)
96
+ validate_repo_filter_modules(repository_record)
97
+
98
+ for yml in sorted(list(workflows_path.glob("*.y*ml"))):
99
+ content = _process_includes(job_result, yml)
100
+ data = yaml.safe_load(content)
101
+
102
+ # Track this workflow key
103
+ workflow_key = data.get("key")
104
+ if workflow_key:
105
+ synced_workflow_keys.add(workflow_key)
106
+
107
+ # Check if workflow is explicitly disabled in YAML
108
+ yaml_enabled = data.get("enabled", True)
109
+
110
+ wf, _ = Workflow.objects.update_or_create(
111
+ key=data["key"],
112
+ defaults={
113
+ "name": data.get("name", data["key"]),
114
+ "description": data.get("description", ""),
115
+ "category": data.get("category", ""),
116
+ "enabled": yaml_enabled, # Use enabled flag from YAML
117
+ "manual_duration_minutes": data.get("manual_duration_minutes"),
118
+ "raw_definition": data,
119
+ "source_repo": repository_record,
120
+ },
121
+ )
122
+ wf.inputs.all().delete()
123
+ for order, ip in enumerate(data.get("inputs", []), start=1):
124
+ ct = None
125
+ if ip.get("type") in ("object", "multiobject") and ip.get("model"):
126
+ app_label, model = ip["model"].split(".")
127
+ try:
128
+ ct = ContentType.objects.get(app_label=app_label, model=model.lower())
129
+ except ContentType.DoesNotExist:
130
+ raise ValueError(f"[Workflow {wf.key}] Invalid model for input {ip['key']}: {ip['model']}")
131
+
132
+ # Normalize validators:
133
+ # - support either a single "regex" (with optional message/flags near root)
134
+ # - or a list under "validators", e.g. [{"regex": "...", "flags": ["IGNORECASE"], "message": "..."}]
135
+ validators = ip.get("validators")
136
+ if not validators:
137
+ if "regex" in ip:
138
+ v = {"regex": ip.get("regex")}
139
+ if "message" in ip:
140
+ v["message"] = ip["message"]
141
+ if "flags" in ip:
142
+ v["flags"] = ip["flags"]
143
+ validators = [v]
144
+
145
+ WorkflowInput.objects.create(
146
+ workflow=wf,
147
+ key=ip["key"],
148
+ label=ip.get("label", ip["key"]),
149
+ input_type=ip["type"],
150
+ content_type=ct,
151
+ required=ip.get("required", False),
152
+ default=ip.get("default"),
153
+ depends_on=ip.get("depends_on", []),
154
+ filter_query=ip.get("filter", {}).get("query"),
155
+ choices=ip.get("choices"),
156
+ columns=ip.get("columns"),
157
+ visible_when=ip.get("show_if"),
158
+ validators=validators,
159
+ order=order,
160
+ )
161
+ # Use bulk deletion to avoid get_absolute_url issues
162
+ WorkflowAction.objects.filter(workflow=wf).delete()
163
+
164
+ phased_actions = [
165
+ ("pre", _get_action_list(data, "pre_actions", "pre-actions", "before_actions", "before-actions")),
166
+ # backwards-compatible existing key
167
+ ("main", _get_action_list(data, "actions", "main_actions", "main-actions")),
168
+ ("post", _get_action_list(data, "post_actions", "post-actions", "after_actions", "after-actions")),
169
+ ]
170
+
171
+ for phase, actions in phased_actions:
172
+ for order, act in enumerate(actions, start=1):
173
+
174
+ if act.get("type") == "nautobot":
175
+ script_path = (act.get("script") or "").lstrip("/")
176
+ if not script_path:
177
+ raise ValueError(f"[Workflow {wf.key}] 'script' is required for nautobot actions")
178
+
179
+ # Verify the script exists in WorkflowHelper
180
+ if not WorkflowHelper.objects.filter(source_repo=repository_record, file_path=script_path).exists():
181
+ raise ValueError(f"[Workflow {wf.key}] Script not found in repository: {script_path}")
182
+
183
+ WorkflowAction.objects.create(
184
+ workflow=wf,
185
+ phase=phase,
186
+ order=order,
187
+ action_type="nautobot",
188
+ name=act.get("name", "nautobot"),
189
+ when=act.get("when", ""),
190
+ script_path="/" + script_path,
191
+ with_vars=act.get("with", {}),
192
+ )
193
+
194
+ elif act.get("type") == "awx":
195
+ WorkflowAction.objects.create(
196
+ workflow=wf,
197
+ phase=phase,
198
+ order=order,
199
+ action_type="awx",
200
+ name=act.get("name", "awx"),
201
+ when=act.get("when", ""),
202
+ awx_template_id=act.get("template_id"),
203
+ awx_extra_vars=act.get("extra_vars", {}),
204
+ monitor=act.get("monitor", True),
205
+ )
206
+
207
+ else:
208
+ raise ValueError(
209
+ f"[Workflow {wf.key}] Unsupported action type in phase '{phase}': {act.get('type')}"
210
+ )
211
+
212
+ # Disable workflows that are no longer in the repository (instead of deleting)
213
+ if delete:
214
+ workflows_to_disable = Workflow.objects.filter(
215
+ source_repo=repository_record,
216
+ enabled=True, # Only disable currently enabled workflows
217
+ ).exclude(key__in=synced_workflow_keys)
218
+
219
+ disabled_count = 0
220
+
221
+ for workflow in workflows_to_disable:
222
+ # Count related objects for logging
223
+ action_count = workflow.actions.count()
224
+ input_count = workflow.inputs.count()
225
+ run_count = workflow.runs.count()
226
+
227
+ job_result.log(
228
+ message=f"Disabling workflow '{workflow.name}' (key: {workflow.key}) "
229
+ f"with {action_count} actions, {input_count} inputs, {run_count} runs - no longer in repository"
230
+ )
231
+
232
+ # Disable instead of delete to preserve history
233
+ workflow.enabled = False
234
+ workflow.save()
235
+ disabled_count += 1
236
+
237
+ if disabled_count > 0:
238
+ job_result.log(
239
+ message=f"Disabled {disabled_count} workflows no longer present in repository. Run history preserved."
240
+ )
241
+
242
+ clear_repo_script_cache(repository_record)
243
+
244
+
245
+ datasource_contents = []
246
+
247
+ datasource_contents.append(
248
+ (
249
+ "extras.gitrepository",
250
+ DatasourceContent(
251
+ name="workflows",
252
+ content_identifier="nautobot_workflow_engine.workflows",
253
+ icon="mdi-file-document-outline",
254
+ callback=sync_workflows_from_repo,
255
+ ),
256
+ )
257
+ )