datasecops-cli 0.4.7__tar.gz → 0.4.9__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.
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/CHANGELOG.md +28 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/PKG-INFO +1 -1
- datasecops_cli-0.4.9/docs/git-operations.md +205 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/pyproject.toml +1 -1
- datasecops_cli-0.4.9/src/datasecops_cli/__init__.py +1 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/main.py +33 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/menus/configuration.py +17 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/menus/git_operations.py +111 -40
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/models/project_config.py +1 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/dbt_runner.py +63 -5
- datasecops_cli-0.4.9/src/datasecops_cli/services/git_service.py +399 -0
- datasecops_cli-0.4.7/src/datasecops_cli/__init__.py +0 -1
- datasecops_cli-0.4.7/src/datasecops_cli/services/git_service.py +0 -183
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/.github/workflows/auto-tag.yml +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/.github/workflows/publish-cli.yml +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/.gitignore +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/DEVELOPMENT.md +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/LICENSE +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/README.md +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/docs/getting-started.md +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/docs/legacy.md +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/docs/legacy_plan_of_action.md +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/docs/mcp-server.md +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/mcp-servers.json +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/setup.ps1 +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/setup.sh +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/config.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/menus/__init__.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/menus/development.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/menus/downloads.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/models/__init__.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/models/git_helpers.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/__init__.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/bootstrap_service.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/dbt_project_generator.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/directory_scaffolder.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/download_service.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/linting_service.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/skill_service.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/snowflake_service.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/services/upstream_service.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/utilities/__init__.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/utilities/display.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/utilities/file_utils.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_cli/utilities/yaml_utils.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_mcp/__init__.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_mcp/__main__.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_mcp/connection.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/src/datasecops_mcp/server.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/tests/__init__.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/tests/test_config.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/tests/test_file_utils.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/tests/test_main.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/tests/test_models.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/tests/test_version.py +0 -0
- {datasecops_cli-0.4.7 → datasecops_cli-0.4.9}/tests/test_yaml_utils.py +0 -0
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the DataSecOps CLI are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.9] - 2026-05-21
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Remove from test** — new git menu option to remove a branch from the test integration branch. Displays squash-merged branches by name and drops the selected commit from test's history via rebase, then force-pushes. The test branch is never deleted.
|
|
10
|
+
- **Branch cleanup** — new option in branch management to delete all local branches (except main, test, and current) and prune stale remote tracking references in one step.
|
|
11
|
+
- **Git operations documentation** — `docs/git-operations.md` documenting the full branch strategy, menu structure, and behaviour of each operation.
|
|
12
|
+
- **Ticket integration plan** — `plans/ticket-integration-plan.md` specifying the design for Azure DevOps, Jira, and GitHub Projects ticket fetching during branch creation.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **Squash merge into test rebases onto main first** — prevents drift by rebasing the test branch onto `origin/main` before squash-merging, ensuring test always has main as its base with squash commits layered on top.
|
|
17
|
+
- **Branch creation uses configured `branch_format`** — branch names are now built using the `branch_format` field from the framework's SOURCE_CONTROL config instead of a hardcoded pattern.
|
|
18
|
+
- **Deploy to prod enforced from main only** — production deployments are hardcoded to `force=False` and can only originate from the main branch.
|
|
19
|
+
- **Protected branches** — `main` and `test` cannot be deleted via delete, cleanup, or reset operations.
|
|
20
|
+
- **Stash/restore on branch-switching operations** — squash merge and remove from test now stash uncommitted changes before switching branches and restore them afterward, preventing dirty-tree errors.
|
|
21
|
+
- **Test branch auto-creation** — if neither local nor remote `test` branch exists, it is created from `origin/main` automatically during squash merge or remove operations.
|
|
22
|
+
|
|
23
|
+
## [0.4.8] - 2026-05-20
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **dbt Fusion update menu option** — new `[7] dbtf update` in the Configuration menu runs `dbtf system update` to upgrade dbt Fusion to the latest version. Checks that `dbtf` is on PATH before attempting the update.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **`dbt docs generate` replaced with `dbt compile --write-catalog` for dbt Fusion** — dbt Fusion does not support `dbt docs generate`. The docs menu option now runs `dbtf compile --write-catalog` when using dbt Fusion, writing `catalog.json` to the target directory. Serving docs automatically downloads `index.html` from dbt-core if missing, launches `python -m http.server 8080` in a new terminal window, and opens the browser at `http://localhost:8080`. The CLI is not blocked — close the server terminal when done.
|
|
32
|
+
|
|
5
33
|
## [0.4.7] - 2026-05-19
|
|
6
34
|
|
|
7
35
|
### Fixed
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Git Operations
|
|
2
|
+
|
|
3
|
+
The CLI provides a structured git workflow designed around a branching strategy where feature branches are squash-merged into a `test` branch for integration testing, and only `main` is deployed to production.
|
|
4
|
+
|
|
5
|
+
## Branch Strategy
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
main ─────────────────────────────────────── prod
|
|
9
|
+
│
|
|
10
|
+
├── feature/ticket_name ──┐
|
|
11
|
+
├── bugfix/ticket_name ──┤── squash merge ──► test
|
|
12
|
+
└── hotfix/ticket_name ──┘
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- `main` is the source of truth and the only branch that deploys to production
|
|
16
|
+
- `test` is an integration branch built on top of main (rebased to prevent drift)
|
|
17
|
+
- Feature/bugfix/hotfix branches are created from main and squash-merged into test
|
|
18
|
+
- Branches can be removed from test without affecting main or other branches on test
|
|
19
|
+
|
|
20
|
+
## Main Menu
|
|
21
|
+
|
|
22
|
+
| Option | Name | Description |
|
|
23
|
+
|--------|------|-------------|
|
|
24
|
+
| 1 | Branching | Manage branches (create, checkout, switch, delete, prune, cleanup, reset) |
|
|
25
|
+
| 2 | Commit | Stage all changes and commit with a message, then push |
|
|
26
|
+
| 3 | Push | Push current branch to remote |
|
|
27
|
+
| 4 | Pull | Pull latest changes from remote for current branch |
|
|
28
|
+
| 5 | Rebase | Rebase current branch with main |
|
|
29
|
+
| 6 | Deploy | Deploy to an environment branch (e.g. test, prod) |
|
|
30
|
+
| 7 | Squash to test | Squash merge current branch into test |
|
|
31
|
+
| 8 | Remove from test | Remove a branch's changes from test |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Operations Detail
|
|
36
|
+
|
|
37
|
+
### 1. Branching
|
|
38
|
+
|
|
39
|
+
Submenu for branch management operations.
|
|
40
|
+
|
|
41
|
+
| Option | Name | Description |
|
|
42
|
+
|--------|------|-------------|
|
|
43
|
+
| 1 | New | Create a new branch from main |
|
|
44
|
+
| 2 | Checkout | Checkout a remote branch locally |
|
|
45
|
+
| 3 | Switch | Switch to an existing local branch |
|
|
46
|
+
| 4 | Delete | Delete a local branch (and its remote) |
|
|
47
|
+
| 5 | Prune | Remove stale remote tracking references |
|
|
48
|
+
| 6 | Cleanup | Delete all local branches (except main, test, current) and prune remote |
|
|
49
|
+
| 7 | Reset | Hard reset to main and delete all local branches |
|
|
50
|
+
|
|
51
|
+
#### New Branch
|
|
52
|
+
|
|
53
|
+
Creates a branch using the naming convention `{type}/{ticket}_{name}`:
|
|
54
|
+
- Prompts for branch type (feature, bugfix, hotfix — or configured types)
|
|
55
|
+
- Prompts for ticket number (required or optional based on config)
|
|
56
|
+
- Prompts for branch name
|
|
57
|
+
- Creates from `origin/main`, checks out, and pushes with upstream tracking
|
|
58
|
+
|
|
59
|
+
#### Delete Branch
|
|
60
|
+
|
|
61
|
+
- Cannot delete the current branch
|
|
62
|
+
- Cannot delete protected branches (`main`, `test`)
|
|
63
|
+
- Deletes both the local branch and the remote branch
|
|
64
|
+
|
|
65
|
+
#### Cleanup
|
|
66
|
+
|
|
67
|
+
- Deletes all local branches except `main`, `test`, and the current branch
|
|
68
|
+
- Prunes stale remote tracking references
|
|
69
|
+
|
|
70
|
+
#### Reset to Main
|
|
71
|
+
|
|
72
|
+
- Checks out main and hard resets to `origin/main`
|
|
73
|
+
- Deletes all local branches except `main` and `test`
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### 2. Commit
|
|
78
|
+
|
|
79
|
+
- Detects uncommitted changes (modified and untracked files)
|
|
80
|
+
- Displays a summary of changed files
|
|
81
|
+
- Prompts for a commit message
|
|
82
|
+
- Stages all changes, commits, and pushes to remote
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### 3. Push
|
|
87
|
+
|
|
88
|
+
Pushes the current branch to its remote counterpart.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### 4. Pull
|
|
93
|
+
|
|
94
|
+
Pulls the latest changes from remote for the current branch.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### 5. Rebase
|
|
99
|
+
|
|
100
|
+
Submenu for rebase operations.
|
|
101
|
+
|
|
102
|
+
| Option | Name | Description |
|
|
103
|
+
|--------|------|-------------|
|
|
104
|
+
| 1 | Rebase | Standard rebase of current branch onto `origin/main` |
|
|
105
|
+
| 2 | Squash & rebase | Squash all commits into one, then rebase onto main |
|
|
106
|
+
| 3 | Continue | Continue rebase after resolving conflicts |
|
|
107
|
+
| 4 | Abort | Abort an in-progress rebase |
|
|
108
|
+
|
|
109
|
+
#### Standard Rebase
|
|
110
|
+
|
|
111
|
+
Fetches from origin and rebases the current branch onto `origin/main`. If conflicts occur, you can resolve them and use "continue" or "abort".
|
|
112
|
+
|
|
113
|
+
#### Squash & Rebase
|
|
114
|
+
|
|
115
|
+
Soft-resets to the main branch point, creates a single squash commit, then rebases onto `origin/main`. This gives you a clean single commit on top of main.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### 6. Deploy
|
|
120
|
+
|
|
121
|
+
Pushes the current branch to a configured deployment branch.
|
|
122
|
+
|
|
123
|
+
**Production rules:**
|
|
124
|
+
- Production deployments can only come from the `main` branch
|
|
125
|
+
- If not on main, the CLI will offer to switch to main first
|
|
126
|
+
- Production pushes are never force-pushed
|
|
127
|
+
|
|
128
|
+
**Non-production environments:**
|
|
129
|
+
- Any branch can be pushed to non-production environments
|
|
130
|
+
- Force-push is used for non-production deployments
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### 7. Squash to Test
|
|
135
|
+
|
|
136
|
+
Squash merges the current feature branch into the `test` integration branch.
|
|
137
|
+
|
|
138
|
+
**Pre-flight checks (before merge):**
|
|
139
|
+
|
|
140
|
+
1. **Branch must be up to date with main** — if your branch is behind `origin/main`, the merge is rejected. Rebase with main first.
|
|
141
|
+
2. **File conflict detection** — checks if any files modified on your branch are also modified by another branch's squash-merge commit currently on test:
|
|
142
|
+
- If the conflicting branch **still exists on remote** → merge is **rejected**. You must resolve with the branch owner or remove that branch from test first.
|
|
143
|
+
- If the conflicting branch **no longer exists** (stale) → the stale commit is **automatically removed** from test before proceeding.
|
|
144
|
+
|
|
145
|
+
**What it does (after pre-flight passes):**
|
|
146
|
+
1. Stashes any uncommitted changes
|
|
147
|
+
2. Fetches latest from origin
|
|
148
|
+
3. Checks out the `test` branch (creates it from `origin/main` if it doesn't exist)
|
|
149
|
+
4. Pulls latest test
|
|
150
|
+
5. Rebases test onto `origin/main` to prevent drift from main
|
|
151
|
+
6. Squash merges the feature branch into test
|
|
152
|
+
7. Commits with message: `squash merge: {branch_name} into test`
|
|
153
|
+
8. Force-pushes test (required due to the rebase)
|
|
154
|
+
9. Checks out the original branch and restores stashed changes
|
|
155
|
+
|
|
156
|
+
**Key behaviours:**
|
|
157
|
+
- The test branch always has `main` as its base — squash commits are layered on top
|
|
158
|
+
- Force-push is used because the rebase rewrites test's history to stay aligned with main
|
|
159
|
+
- The commit message naming convention enables the "remove from test" feature
|
|
160
|
+
- File overlap detection prevents two active branches from silently overwriting each other's changes on test
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### 8. Remove from Test
|
|
165
|
+
|
|
166
|
+
Removes a previously squash-merged branch from the test branch by dropping its commit from history.
|
|
167
|
+
|
|
168
|
+
**What it does:**
|
|
169
|
+
1. Fetches latest and lists all squash-merge commits on test (identified by commit message prefix `squash merge:`)
|
|
170
|
+
2. Displays the branches currently on test by name
|
|
171
|
+
3. User selects which branch to remove
|
|
172
|
+
4. Stashes any uncommitted changes
|
|
173
|
+
5. Checks out test
|
|
174
|
+
6. Rebases test, dropping the selected commit: `git rebase --onto <commit>^ <commit> test`
|
|
175
|
+
7. Force-pushes test
|
|
176
|
+
8. Checks out the original branch and restores stashed changes
|
|
177
|
+
|
|
178
|
+
**Key behaviours:**
|
|
179
|
+
- The commit is completely removed from test's history (not reverted)
|
|
180
|
+
- All other branches on test remain intact
|
|
181
|
+
- The test branch itself is never deleted
|
|
182
|
+
- If the rebase fails (e.g. conflicts), it aborts and returns to the original branch
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Protected Branches
|
|
187
|
+
|
|
188
|
+
The following branches are protected and cannot be deleted:
|
|
189
|
+
- `main` — source of truth, deploys to production
|
|
190
|
+
- `test` — integration branch for testing
|
|
191
|
+
|
|
192
|
+
These are protected across all operations: delete, cleanup, and reset.
|
|
193
|
+
|
|
194
|
+
## Error Handling
|
|
195
|
+
|
|
196
|
+
All operations that switch branches follow a safe pattern:
|
|
197
|
+
1. Stash uncommitted changes before switching
|
|
198
|
+
2. Perform the operation
|
|
199
|
+
3. Switch back to the original branch
|
|
200
|
+
4. Restore stashed changes
|
|
201
|
+
|
|
202
|
+
If any step fails, the error handler will:
|
|
203
|
+
- Abort any in-progress rebase/revert
|
|
204
|
+
- Switch back to the original branch
|
|
205
|
+
- Restore stashed changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.8"
|
|
@@ -196,6 +196,14 @@ def _run_setup(project_dir: Path):
|
|
|
196
196
|
if raw_sc:
|
|
197
197
|
source_control = SourceControl(**{k: v for k, v in raw_sc.items() if k in SourceControl.model_fields})
|
|
198
198
|
|
|
199
|
+
# Get account identifier for MCP URL construction
|
|
200
|
+
account_identifier = ""
|
|
201
|
+
try:
|
|
202
|
+
org_rows = temp_sf.execute_query("SELECT CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME() AS acct")
|
|
203
|
+
account_identifier = org_rows[0]["ACCT"].lower() if org_rows else ""
|
|
204
|
+
except Exception as e:
|
|
205
|
+
warning_line(f"Could not determine account identifier (AI Agent MCP will be skipped): {e}")
|
|
206
|
+
|
|
199
207
|
# --- Resolve profile name ---
|
|
200
208
|
from datasecops_cli.utilities.yaml_utils import get_profile_name
|
|
201
209
|
profile_name = get_profile_name(project_dir)
|
|
@@ -286,6 +294,11 @@ def _run_setup(project_dir: Path):
|
|
|
286
294
|
platform_choice = source_control.source_control_platform
|
|
287
295
|
info_line(f"Platform: {platform_choice}")
|
|
288
296
|
|
|
297
|
+
# Build AI Agent MCP URL once (used in both Cortex Code and mcp.json paths)
|
|
298
|
+
ai_agent_mcp_url = ""
|
|
299
|
+
if project_settings.ai_agents_enabled and account_identifier:
|
|
300
|
+
ai_agent_mcp_url = f"https://{account_identifier}.snowflakecomputing.com/api/v2/databases/{app_database}/schemas/AI/mcp-servers/DATASECOPS_MCP"
|
|
301
|
+
|
|
289
302
|
info_line("")
|
|
290
303
|
info_line("Which AI tool are you configuring?")
|
|
291
304
|
tool_options = ["VS Code (Copilot)", "Cursor", "Claude Code"]
|
|
@@ -304,6 +317,18 @@ def _run_setup(project_dir: Path):
|
|
|
304
317
|
except FileNotFoundError:
|
|
305
318
|
warning_line("cortex not found — run manually: cortex mcp add datasecops-framework -- datasecops-mcp")
|
|
306
319
|
|
|
320
|
+
# Add AI Agent MCP server if enabled
|
|
321
|
+
if ai_agent_mcp_url:
|
|
322
|
+
try:
|
|
323
|
+
result = subprocess.run(["cortex", "mcp", "add", "datasecops-docs", ai_agent_mcp_url, "--transport", "http"],
|
|
324
|
+
capture_output=True, text=True)
|
|
325
|
+
if result.returncode == 0:
|
|
326
|
+
success_line("Added datasecops-docs MCP server (AI Agent documentation search)")
|
|
327
|
+
else:
|
|
328
|
+
warning_line(f"Failed to add AI Agent MCP server: {result.stderr.strip() or 'unknown error'}")
|
|
329
|
+
except Exception as agent_err:
|
|
330
|
+
warning_line(f"Could not add AI Agent MCP server: {agent_err}")
|
|
331
|
+
|
|
307
332
|
if platform_choice == "GitHub":
|
|
308
333
|
github_token = get_input_string("Enter your GitHub PAT (or press Enter to skip): ", allow_empty=True)
|
|
309
334
|
if github_token:
|
|
@@ -343,6 +368,14 @@ def _run_setup(project_dir: Path):
|
|
|
343
368
|
|
|
344
369
|
servers = {"datasecops-framework": {"command": "datasecops-mcp", "args": []}}
|
|
345
370
|
|
|
371
|
+
# Add AI Agent MCP server if enabled
|
|
372
|
+
if ai_agent_mcp_url:
|
|
373
|
+
servers["datasecops-docs"] = {
|
|
374
|
+
"url": ai_agent_mcp_url,
|
|
375
|
+
"headers": {"Authorization": "Bearer <YOUR_PAT>"},
|
|
376
|
+
}
|
|
377
|
+
info_line("Note: Replace <YOUR_PAT> in datasecops-docs headers with your Snowflake PAT")
|
|
378
|
+
|
|
346
379
|
if platform_choice == "GitHub":
|
|
347
380
|
servers["github"] = {
|
|
348
381
|
"command": "npx",
|
|
@@ -51,6 +51,8 @@ class ConfigurationMenu:
|
|
|
51
51
|
self._cortex_upgrade()
|
|
52
52
|
elif option == 6:
|
|
53
53
|
self._toggle_dbt_engine()
|
|
54
|
+
elif option == 7:
|
|
55
|
+
self._dbtf_update()
|
|
54
56
|
self._menu()
|
|
55
57
|
option = get_input_number("Choose an option: ")
|
|
56
58
|
|
|
@@ -64,6 +66,7 @@ class ConfigurationMenu:
|
|
|
64
66
|
menu_option(4, "new project - Initialize a new dbt project with framework profiles")
|
|
65
67
|
menu_option(5, "cortex update - Update Cortex Code to the latest version")
|
|
66
68
|
menu_option(6, f"dbt engine - Switch dbt engine (current: {engine_label})")
|
|
69
|
+
menu_option(7, "dbtf update - Update dbt Fusion to the latest version")
|
|
67
70
|
menu_option(0, "back - Return to main menu")
|
|
68
71
|
|
|
69
72
|
def _install_dbt_requirements(self) -> None:
|
|
@@ -288,3 +291,17 @@ class ConfigurationMenu:
|
|
|
288
291
|
success_line(f"dbt engine switched to {self.datasecops_config.get_engine_label()} ({new_engine})")
|
|
289
292
|
warning_line("Restart the CLI for the change to take effect.")
|
|
290
293
|
complete_action()
|
|
294
|
+
|
|
295
|
+
def _dbtf_update(self) -> None:
|
|
296
|
+
"""Run dbtf system update to upgrade to the latest version."""
|
|
297
|
+
display_action_header("Update dbt Fusion")
|
|
298
|
+
if not shutil.which("dbtf"):
|
|
299
|
+
error_line("dbt Fusion (dbtf) not found. Install it first.")
|
|
300
|
+
complete_action()
|
|
301
|
+
return
|
|
302
|
+
try:
|
|
303
|
+
info_line("Checking for updates...")
|
|
304
|
+
subprocess.run(["dbtf", "system", "update"])
|
|
305
|
+
except FileNotFoundError:
|
|
306
|
+
error_line("dbt Fusion (dbtf) not found.")
|
|
307
|
+
complete_action()
|
|
@@ -15,7 +15,7 @@ class GitOperationsMenu:
|
|
|
15
15
|
self.source_control = source_control
|
|
16
16
|
self.deployment_branches = deployment_branches
|
|
17
17
|
self.profile_name = profile_name
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
def show(self) -> None:
|
|
20
20
|
self._menu()
|
|
21
21
|
option = get_input_number("Choose an option: ")
|
|
@@ -36,9 +36,11 @@ class GitOperationsMenu:
|
|
|
36
36
|
self._squash_merge_test()
|
|
37
37
|
elif option == 8:
|
|
38
38
|
self._cherry_pick_test()
|
|
39
|
+
elif option == 9:
|
|
40
|
+
self._deploy_prod()
|
|
39
41
|
self._menu()
|
|
40
42
|
option = get_input_number("Choose an option: ")
|
|
41
|
-
|
|
43
|
+
|
|
42
44
|
def _menu(self) -> None:
|
|
43
45
|
clear()
|
|
44
46
|
section_header("Git Operations", self.profile_name, self.git.get_current_branch())
|
|
@@ -49,9 +51,10 @@ class GitOperationsMenu:
|
|
|
49
51
|
menu_option(5, "rebase - Rebase with main")
|
|
50
52
|
menu_option(6, "deploy - Deploy to environment")
|
|
51
53
|
menu_option(7, "squash to test - Squash merge into test")
|
|
52
|
-
menu_option(8, "
|
|
54
|
+
menu_option(8, "remove from test - Remove a branch from test")
|
|
55
|
+
menu_option(9, "deploy to prod - Deploy main to production")
|
|
53
56
|
menu_option(0, "back - Return to main menu")
|
|
54
|
-
|
|
57
|
+
|
|
55
58
|
def _branch_menu(self) -> None:
|
|
56
59
|
clear()
|
|
57
60
|
display_action_header("Branch Management")
|
|
@@ -60,7 +63,8 @@ class GitOperationsMenu:
|
|
|
60
63
|
menu_option(3, "switch - Switch to local branch")
|
|
61
64
|
menu_option(4, "delete - Delete local branch")
|
|
62
65
|
menu_option(5, "prune - Prune remote branches")
|
|
63
|
-
menu_option(6, "
|
|
66
|
+
menu_option(6, "cleanup - Delete local branches & prune remote")
|
|
67
|
+
menu_option(7, "reset - Reset to main")
|
|
64
68
|
menu_option(0, "back - Return to git menu")
|
|
65
69
|
option = get_input_number("Choose an option: ")
|
|
66
70
|
if option == 1:
|
|
@@ -86,19 +90,21 @@ class GitOperationsMenu:
|
|
|
86
90
|
elif option == 5:
|
|
87
91
|
self.git.prune_remote_branches()
|
|
88
92
|
elif option == 6:
|
|
93
|
+
self.git.cleanup_branches()
|
|
94
|
+
elif option == 7:
|
|
89
95
|
if get_input_true_false("This will delete all local branches. Continue?", "n"):
|
|
90
96
|
self.git.reset_to_main()
|
|
91
97
|
complete_action()
|
|
92
|
-
|
|
98
|
+
|
|
93
99
|
def _create_branch(self) -> None:
|
|
94
100
|
branch_types = {bt.name: bt.name for bt in self.source_control.branch_types}
|
|
95
101
|
if not branch_types:
|
|
96
102
|
branch_types = {"feature": "feature", "bugfix": "bugfix", "hotfix": "hotfix"}
|
|
97
|
-
|
|
103
|
+
|
|
98
104
|
selected_type = select_from_dict(branch_types, "branch type")
|
|
99
105
|
if selected_type == "back":
|
|
100
106
|
return
|
|
101
|
-
|
|
107
|
+
|
|
102
108
|
ticket = ""
|
|
103
109
|
if self.source_control.ticket_number_required:
|
|
104
110
|
ticket = get_input_string("Enter ticket number: ")
|
|
@@ -106,51 +112,59 @@ class GitOperationsMenu:
|
|
|
106
112
|
return
|
|
107
113
|
else:
|
|
108
114
|
ticket = get_input_string("Enter ticket number (or press Enter to skip): ", allow_empty=True)
|
|
109
|
-
|
|
115
|
+
|
|
110
116
|
name = get_input_string("Enter branch name: ")
|
|
111
117
|
if name == "0":
|
|
112
118
|
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
|
|
120
|
+
branch_name = name.replace(" ", "-").lower()
|
|
121
|
+
fmt = self.source_control.branch_format
|
|
122
|
+
if ticket:
|
|
123
|
+
full_name = fmt.format(branch_type=selected_type, branch_name=f"{ticket}_{branch_name}")
|
|
124
|
+
else:
|
|
125
|
+
full_name = fmt.format(branch_type=selected_type, branch_name=branch_name)
|
|
126
|
+
|
|
127
|
+
self.git.create_branch(full_name)
|
|
128
|
+
|
|
116
129
|
def _commit(self) -> None:
|
|
117
130
|
display_action_header("Commit Changes")
|
|
118
131
|
if not self.git.is_dirty():
|
|
119
132
|
info_line("No changes to commit")
|
|
120
133
|
complete_action()
|
|
121
134
|
return
|
|
122
|
-
|
|
135
|
+
|
|
123
136
|
info_line(f"{self.git.get_uncommitted_file_count()} change(s) detected")
|
|
124
137
|
for f in self.git.get_changed_files():
|
|
125
138
|
info_line(f" M {f.file}")
|
|
126
139
|
for f in self.git.get_new_files():
|
|
127
140
|
info_line(f" + {f.file}")
|
|
128
|
-
|
|
141
|
+
|
|
129
142
|
message = get_input_string("Enter commit message (0 to cancel): ")
|
|
130
143
|
if message == "0":
|
|
131
144
|
return
|
|
132
|
-
|
|
145
|
+
|
|
133
146
|
self.git.commit_changes(message)
|
|
134
147
|
self.git.push_branch()
|
|
135
148
|
complete_action()
|
|
136
|
-
|
|
149
|
+
|
|
137
150
|
def _push(self) -> None:
|
|
138
151
|
display_action_header("Push")
|
|
139
152
|
self.git.push_branch()
|
|
140
153
|
complete_action()
|
|
141
|
-
|
|
154
|
+
|
|
142
155
|
def _pull(self) -> None:
|
|
143
156
|
display_action_header("Pull")
|
|
144
157
|
self.git.pull_branch()
|
|
145
158
|
complete_action()
|
|
146
|
-
|
|
159
|
+
|
|
147
160
|
def _rebase_menu(self) -> None:
|
|
148
161
|
clear()
|
|
149
162
|
display_action_header("Rebase Options")
|
|
150
163
|
menu_option(1, "rebase - Standard rebase with main")
|
|
151
164
|
menu_option(2, "squash & rebase - Squash commits then rebase")
|
|
152
|
-
menu_option(3, "
|
|
153
|
-
menu_option(4, "
|
|
165
|
+
menu_option(3, "squash - Squash all commits into one")
|
|
166
|
+
menu_option(4, "continue - Continue after resolving conflicts")
|
|
167
|
+
menu_option(5, "abort - Abort rebase")
|
|
154
168
|
menu_option(0, "back - Return to git menu")
|
|
155
169
|
option = get_input_number("Choose an option: ")
|
|
156
170
|
if option == 1:
|
|
@@ -158,56 +172,113 @@ class GitOperationsMenu:
|
|
|
158
172
|
elif option == 2:
|
|
159
173
|
self.git.squash_and_rebase()
|
|
160
174
|
elif option == 3:
|
|
161
|
-
self.git.
|
|
175
|
+
self.git.squash_commits()
|
|
162
176
|
elif option == 4:
|
|
177
|
+
self.git.rebase_continue()
|
|
178
|
+
elif option == 5:
|
|
163
179
|
self.git.rebase_abort()
|
|
164
180
|
complete_action()
|
|
165
|
-
|
|
181
|
+
|
|
166
182
|
def _deploy(self) -> None:
|
|
167
183
|
display_action_header("Deploy to Environment")
|
|
168
|
-
|
|
184
|
+
|
|
185
|
+
# Build environment list from framework config, filtering non-deployable environments
|
|
186
|
+
excluded = {"unit-test", "ci", "local-dev", "prod", "test"}
|
|
187
|
+
environments = {}
|
|
188
|
+
if self.source_control.environments:
|
|
189
|
+
for env in self.source_control.environments:
|
|
190
|
+
if env.branch_name and env.branch_name.lower() not in excluded:
|
|
191
|
+
environments[env.branch_name] = env.branch_name
|
|
192
|
+
|
|
193
|
+
# Fall back to deployment_branches if no framework environments configured
|
|
194
|
+
if not environments:
|
|
195
|
+
environments = {
|
|
196
|
+
k: v for k, v in self.deployment_branches.items()
|
|
197
|
+
if k.lower() not in excluded
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if not environments:
|
|
169
201
|
error_line("No deployment branches configured")
|
|
170
202
|
complete_action()
|
|
171
203
|
return
|
|
172
|
-
|
|
173
|
-
selected = select_from_dict(
|
|
204
|
+
|
|
205
|
+
selected = select_from_dict(environments, "environment")
|
|
174
206
|
if selected == "back":
|
|
175
207
|
return
|
|
176
|
-
|
|
208
|
+
|
|
209
|
+
current = self.git.get_current_branch()
|
|
210
|
+
info_line(f"This will squash {current} and force-push to {selected}")
|
|
211
|
+
if get_input_true_false("Continue?", "n"):
|
|
212
|
+
self.git.squash_push_to_destination(selected)
|
|
213
|
+
complete_action()
|
|
214
|
+
|
|
215
|
+
def _deploy_prod(self) -> None:
|
|
216
|
+
display_action_header("Deploy to Production")
|
|
177
217
|
current = self.git.get_current_branch()
|
|
178
|
-
if
|
|
218
|
+
if current.lower() != "main":
|
|
179
219
|
warning_line("Production deployments must come from the main branch")
|
|
180
220
|
if get_input_true_false("Switch to main and deploy?", "n"):
|
|
181
221
|
self.git.checkout_branch("main")
|
|
182
222
|
self.git.pull_branch()
|
|
183
223
|
else:
|
|
224
|
+
complete_action()
|
|
184
225
|
return
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
226
|
+
info_line("This will push main to prod (no force-push)")
|
|
227
|
+
if get_input_true_false("Continue?", "n"):
|
|
228
|
+
self.git.push_branch_to_destination("prod", force=False)
|
|
188
229
|
complete_action()
|
|
189
|
-
|
|
230
|
+
|
|
190
231
|
def _squash_merge_test(self) -> None:
|
|
191
232
|
display_action_header("Squash Merge into Test")
|
|
192
233
|
current = self.git.get_current_branch()
|
|
193
234
|
info_line(f"This will squash merge {current} into the test branch")
|
|
235
|
+
|
|
236
|
+
# Check current branch is up to date with main
|
|
237
|
+
if not self.git.is_branch_up_to_date_with_main():
|
|
238
|
+
error_line("Your branch is not up to date with main. Rebase with main first.")
|
|
239
|
+
complete_action()
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# Check for file overlaps with other branches on test
|
|
243
|
+
conflicts = self.git.get_test_file_conflicts()
|
|
244
|
+
if conflicts:
|
|
245
|
+
# If conflicting branch still exists, reject
|
|
246
|
+
active_conflicts = [c for c in conflicts if c["branch_exists"]]
|
|
247
|
+
if active_conflicts:
|
|
248
|
+
error_line("Cannot merge: files overlap with active branches on test:")
|
|
249
|
+
for c in active_conflicts:
|
|
250
|
+
warning_line(f" {c['branch']}:")
|
|
251
|
+
for f in c["files"]:
|
|
252
|
+
info_line(f" - {f}")
|
|
253
|
+
error_line("Resolve with the branch owner or remove that branch from test first.")
|
|
254
|
+
complete_action()
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
# Conflicting branches no longer exist — remove their stale commits and proceed
|
|
258
|
+
stale_conflicts = [c for c in conflicts if not c["branch_exists"]]
|
|
259
|
+
if stale_conflicts:
|
|
260
|
+
for c in stale_conflicts:
|
|
261
|
+
warning_line(f"Removing stale branch {c['branch']} from test (branch no longer exists)")
|
|
262
|
+
self.git.revert_from_test(c["sha"], c["branch"])
|
|
263
|
+
|
|
194
264
|
if get_input_true_false("Continue?", "n"):
|
|
195
265
|
self.git.squash_merge_into_test()
|
|
196
266
|
complete_action()
|
|
197
|
-
|
|
267
|
+
|
|
198
268
|
def _cherry_pick_test(self) -> None:
|
|
199
|
-
display_action_header("
|
|
269
|
+
display_action_header("Remove Branch from Test")
|
|
200
270
|
commits = self.git.get_test_commits()
|
|
201
271
|
if not commits:
|
|
202
|
-
info_line("No
|
|
272
|
+
info_line("No branch merges found on test branch")
|
|
203
273
|
complete_action()
|
|
204
274
|
return
|
|
205
|
-
|
|
206
|
-
info_line("
|
|
275
|
+
|
|
276
|
+
info_line("Branches currently on test:")
|
|
207
277
|
for i, c in enumerate(commits):
|
|
208
|
-
info_line(f" [{i+1}] {c['
|
|
209
|
-
|
|
210
|
-
choice = get_input_number("Select
|
|
278
|
+
info_line(f" [{i+1}] {c['branch']} ({c['sha'][:8]})")
|
|
279
|
+
|
|
280
|
+
choice = get_input_number("Select branch to remove (0 to cancel): ")
|
|
211
281
|
if 0 < choice <= len(commits):
|
|
212
|
-
|
|
282
|
+
selected = commits[choice-1]
|
|
283
|
+
self.git.revert_from_test(selected["sha"], selected["branch"])
|
|
213
284
|
complete_action()
|
|
@@ -29,6 +29,7 @@ class ProjectSettings(BaseModel):
|
|
|
29
29
|
project_dir: str = "./dbt"
|
|
30
30
|
profile_dir: str = "~/.dbt"
|
|
31
31
|
execution_mode: str = "dbt_cli"
|
|
32
|
+
ai_agents_enabled: bool = False
|
|
32
33
|
targets: list[DbtTarget] = Field(default_factory=lambda: [
|
|
33
34
|
DbtTarget(target_name="dev", branch_name="dev", target_role="DEVELOPERS", target_warehouse="DEV_WH", is_default=True),
|
|
34
35
|
DbtTarget(target_name="test", branch_name="test", target_role="DATAOPS_ADMIN", target_warehouse="DATAOPS_WH"),
|