opentide 0.1.0__py3-none-any.whl

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 (329) hide show
  1. opentide/__init__.py +8 -0
  2. opentide/_version.py +24 -0
  3. opentide/ci/__init__.py +6 -0
  4. opentide/ci/azure.py +174 -0
  5. opentide/ci/discovery.py +31 -0
  6. opentide/ci/github.py +279 -0
  7. opentide/ci/gitlab.py +176 -0
  8. opentide/ci/inflight.py +283 -0
  9. opentide/ci/models.py +86 -0
  10. opentide/ci/render.py +23 -0
  11. opentide/ci/stages.py +64 -0
  12. opentide/cli/__init__.py +540 -0
  13. opentide/cli/context.py +72 -0
  14. opentide/cli/enums.py +113 -0
  15. opentide/cli/exit_codes.py +59 -0
  16. opentide/cli/output.py +101 -0
  17. opentide/cli/services/__init__.py +1 -0
  18. opentide/cli/services/ci_generator.py +19 -0
  19. opentide/cli/services/deploy.py +123 -0
  20. opentide/cli/services/document.py +46 -0
  21. opentide/cli/services/export.py +49 -0
  22. opentide/cli/services/extraction.py +54 -0
  23. opentide/cli/services/generation.py +151 -0
  24. opentide/cli/services/info.py +74 -0
  25. opentide/cli/services/setup/__init__.py +17 -0
  26. opentide/cli/services/setup/ci.py +68 -0
  27. opentide/cli/services/setup/interactive.py +158 -0
  28. opentide/cli/services/setup/mcp.py +90 -0
  29. opentide/cli/services/setup/orchestrator.py +281 -0
  30. opentide/cli/services/setup/platforms.py +74 -0
  31. opentide/cli/services/setup/repo.py +128 -0
  32. opentide/cli/services/setup/skills.py +245 -0
  33. opentide/cli/services/setup/skills_registry.py +252 -0
  34. opentide/cli/services/setup/templates.py +69 -0
  35. opentide/cli/services/setup/vscode.py +145 -0
  36. opentide/cli/services/validation.py +246 -0
  37. opentide/cli/setup_app.py +498 -0
  38. opentide/core/__init__.py +5 -0
  39. opentide/core/chaining.py +32 -0
  40. opentide/core/debug.py +17 -0
  41. opentide/core/environment.py +111 -0
  42. opentide/core/errors.py +82 -0
  43. opentide/core/files.py +182 -0
  44. opentide/core/index_manager.py +52 -0
  45. opentide/core/io.py +113 -0
  46. opentide/core/logging/__init__.py +35 -0
  47. opentide/core/logging/config.py +201 -0
  48. opentide/core/logging/console.py +89 -0
  49. opentide/core/logging/render.py +63 -0
  50. opentide/core/object_refs.py +27 -0
  51. opentide/core/registry.py +601 -0
  52. opentide/core/root.py +41 -0
  53. opentide/core/runtime.py +23 -0
  54. opentide/core/time.py +21 -0
  55. opentide/core/types.py +23 -0
  56. opentide/core/typing.py +5 -0
  57. opentide/data/__init__.py +1 -0
  58. opentide/data/configurations/__init__.py +1 -0
  59. opentide/data/configurations/deployment.toml +61 -0
  60. opentide/data/configurations/documentation.toml +38 -0
  61. opentide/data/configurations/global.toml +54 -0
  62. opentide/data/configurations/paths.toml +46 -0
  63. opentide/data/configurations/platforms/__init__.py +1 -0
  64. opentide/data/configurations/platforms/carbon_black_cloud.toml +29 -0
  65. opentide/data/configurations/platforms/crowdstrike.toml +31 -0
  66. opentide/data/configurations/platforms/defender_for_endpoint.toml +32 -0
  67. opentide/data/configurations/platforms/harfanglab.toml +31 -0
  68. opentide/data/configurations/platforms/sentinel.toml +34 -0
  69. opentide/data/configurations/platforms/sentinel_one.toml +33 -0
  70. opentide/data/configurations/platforms/splunk.toml +68 -0
  71. opentide/data/configurations/resources.toml +2 -0
  72. opentide/data/configurations/schema.toml +32 -0
  73. opentide/data/configurations/visibility.toml +4 -0
  74. opentide/data/external/.gitkeep +0 -0
  75. opentide/data/log_sources/LOG0000 - Log Source TEMPLATE.yaml +38 -0
  76. opentide/data/log_sources/LOG0001 - Proto AWS Security Hub.yaml +172 -0
  77. opentide/data/log_sources/Proto Doc AWS Security Hub.md +138 -0
  78. opentide/data/pins/objective.toml +15 -0
  79. opentide/data/pins/rule.toml +10 -0
  80. opentide/data/pins/threat.toml +29 -0
  81. opentide/data/setup/mcp/README.md +17 -0
  82. opentide/data/setup/mcp/claude-code.json +7 -0
  83. opentide/data/setup/mcp/cursor.json +7 -0
  84. opentide/data/setup/mcp/generic.json +7 -0
  85. opentide/data/setup/mcp/vscode.json +7 -0
  86. opentide/data/setup/skills/AGENTS.md.template +30 -0
  87. opentide/data/setup/skills/CLAUDE.md.template +29 -0
  88. opentide/data/setup/skills/copilot-instructions.md +21 -0
  89. opentide/data/setup/skills/detection-ops/SKILL.md +47 -0
  90. opentide/data/setup/vscode/settings.fragment.json +5 -0
  91. opentide/data/skills/manifest.json +143 -0
  92. opentide/data/vocabulary/actors.vocab.toml +6766 -0
  93. opentide/data/vocabulary/alert_severity.vocab.toml +30 -0
  94. opentide/data/vocabulary/att&ck.groups.vocab.toml +894 -0
  95. opentide/data/vocabulary/att&ck.vocab.toml +6188 -0
  96. opentide/data/vocabulary/chaining_relations.vocab.toml +72 -0
  97. opentide/data/vocabulary/collection.vocab.toml +386 -0
  98. opentide/data/vocabulary/criticality.vocab.toml +48 -0
  99. opentide/data/vocabulary/datasources.vocab.toml +721 -0
  100. opentide/data/vocabulary/detection.composition.vocab.toml +35 -0
  101. opentide/data/vocabulary/detection.methodology.vocab.toml +55 -0
  102. opentide/data/vocabulary/detection.types.vocab.toml +25 -0
  103. opentide/data/vocabulary/efforts.vocab.toml +22 -0
  104. opentide/data/vocabulary/feasibility.vocab.toml +40 -0
  105. opentide/data/vocabulary/impact.vocab.toml +101 -0
  106. opentide/data/vocabulary/killchain.vocab.toml +133 -0
  107. opentide/data/vocabulary/level.vocab.toml +35 -0
  108. opentide/data/vocabulary/leverage.vocab.toml +106 -0
  109. opentide/data/vocabulary/malapi.vocab.toml +2254 -0
  110. opentide/data/vocabulary/maturity.vocab.toml +60 -0
  111. opentide/data/vocabulary/mitigations.vocab.toml +660 -0
  112. opentide/data/vocabulary/objectives.vocab.toml +84 -0
  113. opentide/data/vocabulary/pap.vocab.toml +30 -0
  114. opentide/data/vocabulary/resources.vocab.toml +41 -0
  115. opentide/data/vocabulary/responders.vocab.toml +7 -0
  116. opentide/data/vocabulary/rsit.vocab.toml +201 -0
  117. opentide/data/vocabulary/scheduling.vocab.toml +93 -0
  118. opentide/data/vocabulary/sectors.vocab.toml +253 -0
  119. opentide/data/vocabulary/severity.vocab.toml +36 -0
  120. opentide/data/vocabulary/signal.entities.vocab.toml +188 -0
  121. opentide/data/vocabulary/sophistication.vocab.toml +49 -0
  122. opentide/data/vocabulary/stakeholders.vocab.toml +46 -0
  123. opentide/data/vocabulary/surface.vocab.toml +4528 -0
  124. opentide/data/vocabulary/tier.vocab.toml +49 -0
  125. opentide/data/vocabulary/tlp.vocab.toml +41 -0
  126. opentide/data/vocabulary/viability.vocab.toml +53 -0
  127. opentide/data/vocabulary/violation.vocab.toml +41 -0
  128. opentide/deployment/__init__.py +53 -0
  129. opentide/deployment/ci.py +49 -0
  130. opentide/deployment/git_backend.py +192 -0
  131. opentide/deployment/git_repo.py +276 -0
  132. opentide/deployment/planning.py +364 -0
  133. opentide/deployment/preview.py +90 -0
  134. opentide/deployment/utils.py +226 -0
  135. opentide/documentation/__init__.py +78 -0
  136. opentide/documentation/api.py +138 -0
  137. opentide/documentation/catalog.py +427 -0
  138. opentide/documentation/cli.py +328 -0
  139. opentide/documentation/config.py +60 -0
  140. opentide/documentation/context.py +38 -0
  141. opentide/documentation/diagram/__init__.py +13 -0
  142. opentide/documentation/diagram/builders.py +186 -0
  143. opentide/documentation/diagram/flowchart.py +41 -0
  144. opentide/documentation/diagram/graph.py +48 -0
  145. opentide/documentation/diagram/mindmap.py +17 -0
  146. opentide/documentation/diagram/sanitize.py +37 -0
  147. opentide/documentation/format/__init__.py +6 -0
  148. opentide/documentation/format/azure_devops.py +50 -0
  149. opentide/documentation/format/factory.py +21 -0
  150. opentide/documentation/format/generic.py +17 -0
  151. opentide/documentation/format/github.py +17 -0
  152. opentide/documentation/format/gitlab.py +49 -0
  153. opentide/documentation/format/protocol.py +114 -0
  154. opentide/documentation/markdown/builder.py +8 -0
  155. opentide/documentation/markdown/links.py +117 -0
  156. opentide/documentation/objects/__init__.py +14 -0
  157. opentide/documentation/objects/base.py +79 -0
  158. opentide/documentation/objects/objective.py +45 -0
  159. opentide/documentation/objects/rule.py +47 -0
  160. opentide/documentation/objects/threat.py +84 -0
  161. opentide/documentation/parts/__init__.py +5 -0
  162. opentide/documentation/parts/sections.py +585 -0
  163. opentide/documentation/publish/index.py +178 -0
  164. opentide/documentation/publish/paths.py +33 -0
  165. opentide/documentation/publish/writer.py +11 -0
  166. opentide/documentation/types.py +46 -0
  167. opentide/documentation/vocabulary.py +51 -0
  168. opentide/export/attack_navigator_layer.py +160 -0
  169. opentide/export/explorer_export.py +238 -0
  170. opentide/export/playbook_map.py +42 -0
  171. opentide/export/revisions_export.py +57 -0
  172. opentide/export/table_export.py +129 -0
  173. opentide/extraction/__init__.py +1 -0
  174. opentide/extraction/malapi.py +94 -0
  175. opentide/extraction/mde_importer.py +228 -0
  176. opentide/extraction/sentinel_importer.py +387 -0
  177. opentide/generation/__init__.py +34 -0
  178. opentide/generation/artifact_gate.py +135 -0
  179. opentide/generation/framework.py +530 -0
  180. opentide/generation/model_json_schema.py +21 -0
  181. opentide/generation/pydantic_metaschema.py +345 -0
  182. opentide/generation/pydantic_schemas.py +111 -0
  183. opentide/generation/pydantic_templates.py +48 -0
  184. opentide/generation/router_schema.py +46 -0
  185. opentide/generation/schema.py +46 -0
  186. opentide/generation/schema_pipeline.py +894 -0
  187. opentide/generation/schema_utils.py +14 -0
  188. opentide/generation/template.py +10 -0
  189. opentide/generation/template_engine.py +289 -0
  190. opentide/generation/template_renderer.py +103 -0
  191. opentide/generation/vocabulary.py +486 -0
  192. opentide/generation/vscode_snippets.py +83 -0
  193. opentide/indexing/__init__.py +4 -0
  194. opentide/indexing/inflight.py +304 -0
  195. opentide/indexing/inflight_change.py +127 -0
  196. opentide/indexing/inflight_shard.py +71 -0
  197. opentide/indexing/object_vocab.py +138 -0
  198. opentide/loading/__init__.py +13 -0
  199. opentide/loading/compat.py +20 -0
  200. opentide/loading/config_loader.py +64 -0
  201. opentide/loading/object_loader.py +95 -0
  202. opentide/loading/objective_loader.py +18 -0
  203. opentide/loading/platform_loader.py +465 -0
  204. opentide/loading/rule_loader.py +58 -0
  205. opentide/mcp_server/__init__.py +1 -0
  206. opentide/mcp_server/catalog.py +114 -0
  207. opentide/mcp_server/constants.py +8 -0
  208. opentide/mcp_server/resources.py +96 -0
  209. opentide/mcp_server/server.py +166 -0
  210. opentide/mcp_server/tools.py +148 -0
  211. opentide/models/__init__.py +58 -0
  212. opentide/models/base.py +49 -0
  213. opentide/models/deployment_enums.py +49 -0
  214. opentide/models/enums.py +43 -0
  215. opentide/models/metadata.py +59 -0
  216. opentide/models/migration.py +24 -0
  217. opentide/models/object_types.py +17 -0
  218. opentide/models/objective.py +82 -0
  219. opentide/models/platform.py +157 -0
  220. opentide/models/platform_configs.py +321 -0
  221. opentide/models/platform_schema.py +200 -0
  222. opentide/models/response.py +24 -0
  223. opentide/models/results.py +18 -0
  224. opentide/models/rule.py +116 -0
  225. opentide/models/schema_registry.py +138 -0
  226. opentide/models/system_config.py +238 -0
  227. opentide/models/threat.py +58 -0
  228. opentide/models/version.py +102 -0
  229. opentide/models/visibility.py +59 -0
  230. opentide/models/vocab_pins.py +67 -0
  231. opentide/mutation/__init__.py +1 -0
  232. opentide/mutation/file_name.py +60 -0
  233. opentide/mutation/promotion.py +213 -0
  234. opentide/mutation/references.py +138 -0
  235. opentide/mutation/security_domain.py +122 -0
  236. opentide/package/__init__.py +5 -0
  237. opentide/package/paths.py +30 -0
  238. opentide/platforms/__init__.py +1 -0
  239. opentide/platforms/base.py +54 -0
  240. opentide/platforms/carbon_black/__init__.py +15 -0
  241. opentide/platforms/carbon_black/client.py +133 -0
  242. opentide/platforms/carbon_black/deployer.py +195 -0
  243. opentide/platforms/carbon_black/validator.py +96 -0
  244. opentide/platforms/carbon_black.py +7 -0
  245. opentide/platforms/config.py +243 -0
  246. opentide/platforms/crowdstrike/__init__.py +3 -0
  247. opentide/platforms/crowdstrike/client.py +144 -0
  248. opentide/platforms/crowdstrike/deployer.py +148 -0
  249. opentide/platforms/crowdstrike.py +7 -0
  250. opentide/platforms/defender_for_endpoint/__init__.py +3 -0
  251. opentide/platforms/defender_for_endpoint/client.py +196 -0
  252. opentide/platforms/defender_for_endpoint/deployer.py +278 -0
  253. opentide/platforms/defender_for_endpoint/validator.py +196 -0
  254. opentide/platforms/defender_for_endpoint.py +7 -0
  255. opentide/platforms/enabled.py +30 -0
  256. opentide/platforms/harfanglab/__init__.py +3 -0
  257. opentide/platforms/harfanglab/client.py +405 -0
  258. opentide/platforms/harfanglab/deployer.py +227 -0
  259. opentide/platforms/harfanglab.py +7 -0
  260. opentide/platforms/kql.py +62 -0
  261. opentide/platforms/plugins.py +211 -0
  262. opentide/platforms/registry.py +186 -0
  263. opentide/platforms/sentinel/__init__.py +3 -0
  264. opentide/platforms/sentinel/client.py +39 -0
  265. opentide/platforms/sentinel/deployer.py +189 -0
  266. opentide/platforms/sentinel/validator.py +83 -0
  267. opentide/platforms/sentinel.py +7 -0
  268. opentide/platforms/sentinel_one/__init__.py +3 -0
  269. opentide/platforms/sentinel_one/client.py +180 -0
  270. opentide/platforms/sentinel_one/deployer.py +143 -0
  271. opentide/platforms/sentinel_one/validator.py +55 -0
  272. opentide/platforms/sentinel_one.py +7 -0
  273. opentide/platforms/splunk/__init__.py +15 -0
  274. opentide/platforms/splunk/client.py +264 -0
  275. opentide/platforms/splunk/deployer.py +466 -0
  276. opentide/platforms/splunk/validator.py +120 -0
  277. opentide/platforms/splunk.py +7 -0
  278. opentide/py.typed +0 -0
  279. opentide/registry/__init__.py +15 -0
  280. opentide/registry/artifacts.py +29 -0
  281. opentide/registry/builder.py +289 -0
  282. opentide/registry/discovery.py +41 -0
  283. opentide/registry/paths.py +175 -0
  284. opentide/schemas/__init__.py +8 -0
  285. opentide/schemas/data/platform_templates/MDR Systems Deployment/Templates/Carbon Black Cloud Enterprise EDR Template.yaml +15 -0
  286. opentide/schemas/data/platform_templates/MDR Systems Deployment/Templates/Crowdstrike Falcon Correlation Rules Template.yaml +13 -0
  287. opentide/schemas/data/platform_templates/MDR Systems Deployment/Templates/Crowdstrike Falcon Template.yaml +13 -0
  288. opentide/schemas/data/platform_templates/MDR Systems Deployment/Templates/HarfangLab Template.yaml +14 -0
  289. opentide/schemas/data/platform_templates/MDR Systems Deployment/Templates/Microsoft Defender for Endpoint Template.yaml +17 -0
  290. opentide/schemas/data/platform_templates/MDR Systems Deployment/Templates/Microsoft Sentinel Template.yaml +17 -0
  291. opentide/schemas/data/platform_templates/MDR Systems Deployment/Templates/Sentinel One Template.yaml +11 -0
  292. opentide/schemas/data/platform_templates/MDR Systems Deployment/Templates/Splunk Enterprise Template.yaml +16 -0
  293. opentide/schemas/data/vocabulary.schema.json +45 -0
  294. opentide/schemas/store.py +45 -0
  295. opentide/validation/__init__.py +42 -0
  296. opentide/validation/carbon_black_cloud_query.py +7 -0
  297. opentide/validation/checks/__init__.py +1 -0
  298. opentide/validation/checks/cross_object.py +133 -0
  299. opentide/validation/checks/kinds.py +14 -0
  300. opentide/validation/cve.py +35 -0
  301. opentide/validation/cve_check.py +78 -0
  302. opentide/validation/deprecated_fields.py +104 -0
  303. opentide/validation/errors.py +115 -0
  304. opentide/validation/field_vocab.py +164 -0
  305. opentide/validation/id_scan.py +74 -0
  306. opentide/validation/id_uniqueness.py +33 -0
  307. opentide/validation/issues.py +60 -0
  308. opentide/validation/legacy.py +47 -0
  309. opentide/validation/parallel.py +79 -0
  310. opentide/validation/pipeline.py +78 -0
  311. opentide/validation/preflight.py +171 -0
  312. opentide/validation/scope.py +75 -0
  313. opentide/validation/session.py +398 -0
  314. opentide/validation/splunk_query.py +7 -0
  315. opentide/validation/tide_schema.py +68 -0
  316. opentide/validation/uuid_v4.py +33 -0
  317. opentide/validation/vocab_resolver.py +321 -0
  318. opentide/vocabulary/__init__.py +21 -0
  319. opentide/vocabulary/fetch_stix.py +71 -0
  320. opentide/vocabulary/generate_actors.py +86 -0
  321. opentide/vocabulary/generate_attack.py +133 -0
  322. opentide/vocabulary/io.py +100 -0
  323. opentide/vocabulary/stix_attack.py +229 -0
  324. opentide/vocabulary/writers.py +43 -0
  325. opentide-0.1.0.dist-info/METADATA +96 -0
  326. opentide-0.1.0.dist-info/RECORD +329 -0
  327. opentide-0.1.0.dist-info/WHEEL +4 -0
  328. opentide-0.1.0.dist-info/entry_points.txt +12 -0
  329. opentide-0.1.0.dist-info/licenses/LICENSE +287 -0
opentide/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """OpenTide DetectionOps engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from opentide._version import __version__
6
+ from opentide.core.registry import OpenTide
7
+
8
+ __all__ = ["__version__", "OpenTide"]
opentide/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,6 @@
1
+ """CI pipeline template renderers for GitHub, GitLab, and Azure DevOps."""
2
+
3
+ from opentide.ci.models import CiRenderOptions
4
+ from opentide.ci.render import render_ci
5
+
6
+ __all__ = ["CiRenderOptions", "render_ci"]
opentide/ci/azure.py ADDED
@@ -0,0 +1,174 @@
1
+ """Azure DevOps pipeline renderer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import textwrap
6
+
7
+ from opentide.ci.inflight import azure_inflight_job, azure_inflight_prune_job
8
+ from opentide.ci.models import CiRenderOptions
9
+ from opentide.ci.stages import (
10
+ document_steps,
11
+ header_comment,
12
+ pip_install,
13
+ production_deploy_steps,
14
+ promotion_steps,
15
+ staging_deploy_steps,
16
+ )
17
+ from opentide.cli.enums import QUERY_VALIDATION_PLATFORMS
18
+
19
+
20
+ def _bash_script(commands: list[str], indent: int = 12) -> str:
21
+ joined = " && ".join(commands)
22
+ pad = " " * indent
23
+ return f"{pad}- script: |\n{pad} {joined}"
24
+
25
+
26
+ def _python_setup(options: CiRenderOptions, indent: int = 10) -> str:
27
+ pad = " " * indent
28
+ return textwrap.dedent(
29
+ f"""\
30
+ {pad}- task: UsePythonVersion@0
31
+ {pad} inputs:
32
+ {pad} versionSpec: '{options.python_version}'
33
+ {pad}- script: {pip_install(options)}
34
+ {pad} displayName: Install OpenTide
35
+ """
36
+ )
37
+
38
+
39
+ def render_azure(options: CiRenderOptions) -> str:
40
+ branch = options.default_branch
41
+ validate_steps = _python_setup(options) + "\n" + _bash_script(["opentide validate"])
42
+
43
+ query_jobs: list[str] = []
44
+ for platform in options.platforms:
45
+ if platform not in QUERY_VALIDATION_PLATFORMS:
46
+ continue
47
+ safe = platform.replace("_", "-")
48
+ query_jobs.append(
49
+ textwrap.dedent(
50
+ f"""\
51
+ - job: validate_query_{safe}
52
+ displayName: Validate query ({platform})
53
+ steps:
54
+ {_python_setup(options)}
55
+ {_bash_script([f"opentide validate query --platform {platform}"])}
56
+ """
57
+ )
58
+ )
59
+
60
+ staging_job = ""
61
+ inflight_job = ""
62
+ inflight_prune_job = ""
63
+ if options.staging:
64
+ staging_job = textwrap.dedent(
65
+ f"""\
66
+ - job: deploy_staging
67
+ displayName: Deploy Staging
68
+ dependsOn: generate
69
+ condition: eq(variables['Build.Reason'], 'PullRequest')
70
+ steps:
71
+ {_python_setup(options)}
72
+ {_bash_script(staging_deploy_steps(options))}
73
+ """
74
+ )
75
+ if options.inflight:
76
+ inflight_job = azure_inflight_job(
77
+ python_version=options.python_version,
78
+ opentide_version=options.opentide_version,
79
+ default_branch=branch,
80
+ )
81
+ inflight_prune_job = azure_inflight_prune_job(
82
+ python_version=options.python_version,
83
+ opentide_version=options.opentide_version,
84
+ default_branch=branch,
85
+ )
86
+
87
+ promote_stage = ""
88
+ promote_cmds = promotion_steps(options)
89
+ if promote_cmds:
90
+ promote_stage = textwrap.dedent(
91
+ f"""\
92
+ - stage: Promote
93
+ displayName: Promote
94
+ dependsOn: Deploy
95
+ condition: >
96
+ and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/{branch}'))
97
+ jobs:
98
+ - job: promote
99
+ displayName: Promote rules
100
+ steps:
101
+ {_python_setup(options)}
102
+ {_bash_script(promotion_steps(options))}
103
+ """
104
+ )
105
+
106
+ query_jobs_block = "\n".join(query_jobs)
107
+ validate_jobs = textwrap.dedent(
108
+ f"""\
109
+ - job: validate
110
+ displayName: Validate objects
111
+ steps:
112
+ {validate_steps}
113
+ """
114
+ )
115
+ if query_jobs_block:
116
+ validate_jobs = validate_jobs + query_jobs_block
117
+
118
+ body = textwrap.dedent(
119
+ f"""\
120
+ trigger:
121
+ branches:
122
+ include:
123
+ - {branch}
124
+
125
+ pr:
126
+ branches:
127
+ include:
128
+ - {branch}
129
+
130
+ stages:
131
+ - stage: Validate
132
+ displayName: Validate
133
+ jobs:
134
+ {validate_jobs}
135
+
136
+ - stage: Generate
137
+ displayName: Generate
138
+ dependsOn: Validate
139
+ jobs:
140
+ - job: generate
141
+ displayName: Generate framework artifacts
142
+ steps:
143
+ {_python_setup(options)}
144
+ {_bash_script(["opentide generate"])}
145
+
146
+ - stage: Deploy
147
+ displayName: Deploy
148
+ dependsOn: Generate
149
+ jobs:
150
+ {staging_job}
151
+ {inflight_job}
152
+ {inflight_prune_job}
153
+ - job: deploy_production
154
+ displayName: Deploy Production
155
+ dependsOn: generate
156
+ condition: eq(variables['Build.SourceBranch'], 'refs/heads/{branch}')
157
+ steps:
158
+ {_python_setup(options)}
159
+ {_bash_script(production_deploy_steps(options))}
160
+
161
+ - stage: Document
162
+ displayName: Document
163
+ dependsOn: Generate
164
+ condition: eq(variables['Build.SourceBranch'], 'refs/heads/{branch}')
165
+ jobs:
166
+ - job: document
167
+ displayName: Generate documentation
168
+ steps:
169
+ {_python_setup(options)}
170
+ {_bash_script(document_steps(options))}
171
+ {promote_stage}
172
+ """
173
+ )
174
+ return header_comment(options) + body
@@ -0,0 +1,31 @@
1
+ """Discover enabled platforms from repository configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ from opentide.registry.discovery import OPENTIDE_DIR
9
+
10
+ _PLATFORM_SECTION = re.compile(r"^\[platform\]\s*$", re.MULTILINE)
11
+ _ENABLED = re.compile(r"^enabled\s*=\s*true\s*$", re.MULTILINE | re.IGNORECASE)
12
+
13
+
14
+ def platforms_config_dir(repo: Path) -> Path:
15
+ return repo / OPENTIDE_DIR / "configurations" / "platforms"
16
+
17
+
18
+ def discover_enabled_platforms(repo: Path) -> list[str]:
19
+ """Return platform identifiers with ``enabled = true`` in platform TOML files."""
20
+ config_dir = platforms_config_dir(repo)
21
+ if not config_dir.is_dir():
22
+ return []
23
+ enabled: list[str] = []
24
+ for path in sorted(config_dir.glob("*.toml")):
25
+ text = path.read_text(encoding="utf-8")
26
+ if not _PLATFORM_SECTION.search(text):
27
+ continue
28
+ if not _ENABLED.search(text):
29
+ continue
30
+ enabled.append(path.stem)
31
+ return enabled
opentide/ci/github.py ADDED
@@ -0,0 +1,279 @@
1
+ """GitHub Actions workflow renderer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import textwrap
6
+
7
+ from opentide.ci.inflight import github_inflight_job, github_inflight_prune_job
8
+ from opentide.ci.models import CiRenderOptions
9
+ from opentide.ci.stages import (
10
+ document_steps,
11
+ header_comment,
12
+ pip_install,
13
+ production_deploy_steps,
14
+ promotion_steps,
15
+ query_platforms,
16
+ staging_deploy_steps,
17
+ )
18
+
19
+
20
+ def _indent_yaml(text: str, spaces: int) -> str:
21
+ pad = " " * spaces
22
+ return "\n".join(pad + line if line else line for line in text.splitlines())
23
+
24
+
25
+ def _run_steps(commands: list[str], indent: int = 10) -> str:
26
+ lines = []
27
+ for cmd in commands:
28
+ lines.append(f"- run: {cmd}")
29
+ return _indent_yaml("\n".join(lines), indent)
30
+
31
+
32
+ def _explorer_jobs(branch: str, python_version: str) -> list[str]:
33
+ explorer_job = textwrap.dedent(
34
+ f"""\
35
+ explorer:
36
+ name: Build explorer
37
+ runs-on: ubuntu-latest
38
+ needs: document
39
+ env:
40
+ HAS_GITHUB_APP: ${{{{ secrets.GH_APP_ID && secrets.GH_APP_KEY && 'true' || 'false' }}}}
41
+ steps:
42
+ - name: Checkout repository
43
+ uses: actions/checkout@v4
44
+
45
+ - name: Generate GitHub App token
46
+ id: generate_token
47
+ if: ${{{{ env.HAS_GITHUB_APP == 'true' }}}}
48
+ uses: actions/create-github-app-token@v1
49
+ with:
50
+ app-id: ${{{{ secrets.GH_APP_ID }}}}
51
+ private-key: ${{{{ secrets.GH_APP_KEY }}}}
52
+ owner: OpenTideHQ
53
+ repositories: opentide
54
+
55
+ - name: Checkout opentide
56
+ uses: actions/checkout@v4
57
+ with:
58
+ repository: OpenTideHQ/opentide
59
+ ref: development
60
+ path: opentide
61
+ token: ${{{{ steps.generate_token.outputs.token || secrets.OPENTIDE_REPO_TOKEN }}}}
62
+
63
+ - name: Checkout explorer
64
+ uses: actions/checkout@v4
65
+ with:
66
+ repository: OpenTideHQ/explorer
67
+ ref: main
68
+ path: explorer
69
+
70
+ - uses: actions/setup-python@v5
71
+ with:
72
+ python-version: "{python_version}"
73
+
74
+ - uses: actions/setup-node@v4
75
+ with:
76
+ node-version: "22"
77
+
78
+ - name: Install opentide
79
+ run: pip install -e ./opentide
80
+
81
+ - name: Build explorer static site
82
+ run: |
83
+ opentide explorer build \
84
+ --output ./out/explorer \
85
+ --base-path /${{{{ github.event.repository.name }}}}
86
+ env:
87
+ OPENTIDE_REPO_ROOT: ${{{{ github.workspace }}}}
88
+ OPENTIDE_EXPLORER_PATH: ${{{{ github.workspace }}}}/explorer
89
+
90
+ - name: Upload Pages artifact
91
+ uses: actions/upload-pages-artifact@v3
92
+ with:
93
+ path: ./out/explorer
94
+ """
95
+ )
96
+ deploy_job = textwrap.dedent(
97
+ f"""\
98
+ deploy-explorer:
99
+ name: Deploy explorer to GitHub Pages
100
+ runs-on: ubuntu-latest
101
+ needs: explorer
102
+ if: github.event_name == 'push' && github.ref == format('refs/heads/{branch}')
103
+ permissions:
104
+ pages: write
105
+ id-token: write
106
+ environment:
107
+ name: github-pages
108
+ url: ${{{{ steps.deployment.outputs.page_url }}}}
109
+ steps:
110
+ - name: Deploy to GitHub Pages
111
+ id: deployment
112
+ uses: actions/deploy-pages@v4
113
+ """
114
+ )
115
+ return [explorer_job, deploy_job]
116
+
117
+
118
+ def render_github(options: CiRenderOptions) -> str:
119
+ branch = options.default_branch
120
+ setup_steps = textwrap.dedent(
121
+ f"""\
122
+ - uses: actions/checkout@v4
123
+ - uses: actions/setup-python@v5
124
+ with:
125
+ python-version: "{options.python_version}"
126
+ - run: {pip_install(options)}
127
+ """
128
+ )
129
+
130
+ validate_body = setup_steps + _run_steps(
131
+ ["opentide validate"]
132
+ + [f"opentide validate query --platform {p}" for p in query_platforms(options)]
133
+ )
134
+
135
+ generate_body = setup_steps + _run_steps(["opentide generate"])
136
+
137
+ jobs: list[str] = []
138
+
139
+ jobs.append(
140
+ textwrap.dedent(
141
+ f"""\
142
+ validate:
143
+ name: Validate
144
+ runs-on: ubuntu-latest
145
+ steps:
146
+ {validate_body}
147
+ """
148
+ )
149
+ )
150
+
151
+ jobs.append(
152
+ textwrap.dedent(
153
+ f"""\
154
+ generate:
155
+ name: Generate
156
+ needs: validate
157
+ runs-on: ubuntu-latest
158
+ steps:
159
+ {generate_body}
160
+ """
161
+ )
162
+ )
163
+
164
+ if options.staging:
165
+ staging_cmds = staging_deploy_steps(options)
166
+ jobs.append(
167
+ textwrap.dedent(
168
+ f"""\
169
+ deploy_staging:
170
+ name: Deploy Staging
171
+ needs: generate
172
+ if: github.event_name == 'pull_request'
173
+ runs-on: ubuntu-latest
174
+ steps:
175
+ {setup_steps}{_run_steps(staging_cmds)}
176
+ """
177
+ )
178
+ )
179
+
180
+ if options.inflight:
181
+ jobs.append(
182
+ github_inflight_job(
183
+ python_version=options.python_version,
184
+ opentide_version=options.opentide_version,
185
+ default_branch=branch,
186
+ )
187
+ )
188
+ jobs.append(
189
+ github_inflight_prune_job(
190
+ python_version=options.python_version,
191
+ opentide_version=options.opentide_version,
192
+ default_branch=branch,
193
+ )
194
+ )
195
+
196
+ prod_needs = "deploy_staging" if options.staging else "generate"
197
+ prod_if = f"github.event_name == 'push' && github.ref == format('refs/heads/{branch}')"
198
+ jobs.append(
199
+ textwrap.dedent(
200
+ f"""\
201
+ deploy_production:
202
+ name: Deploy Production
203
+ needs: {prod_needs}
204
+ if: {prod_if}
205
+ runs-on: ubuntu-latest
206
+ steps:
207
+ {setup_steps}{_run_steps(production_deploy_steps(options))}
208
+ """
209
+ )
210
+ )
211
+
212
+ if options.promotion and promotion_steps(options):
213
+ jobs.append(
214
+ textwrap.dedent(
215
+ f"""\
216
+ promote:
217
+ name: Promote Rules
218
+ needs: deploy_production
219
+ if: {prod_if}
220
+ runs-on: ubuntu-latest
221
+ steps:
222
+ {setup_steps}{_run_steps(promotion_steps(options))}
223
+ """
224
+ )
225
+ )
226
+
227
+ jobs.append(
228
+ textwrap.dedent(
229
+ f"""\
230
+ document:
231
+ name: Document
232
+ needs: generate
233
+ if: {prod_if}
234
+ runs-on: ubuntu-latest
235
+ steps:
236
+ {setup_steps}{_run_steps(document_steps(options))}
237
+ """
238
+ )
239
+ )
240
+
241
+ if options.explorer_pages:
242
+ jobs.extend(_explorer_jobs(branch, options.python_version))
243
+
244
+ permissions = ""
245
+ concurrency = ""
246
+ if options.explorer_pages:
247
+ permissions = textwrap.dedent(
248
+ """\
249
+ permissions:
250
+ contents: read
251
+ pages: write
252
+ id-token: write
253
+
254
+ """
255
+ )
256
+ concurrency = textwrap.dedent(
257
+ """\
258
+ concurrency:
259
+ group: pages
260
+ cancel-in-progress: false
261
+
262
+ """
263
+ )
264
+
265
+ body = textwrap.dedent(
266
+ f"""\
267
+ name: OpenTide
268
+
269
+ on:
270
+ pull_request:
271
+ push:
272
+ branches:
273
+ - {branch}
274
+
275
+ {permissions}{concurrency}jobs:
276
+ """
277
+ )
278
+ job_yaml = "\n".join(_indent_yaml(job, 2) for job in jobs)
279
+ return header_comment(options) + body + job_yaml
opentide/ci/gitlab.py ADDED
@@ -0,0 +1,176 @@
1
+ """GitLab CI pipeline renderer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import textwrap
6
+
7
+ from opentide.ci.inflight import gitlab_inflight_job, gitlab_inflight_prune_job
8
+ from opentide.ci.models import CiRenderOptions
9
+ from opentide.ci.stages import (
10
+ document_steps,
11
+ header_comment,
12
+ pip_install,
13
+ production_deploy_steps,
14
+ promotion_steps,
15
+ staging_deploy_steps,
16
+ )
17
+ from opentide.cli.enums import QUERY_VALIDATION_PLATFORMS
18
+
19
+
20
+ def _script_block(commands: list[str], indent: int = 4) -> str:
21
+ pad = " " * indent
22
+ lines = [f"{pad}- {cmd}" for cmd in commands]
23
+ return "\n".join(lines)
24
+
25
+
26
+ def _base_before_script(options: CiRenderOptions) -> str:
27
+ return _script_block([pip_install(options)])
28
+
29
+
30
+ def render_gitlab(options: CiRenderOptions) -> str:
31
+ stages = ["validate", "generate", "deploy", "document"]
32
+ promote_cmds = promotion_steps(options)
33
+ if promote_cmds:
34
+ stages.insert(3, "promote")
35
+
36
+ validate_script = _script_block(["opentide validate"])
37
+ query_jobs: list[str] = []
38
+ for platform in options.platforms:
39
+ if platform not in QUERY_VALIDATION_PLATFORMS:
40
+ continue
41
+ job_name = platform.replace("_", "-")
42
+ query_jobs.append(
43
+ textwrap.dedent(
44
+ f"""\
45
+ validate_query_{job_name}:
46
+ stage: validate
47
+ image: python:{options.python_version}-slim
48
+ before_script:
49
+ {_base_before_script(options)}
50
+ script:
51
+ - opentide validate query --platform {platform}
52
+ """
53
+ )
54
+ )
55
+
56
+ deploy_jobs: list[str] = []
57
+ if options.staging:
58
+ deploy_jobs.append(
59
+ textwrap.dedent(
60
+ f"""\
61
+ deploy_staging:
62
+ stage: deploy
63
+ image: python:{options.python_version}-slim
64
+ rules:
65
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
66
+ before_script:
67
+ {_base_before_script(options)}
68
+ script:
69
+ {_script_block(staging_deploy_steps(options))}
70
+ needs:
71
+ - generate
72
+ """
73
+ )
74
+ )
75
+
76
+ deploy_jobs.append(
77
+ textwrap.dedent(
78
+ f"""\
79
+ deploy_production:
80
+ stage: deploy
81
+ image: python:{options.python_version}-slim
82
+ rules:
83
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
84
+ before_script:
85
+ {_base_before_script(options)}
86
+ script:
87
+ {_script_block(production_deploy_steps(options))}
88
+ needs:
89
+ - generate
90
+ """
91
+ )
92
+ )
93
+
94
+ if options.inflight:
95
+ deploy_jobs.append(
96
+ gitlab_inflight_job(
97
+ python_version=options.python_version,
98
+ opentide_version=options.opentide_version,
99
+ )
100
+ )
101
+ deploy_jobs.append(
102
+ gitlab_inflight_prune_job(
103
+ python_version=options.python_version,
104
+ opentide_version=options.opentide_version,
105
+ )
106
+ )
107
+
108
+ promote_job = ""
109
+ if promote_cmds:
110
+ promote_job = textwrap.dedent(
111
+ f"""\
112
+ promote:
113
+ stage: promote
114
+ image: python:{options.python_version}-slim
115
+ rules:
116
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
117
+ before_script:
118
+ {_base_before_script(options)}
119
+ script:
120
+ {_script_block(promotion_steps(options))}
121
+ needs:
122
+ - deploy_production
123
+ """
124
+ )
125
+
126
+ document_job = textwrap.dedent(
127
+ f"""\
128
+ document:
129
+ stage: document
130
+ image: python:{options.python_version}-slim
131
+ rules:
132
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
133
+ before_script:
134
+ {_base_before_script(options)}
135
+ script:
136
+ {_script_block(document_steps(options))}
137
+ needs:
138
+ - generate
139
+ """
140
+ )
141
+
142
+ stage_lines = "\n".join(f" - {stage}" for stage in stages)
143
+
144
+ core = textwrap.dedent(
145
+ f"""\
146
+ stages:
147
+ {stage_lines}
148
+
149
+ default:
150
+ image: python:{options.python_version}-slim
151
+
152
+ validate:
153
+ stage: validate
154
+ before_script:
155
+ {_base_before_script(options)}
156
+ script:
157
+ {validate_script}
158
+
159
+ generate:
160
+ stage: generate
161
+ before_script:
162
+ {_base_before_script(options)}
163
+ script:
164
+ - opentide generate
165
+ needs:
166
+ - validate
167
+ """
168
+ )
169
+
170
+ parts = [header_comment(options), core]
171
+ parts.extend(query_jobs)
172
+ parts.extend(deploy_jobs)
173
+ if promote_job:
174
+ parts.append(promote_job)
175
+ parts.append(document_job)
176
+ return "\n".join(parts)