datasecops-cli 0.7.0b1__tar.gz → 0.7.0b2__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 (88) hide show
  1. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/.gitignore +2 -1
  2. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/CHANGELOG.md +16 -0
  3. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/DEVELOPMENT.md +2 -2
  4. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/PKG-INFO +3 -3
  5. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/README.md +2 -2
  6. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/getting-started.md +38 -1
  7. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/pyproject.toml +1 -1
  8. datasecops_cli-0.7.0b2/src/datasecops_cli/__init__.py +1 -0
  9. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/main.py +119 -75
  10. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/models/project_config.py +3 -0
  11. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/bootstrap_service.py +3 -1
  12. datasecops_cli-0.7.0b2/src/datasecops_cli/services/dbt_project_generator.py +331 -0
  13. datasecops_cli-0.7.0b1/src/datasecops_cli/__init__.py +0 -1
  14. datasecops_cli-0.7.0b1/src/datasecops_cli/services/dbt_project_generator.py +0 -230
  15. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/.github/workflows/auto-tag.yml +0 -0
  16. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/.github/workflows/publish-cli.yml +0 -0
  17. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/.github/workflows/test.yml +0 -0
  18. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/AGENTS.md +0 -0
  19. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/LICENSE +0 -0
  20. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/dcm.md +0 -0
  21. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/git-operations.md +0 -0
  22. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/legacy.md +0 -0
  23. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/legacy_plan_of_action.md +0 -0
  24. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/docs/mcp-server.md +0 -0
  25. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/mcp-servers.json +0 -0
  26. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/scripts/vendor_model.py +0 -0
  27. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/setup.ps1 +0 -0
  28. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/setup.sh +0 -0
  29. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/config.py +0 -0
  30. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/__init__.py +0 -0
  31. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/configuration.py +0 -0
  32. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/dcm_operations.py +0 -0
  33. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/development.py +0 -0
  34. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/downloads.py +0 -0
  35. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/menus/git_operations.py +0 -0
  36. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/models/__init__.py +0 -0
  37. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/models/git_helpers.py +0 -0
  38. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/__init__.py +0 -0
  39. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dbt_runner.py +0 -0
  40. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dcm_orchestrator.py +0 -0
  41. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dcm_pipelines.py +0 -0
  42. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dcm_registry.py +0 -0
  43. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/dcm_service.py +0 -0
  44. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
  45. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/download_service.py +0 -0
  46. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/git_service.py +0 -0
  47. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/linting_service.py +0 -0
  48. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/model_commands.py +0 -0
  49. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/skill_service.py +0 -0
  50. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/snowflake_service.py +0 -0
  51. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/services/upstream_service.py +0 -0
  52. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/__init__.py +0 -0
  53. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/display.py +0 -0
  54. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/file_utils.py +0 -0
  55. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/mcp.py +0 -0
  56. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
  57. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_mcp/__init__.py +0 -0
  58. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_mcp/__main__.py +0 -0
  59. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_mcp/connection.py +0 -0
  60. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_mcp/server.py +0 -0
  61. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/__init__.py +0 -0
  62. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/convert.py +0 -0
  63. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/diagram.py +0 -0
  64. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/expand.py +0 -0
  65. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/extract.py +0 -0
  66. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/render.py +0 -0
  67. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/schema.py +0 -0
  68. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/scope.py +0 -0
  69. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/scripts.py +0 -0
  70. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/starter.py +0 -0
  71. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/templates/macros/role.sql +0 -0
  72. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/templates/macros/role_guard.sql +0 -0
  73. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/templates/macros/warehouse.sql +0 -0
  74. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/src/datasecops_model/validate.py +0 -0
  75. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/__init__.py +0 -0
  76. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_config.py +0 -0
  77. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_dbt_runner.py +0 -0
  78. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_dcm_model.py +0 -0
  79. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_dcm_orchestrator.py +0 -0
  80. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_dcm_service.py +0 -0
  81. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_download_service.py +0 -0
  82. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_file_utils.py +0 -0
  83. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_linting_service.py +0 -0
  84. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_main.py +0 -0
  85. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_model_commands.py +0 -0
  86. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_models.py +0 -0
  87. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_version.py +0 -0
  88. {datasecops_cli-0.7.0b1 → datasecops_cli-0.7.0b2}/tests/test_yaml_utils.py +0 -0
@@ -219,4 +219,5 @@ __marimo__/
219
219
 
220
220
 
221
221
  legacy_framework/
222
- plans/
222
+ plans/
223
+ .cortex/
@@ -4,6 +4,22 @@ All notable changes to the DataSecOps CLI are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Fixed
8
+
9
+ - **Default app database renamed to `MONITORIAL_ORBIT`** — the `datasecops setup` suggested app-database name (and matching docs examples) was `DATA_ENGINEERS_DATASECOPS_FRAMEWORK`; it now matches the native app's actual installed name.
10
+ - **`dbt_project.yml` generator matches the source-based project spec** — added `docs-paths`, fixed `analysis-paths` (was `analyses`), added top-level `seeds:`/`tests:` blocks, `+static_analysis`/`+meta`/`+tags` at the model root, per-object-type `+static_analysis` (stored procedures/UDFs off, tasks baseline), the commented-out post-hook/on-run-start/on-run-end guidance, and the data-governance `vars` block.
11
+ - **`profiles.yml` deploy targets now run as the framework's CI identity** — every target except `local-dev` previously used the local developer's Snowflake user with each target's own role/warehouse. They now use the CI Username/Role/Warehouse from the native app's Deployment settings (new `ProjectSettings.ci_username`/`ci_role`/`ci_warehouse`, falling back to the previous behavior when unset), matching what the CI pipeline actually deploys with.
12
+ - **`datasecops setup` could silently default to GitHub for a non-GitHub platform** — `PROJECT`/`SOURCE_CONTROL`/`WORK_ITEMS` framework config and profile/`profiles.yml` resolution were one single `try/except`; any failure after connecting (including just the `SOURCE_CONTROL` fetch itself) discarded already-fetched config and silently fell back to `SourceControl()`'s default platform (`GitHub`) with no indication why, so the MCP setup step could prompt for the wrong PAT. Each framework config fetch is now independent, so one failing can't blank out another; a missing/failed `SOURCE_CONTROL` config, or one missing `source_control_platform` specifically, now warns explicitly which platform it's defaulting to.
13
+ - **`local-dev` could be overwritten by a same-named framework target, or by the CI identity on a plain `dbt run`** — `generate_profiles_yml` no longer lets a framework target literally named `local-dev` clobber the dedicated external-browser output, and the profile's default target is now always `local-dev` (previously it followed whatever the native app marked as its own default, which could be a deploy-only target running as the CI identity). `_run_interactive` and bootstrap's `dbt deps` step now always run as `local-dev` instead of trusting the framework's default target.
14
+ - **Re-running `datasecops setup` for an already-known profile never refreshed its deployment targets** — a CI identity change in the native app had no effect until `profiles.yml` was deleted. Setup now always refreshes the CI/deployment targets (while preserving an existing `local-dev` target if already customized), and skips writing `profiles.yml` entirely if the `PROJECT` framework config couldn't be loaded (previously it wrote incorrect defaults).
15
+ - **Hardened `dbt_project.yml` generation against unsafe characters and a YAML boolean-coercion bug** — project/profile name, dbt version and warehouse name are now validated against a safe identifier pattern before being substituted into the raw YAML template (closing a potential injection via a newline or quote in native-app-configured values), and `static_analysis: off` is now quoted so YAML doesn't resolve it to the boolean `false`.
16
+
17
+ ### Docs
18
+
19
+ - **Documented setting up `~/.dbt/profiles.yml`** — `datasecops bootstrap` writes a generated `profiles.yml` inside the dbt project folder as a reference only; dbt itself reads from `~/.dbt` (per the native app's `profile_dir` setting), which getting-started.md now explains, including the minimal `local-dev` block a developer needs there.
20
+
21
+ ## [0.7.0b1] - 2026-10-26
22
+
7
23
  ### Added
8
24
 
9
25
  - **Shared DCM model (`datasecops_model`)** — one versioned JSON document (`config/dcm_model.json`, schema `datasecops.dcm/v1`) describing every DCM project (type, dir, `depends_on`, targets with deploy roles), the security model (role groups with environment fan-out, SCIM/UGE prefixes, diagram layout) and the warehouses (the `warehouse()` macro arguments, grants, application grants, budgets). The same package is vendored into the DataSecOps native app (`scripts/vendor_model.py`), so the app and the CLI validate, render and draw identically.
@@ -38,7 +38,7 @@ chmod +x setup.sh
38
38
  The script will prompt you for:
39
39
 
40
40
  - **Connection name** — the name of your Snowflake connection from `~/.snowflake/connections.toml`
41
- - **Native app database name** — the database where the DataSecOps Framework app is installed (e.g. `DATA_ENGINEERS_DATASECOPS_FRAMEWORK`)
41
+ - **Native app database name** — the database where the DataSecOps Framework app is installed (e.g. `MONITORIAL_ORBIT`)
42
42
 
43
43
  ### 2. Activate the virtual environment
44
44
 
@@ -120,7 +120,7 @@ Created by the setup script in your project root. Contains:
120
120
 
121
121
  ```yaml
122
122
  connection_name: "my_connection" # Snowflake connection name
123
- app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK" # Native app database
123
+ app_database: "MONITORIAL_ORBIT" # Native app database
124
124
  profile_name: "" # Auto-detected from dbt_project.yml
125
125
  ```
126
126
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: datasecops-cli
3
- Version: 0.7.0b1
3
+ Version: 0.7.0b2
4
4
  Summary: DataSecOps Framework CLI for Snowflake Native App
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -120,7 +120,7 @@ Your pipeline needs two things:
120
120
 
121
121
  ```yaml
122
122
  connection_name: "ci"
123
- app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
123
+ app_database: "MONITORIAL_ORBIT"
124
124
  ```
125
125
 
126
126
  2. **A Snowflake connection** in `~/.snowflake/connections.toml` for the CI service account:
@@ -264,7 +264,7 @@ The CLI reads from a `.datasecops.yml` file in your project root (created by the
264
264
 
265
265
  ```yaml
266
266
  connection_name: "my_connection"
267
- app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
267
+ app_database: "MONITORIAL_ORBIT"
268
268
  ```
269
269
 
270
270
  Project profiles, linting rules, pipeline templates, and deployment targets are all managed centrally in the native app and pulled down by the CLI.
@@ -100,7 +100,7 @@ Your pipeline needs two things:
100
100
 
101
101
  ```yaml
102
102
  connection_name: "ci"
103
- app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
103
+ app_database: "MONITORIAL_ORBIT"
104
104
  ```
105
105
 
106
106
  2. **A Snowflake connection** in `~/.snowflake/connections.toml` for the CI service account:
@@ -244,7 +244,7 @@ The CLI reads from a `.datasecops.yml` file in your project root (created by the
244
244
 
245
245
  ```yaml
246
246
  connection_name: "my_connection"
247
- app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
247
+ app_database: "MONITORIAL_ORBIT"
248
248
  ```
249
249
 
250
250
  Project profiles, linting rules, pipeline templates, and deployment targets are all managed centrally in the native app and pulled down by the CLI.
@@ -59,7 +59,7 @@ This creates `.datasecops.yml` in your project root:
59
59
 
60
60
  ```yaml
61
61
  connection_name: "my_connection"
62
- app_database: "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
62
+ app_database: "MONITORIAL_ORBIT"
63
63
  ```
64
64
 
65
65
  If you prefer to create it manually, the two required fields are:
@@ -91,6 +91,43 @@ You should see the main menu, confirming the CLI can reach the native app.
91
91
 
92
92
  ---
93
93
 
94
+ ## Step 3b: Set up your local dbt profile (`~/.dbt/profiles.yml`)
95
+
96
+ Running `datasecops bootstrap` (or **Configuration → new project** from the main menu) scaffolds a
97
+ dbt project from the native app's framework config, including a generated `dbt_project.yml` and
98
+ `profiles.yml`. The generated `profiles.yml` is written *inside* the dbt project folder (e.g.
99
+ `./dbt/profiles.yml`) as a reference — it's gitignored and never committed.
100
+
101
+ dbt itself does **not** read that file. It reads whatever `profiles.yml` lives in the directory set
102
+ by `--profiles-dir`, which the CLI passes as the native app's `PROJECT.profile_dir` setting
103
+ (defaults to `~/.dbt`). So you need to copy the generated `local-dev` target into your own
104
+ `~/.dbt/profiles.yml` (create the file/folder if they don't exist yet):
105
+
106
+ ```yaml
107
+ <profile_name>: # matches the `profile:` field in dbt_project.yml
108
+ target: local-dev
109
+ outputs:
110
+ local-dev:
111
+ type: snowflake
112
+ account: "<your_account>"
113
+ authenticator: externalbrowser # SSO login, no password/PAT needed
114
+ role: DEVELOPERS
115
+ warehouse: DEV_WH
116
+ database: "<profile's target database>"
117
+ schema: PUBLIC
118
+ threads: 4
119
+ ```
120
+
121
+ You only need the `local-dev` target locally. The other targets in the generated `profiles.yml`
122
+ (e.g. `dev`, `test`, `prod`) run as the framework's CI/deployment identity (the CI Username, Role
123
+ and Warehouse configured in the native app's Deployment settings) — they're what the CI pipeline
124
+ deploys with, not something a developer runs against directly.
125
+
126
+ If you work on multiple framework projects, each profile (one per project, named after its
127
+ `profile:` field) gets its own block in the same `~/.dbt/profiles.yml` file.
128
+
129
+ ---
130
+
94
131
  ## Step 4: Install MCP servers
95
132
 
96
133
  ### 4a. DataSecOps Framework MCP Server (included)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datasecops-cli"
7
- version = "0.7.0b1"
7
+ version = "0.7.0b2"
8
8
  description = "DataSecOps Framework CLI for Snowflake Native App"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ __version__ = "0.7.0b2"
@@ -475,7 +475,7 @@ def _run_setup(project_dir: Path):
475
475
  # --- App database ---
476
476
  app_database = get_input_string_or_default(
477
477
  "Enter native app database name",
478
- "DATA_ENGINEERS_DATASECOPS_FRAMEWORK"
478
+ "MONITORIAL_ORBIT"
479
479
  )
480
480
 
481
481
  # --- CoCo agent connection ---
@@ -496,104 +496,146 @@ def _run_setup(project_dir: Path):
496
496
  source_control = SourceControl()
497
497
  work_items = WorkItems()
498
498
  matched_profile = None
499
+ account_identifier = ""
500
+ project_settings_loaded = False
499
501
 
502
+ connected = False
500
503
  try:
501
504
  temp_sf.connect()
502
505
  success_line("Connected")
506
+ connected = True
507
+ except Exception as e:
508
+ warning_line(f"Could not connect to Snowflake: {e}")
509
+ warning_line("You can set profile_name manually in .datasecops.yml")
510
+ warning_line("You may need to create profiles.yml manually")
503
511
 
504
- # Load framework PROJECT settings (for profile_dir, targets)
505
- raw_project = temp_sf.get_framework_config("PROJECT")
506
- if raw_project:
507
- project_settings = ProjectSettings(**{k: v for k, v in raw_project.items() if k in ProjectSettings.model_fields})
512
+ profile_name = ""
513
+ if connected:
514
+ # Each framework config code is fetched independently: a failure (or a missing
515
+ # config row) must not discard config that was already fetched successfully, and
516
+ # must not silently fall back to a default (e.g. GitHub) without saying so.
517
+ try:
518
+ raw_project = temp_sf.get_framework_config("PROJECT")
519
+ if raw_project:
520
+ project_settings = ProjectSettings(**{k: v for k, v in raw_project.items() if k in ProjectSettings.model_fields})
521
+ project_settings_loaded = True
522
+ else:
523
+ warning_line("No PROJECT framework config found in native app - using defaults")
524
+ except Exception as e:
525
+ warning_line(f"Could not load PROJECT framework config: {e}")
508
526
 
509
- # Load SOURCE_CONTROL settings (for source_control_platform)
510
- raw_sc = temp_sf.get_framework_config("SOURCE_CONTROL")
511
- if raw_sc:
512
- source_control = SourceControl(**{k: v for k, v in raw_sc.items() if k in SourceControl.model_fields})
527
+ try:
528
+ raw_sc = temp_sf.get_framework_config("SOURCE_CONTROL")
529
+ if raw_sc:
530
+ if "source_control_platform" not in raw_sc:
531
+ warning_line(f"SOURCE_CONTROL framework config has no source_control_platform - "
532
+ f"defaulting to {source_control.source_control_platform}")
533
+ source_control = SourceControl(**{k: v for k, v in raw_sc.items() if k in SourceControl.model_fields})
534
+ else:
535
+ warning_line(f"No SOURCE_CONTROL framework config found in native app - "
536
+ f"defaulting source control platform to {source_control.source_control_platform}")
537
+ except Exception as e:
538
+ warning_line(f"Could not load SOURCE_CONTROL framework config "
539
+ f"(defaulting source control platform to {source_control.source_control_platform}): {e}")
513
540
 
514
- # Load WORK_ITEMS settings (for ticket system and ticket_number_required)
515
- raw_wi = temp_sf.get_framework_config("WORK_ITEMS")
516
- if raw_wi:
517
- work_items = WorkItems(**{k: v for k, v in raw_wi.items() if k in WorkItems.model_fields})
541
+ try:
542
+ raw_wi = temp_sf.get_framework_config("WORK_ITEMS")
543
+ if raw_wi:
544
+ work_items = WorkItems(**{k: v for k, v in raw_wi.items() if k in WorkItems.model_fields})
545
+ except Exception as e:
546
+ warning_line(f"Could not load WORK_ITEMS framework config: {e}")
518
547
 
519
548
  # Get account identifier for MCP URL construction
520
- account_identifier = ""
521
549
  try:
522
550
  org_rows = temp_sf.execute_query("SELECT CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME() AS acct")
523
551
  account_identifier = org_rows[0]["ACCT"].lower() if org_rows else ""
524
552
  except Exception as e:
525
553
  warning_line(f"Could not determine account identifier (AI Agent MCP will be skipped): {e}")
526
554
 
527
- # --- Resolve profile name ---
528
- from datasecops_cli.utilities.yaml_utils import get_profile_name
529
- profile_name = get_profile_name(project_dir)
530
- if profile_name:
531
- success_line(f"Profile found in dbt_project.yml: {profile_name}")
532
- else:
533
- info_line("No dbt_project.yml found. Fetching project profiles...")
534
- profiles_data = temp_sf.get_project_profiles()
535
- if profiles_data:
536
- profile_names = [p.get("PROFILE_NAME", p.get("profile_name", "")) for p in profiles_data]
537
- profile_names = [n for n in profile_names if n]
538
- if len(profile_names) == 1:
539
- profile_name = profile_names[0]
540
- success_line(f"Auto-selected profile: {profile_name}")
541
- elif len(profile_names) > 1:
542
- info_line(f"Found {len(profile_names)} project profiles:")
543
- profile_name = select_from_list(profile_names, "profile", add_back=False)
555
+ try:
556
+ # --- Resolve profile name ---
557
+ from datasecops_cli.utilities.yaml_utils import get_profile_name
558
+ profile_name = get_profile_name(project_dir)
559
+ if profile_name:
560
+ success_line(f"Profile found in dbt_project.yml: {profile_name}")
561
+ else:
562
+ info_line("No dbt_project.yml found. Fetching project profiles...")
563
+ profiles_data = temp_sf.get_project_profiles()
564
+ if profiles_data:
565
+ profile_names = [p.get("PROFILE_NAME", p.get("profile_name", "")) for p in profiles_data]
566
+ profile_names = [n for n in profile_names if n]
567
+ if len(profile_names) == 1:
568
+ profile_name = profile_names[0]
569
+ success_line(f"Auto-selected profile: {profile_name}")
570
+ elif len(profile_names) > 1:
571
+ info_line(f"Found {len(profile_names)} project profiles:")
572
+ profile_name = select_from_list(profile_names, "profile", add_back=False)
573
+ else:
574
+ warning_line("No profiles found in native app")
544
575
  else:
545
576
  warning_line("No profiles found in native app")
546
- else:
547
- warning_line("No profiles found in native app")
548
-
549
- # Find the full profile object for profiles.yml generation
550
- if profile_name:
551
- profiles_data = temp_sf.get_project_profiles()
552
- if profiles_data:
553
- for p in profiles_data:
554
- pn = p.get("PROFILE_NAME", p.get("profile_name", ""))
555
- if pn == profile_name:
556
- matched_profile = ProjectProfile(**{k.lower(): v for k, v in p.items()})
557
- break
558
-
559
- # --- Ensure profiles.yml has the profile entry ---
560
- if profile_name and matched_profile:
561
- import yaml
562
- from datasecops_cli.utilities.file_utils import ensure_dir, write_file
563
- from datasecops_cli.services.dbt_project_generator import generate_profiles_yml
564
577
 
565
- profiles_dir = Path(project_settings.profile_dir).expanduser()
566
- profiles_path = profiles_dir / "profiles.yml"
578
+ # Find the full profile object for profiles.yml generation
579
+ if profile_name:
580
+ profiles_data = temp_sf.get_project_profiles()
581
+ if profiles_data:
582
+ for p in profiles_data:
583
+ pn = p.get("PROFILE_NAME", p.get("profile_name", ""))
584
+ if pn == profile_name:
585
+ matched_profile = ProjectProfile(**{k.lower(): v for k, v in p.items()})
586
+ break
587
+
588
+ # --- Ensure profiles.yml has the profile entry ---
589
+ if profile_name and matched_profile and not project_settings_loaded:
590
+ warning_line(f"Skipping profiles.yml for '{profile_name}': PROJECT framework config "
591
+ "was not loaded, so the correct CI identity/targets aren't known")
592
+ elif profile_name and matched_profile:
593
+ import yaml
594
+ from datasecops_cli.utilities.file_utils import ensure_dir, write_file
595
+ from datasecops_cli.services.dbt_project_generator import generate_profiles_yml
596
+
597
+ profiles_dir = Path(project_settings.profile_dir).expanduser()
598
+ profiles_path = profiles_dir / "profiles.yml"
599
+
600
+ # Read existing profiles.yml or start fresh
601
+ existing_profiles = {}
602
+ if profiles_path.exists():
603
+ with open(profiles_path, "r", encoding="utf-8") as f:
604
+ existing_profiles = yaml.safe_load(f) or {}
567
605
 
568
- # Read existing profiles.yml or start fresh
569
- existing_profiles = {}
570
- if profiles_path.exists():
571
- with open(profiles_path, "r", encoding="utf-8") as f:
572
- existing_profiles = yaml.safe_load(f) or {}
573
-
574
- if profile_name in existing_profiles:
575
- success_line(f"Profile '{profile_name}' already exists in {profiles_path}")
576
- else:
577
- info_line(f"Adding profile '{profile_name}' to {profiles_path}...")
578
606
  account_info = temp_sf.get_account_info()
579
607
  account = account_info.get("ACCOUNT", account_info.get("account", ""))
580
608
  user = account_info.get("USER_NAME", account_info.get("user_name", ""))
581
609
  new_profiles_yaml = generate_profiles_yml(matched_profile, project_settings, account, user)
582
610
  new_profiles = yaml.safe_load(new_profiles_yaml) or {}
583
- existing_profiles.update(new_profiles)
584
- ensure_dir(profiles_dir)
585
- write_file(profiles_path, yaml.dump(existing_profiles, default_flow_style=False, sort_keys=False))
586
- success_line(f"Profile '{profile_name}' written to {profiles_path}")
611
+ new_outputs = new_profiles.get(profile_name, {}).get("outputs", {})
612
+
613
+ if profile_name in existing_profiles:
614
+ # Refresh the CI/deployment targets every run (so a CI identity change in
615
+ # the native app takes effect), but keep an existing local-dev target as
616
+ # the developer may have customized it.
617
+ existing_outputs = existing_profiles[profile_name].setdefault("outputs", {})
618
+ for name, output in new_outputs.items():
619
+ if name == "local-dev" and name in existing_outputs:
620
+ continue
621
+ existing_outputs[name] = output
622
+ existing_profiles[profile_name].setdefault("target", new_profiles[profile_name]["target"])
623
+ write_file(profiles_path, yaml.dump(existing_profiles, default_flow_style=False, sort_keys=False))
624
+ success_line(f"Updated deployment targets for profile '{profile_name}' in {profiles_path}")
625
+ else:
626
+ info_line(f"Adding profile '{profile_name}' to {profiles_path}...")
627
+ existing_profiles.update(new_profiles)
628
+ ensure_dir(profiles_dir)
629
+ write_file(profiles_path, yaml.dump(existing_profiles, default_flow_style=False, sort_keys=False))
630
+ success_line(f"Profile '{profile_name}' written to {profiles_path}")
631
+ except Exception as e:
632
+ warning_line(f"Could not resolve the project profile / write profiles.yml: {e}")
633
+ warning_line("You can set profile_name manually in .datasecops.yml")
634
+ warning_line("You may need to create profiles.yml manually")
635
+ from datasecops_cli.utilities.yaml_utils import get_profile_name
636
+ profile_name = get_profile_name(project_dir) or ""
587
637
 
588
- except Exception as e:
589
- warning_line(f"Could not connect to Snowflake: {e}")
590
- warning_line("You can set profile_name manually in .datasecops.yml")
591
- warning_line("You may need to create profiles.yml manually")
592
- # Fall back to reading profile from dbt_project.yml if available
593
- from datasecops_cli.utilities.yaml_utils import get_profile_name
594
- profile_name = get_profile_name(project_dir) or ""
595
- finally:
596
- temp_sf.close()
638
+ temp_sf.close()
597
639
 
598
640
  # --- Write config ---
599
641
  config_data = {
@@ -860,7 +902,9 @@ def _run_interactive(config: Config):
860
902
  dbt_runner = DbtRunner(
861
903
  project_dir=config.dbt_project_dir,
862
904
  profiles_dir=config.get_dbt_profiles_dir(),
863
- target=config.project_settings.get_default_target().target_name if config.get_default_target() else "dev",
905
+ # Always the developer's own identity, never the framework's CI/deployment
906
+ # default target (which may be a deploy-only target running as the CI identity).
907
+ target="local-dev",
864
908
  engine=config.datasecops.dbt_engine or "dbt_core"
865
909
  )
866
910
 
@@ -40,6 +40,9 @@ class ProjectSettings(BaseModel):
40
40
  execution_mode: str = "dbt_cli"
41
41
  dbt_engine: str = "dbt_core"
42
42
  ai_agents_enabled: bool = False
43
+ ci_username: str = ""
44
+ ci_role: str = ""
45
+ ci_warehouse: str = ""
43
46
  targets: list[DbtTarget] = Field(default_factory=lambda: [
44
47
  DbtTarget(target_name="dev", branch_name="dev", target_role="DEVELOPERS", target_warehouse="DEV_WH", is_default=True),
45
48
  DbtTarget(target_name="test", branch_name="test", target_role="DATAOPS_ADMIN", target_warehouse="DATAOPS_WH"),
@@ -112,7 +112,9 @@ class BootstrapService:
112
112
  dbt_runner = DbtRunner(
113
113
  project_dir=dbt_project_dir,
114
114
  profiles_dir=Path(self.project_settings.profile_dir).expanduser(),
115
- target=self.project_settings.get_default_target().target_name if self.project_settings.get_default_target() else "dev",
115
+ # Always the developer's own identity, never the framework's CI/deployment
116
+ # default target.
117
+ target="local-dev",
116
118
  engine=self.datasecops_config.dbt_engine,
117
119
  )
118
120
  result = dbt_runner.deps()