@atlashub/smartstack-cli 4.37.0 → 4.39.0
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.
- package/dist/mcp-entry.mjs +3 -17
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/agents/efcore/migration.md +20 -59
- package/templates/agents/efcore/rebase-snapshot.md +107 -58
- package/templates/agents/efcore/squash.md +134 -71
- package/templates/agents/gitflow/cleanup.md +17 -6
- package/templates/project/claude-md/infrastructure.CLAUDE.md.template +1 -1
- package/templates/skills/application/references/migration-checklist-troubleshooting.md +3 -15
- package/templates/skills/application/steps/step-06-migration.md +0 -4
- package/templates/skills/application/templates-seed.md +33 -27
- package/templates/skills/dev-start/SKILL.md +192 -41
- package/templates/skills/efcore/SKILL.md +55 -12
- package/templates/skills/efcore/references/database-operations.md +2 -3
- package/templates/skills/efcore/references/seed-methods.md +1 -1
- package/templates/skills/efcore/references/shared-init-functions.md +8 -8
- package/templates/skills/efcore/references/sql-objects-injection.md +12 -54
- package/templates/skills/efcore/references/zero-downtime-patterns.md +2 -0
- package/templates/skills/gitflow/phases/cleanup.md +33 -3
- package/templates/skills/gitflow/references/commit-migration-validation.md +4 -0
package/package.json
CHANGED
|
@@ -47,6 +47,7 @@ SmartStack uses two separate DbContexts with separate migration histories:
|
|
|
47
47
|
| `extensions` | ExtensionsDbContext | `extensions` | `extensions.__EFMigrationsHistory` | Client-specific entities |
|
|
48
48
|
|
|
49
49
|
**Detection Logic:**
|
|
50
|
+
0. If `--context` flag provided → use specified DbContext (skip auto-detection)
|
|
50
51
|
1. If project contains `SmartStack.Domain` → use `CoreDbContext`
|
|
51
52
|
2. If project contains `{ClientName}.Domain` with SmartStack NuGet → use `ExtensionsDbContext`
|
|
52
53
|
3. If unclear → ask user which context to use
|
|
@@ -80,6 +81,8 @@ MCP returns the fully compliant name: `{context}_v{version}_{sequence}_{Descript
|
|
|
80
81
|
|
|
81
82
|
## Commands
|
|
82
83
|
|
|
84
|
+
> **IMPORTANT:** Project detection, migration search, and migration creation should run in a single Bash call to preserve shell variables (`$INFRA_PROJECT`, `$MIGRATIONS_DIR`, `$STARTUP_PROJECT`).
|
|
85
|
+
|
|
83
86
|
**Prerequisite — ensure `dotnet ef` is on PATH (MANDATORY before any command):**
|
|
84
87
|
|
|
85
88
|
```bash
|
|
@@ -96,66 +99,35 @@ fi
|
|
|
96
99
|
> NEVER rely on `$HOME` alone — in WSL it resolves to `/home/{user}` where .NET SDK is absent.
|
|
97
100
|
|
|
98
101
|
```bash
|
|
102
|
+
# Detect project paths
|
|
103
|
+
INFRA_PROJECT=$(find src -name "*Infrastructure.csproj" | head -1)
|
|
104
|
+
INFRA_DIR=$(dirname "$INFRA_PROJECT")
|
|
105
|
+
MIGRATIONS_DIR=$(find "$INFRA_DIR" -type d -name "Migrations" -path "*/Persistence/*" | head -1)
|
|
106
|
+
STARTUP_PROJECT=$(find src -name "*.Api.csproj" | head -1)
|
|
107
|
+
|
|
99
108
|
# Existing migrations for Core
|
|
100
|
-
find
|
|
109
|
+
find "$MIGRATIONS_DIR" -name "core_*.cs" | grep -v Designer | grep -v Snapshot
|
|
101
110
|
|
|
102
111
|
# Existing migrations for Extensions
|
|
103
|
-
find
|
|
112
|
+
find "$MIGRATIONS_DIR" -name "ext_*.cs" | grep -v Designer | grep -v Snapshot
|
|
104
113
|
|
|
105
|
-
# Create Core migration (use
|
|
106
|
-
dotnet ef migrations add $
|
|
114
|
+
# Create Core migration (use MCP_MIGRATION_NAME from suggest_migration)
|
|
115
|
+
dotnet ef migrations add "$MCP_MIGRATION_NAME" --context CoreDbContext -o Persistence/Migrations --project "$INFRA_PROJECT" --startup-project "$STARTUP_PROJECT"
|
|
107
116
|
|
|
108
|
-
# Create Extensions migration (use
|
|
109
|
-
dotnet ef migrations add $
|
|
117
|
+
# Create Extensions migration (use MCP_MIGRATION_NAME from suggest_migration)
|
|
118
|
+
dotnet ef migrations add "$MCP_MIGRATION_NAME" --context ExtensionsDbContext -o Persistence/Migrations --project "$INFRA_PROJECT" --startup-project "$STARTUP_PROJECT"
|
|
110
119
|
|
|
111
120
|
# Delete existing migration
|
|
112
|
-
rm
|
|
121
|
+
rm -f "$MIGRATIONS_DIR"/*${OLD_NAME}*.cs
|
|
113
122
|
```
|
|
114
123
|
|
|
115
|
-
> **Note:** `$
|
|
116
|
-
|
|
117
|
-
## SQL Objects Post-Injection (MANDATORY after every migration creation)
|
|
124
|
+
> **Note:** `$MCP_MIGRATION_NAME` is obtained via MCP call, never computed locally.
|
|
118
125
|
|
|
119
|
-
|
|
126
|
+
## SQL Objects (NO injection needed)
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
# Detect the generated migration file (the main .cs, not .Designer.cs)
|
|
123
|
-
MIGRATION_FILE=$(find "$MIGRATIONS_DIR" -name "*${MCP_NAME}.cs" ! -name "*.Designer.cs" | head -1)
|
|
124
|
-
SQL_OBJECTS_DIR="$INFRA_PROJECT_DIR/Persistence/SqlObjects"
|
|
125
|
-
SQL_FILES=$(find "$SQL_OBJECTS_DIR" -name "*.sql" 2>/dev/null | wc -l)
|
|
126
|
-
|
|
127
|
-
if [ "$SQL_FILES" -gt 0 ]; then
|
|
128
|
-
echo "Found $SQL_FILES SQL object(s) — injecting SqlObjectHelper.ApplyAll()..."
|
|
129
|
-
|
|
130
|
-
# Add using directive if not present
|
|
131
|
-
if ! grep -q "using SmartStack.Infrastructure.Persistence.SqlObjects;" "$MIGRATION_FILE"; then
|
|
132
|
-
sed -i '1s/^/using SmartStack.Infrastructure.Persistence.SqlObjects;\n/' "$MIGRATION_FILE"
|
|
133
|
-
fi
|
|
134
|
-
|
|
135
|
-
# Add SqlObjectHelper.ApplyAll at the end of Up() method
|
|
136
|
-
# Use Edit tool to insert before the closing brace of Up()
|
|
137
|
-
# Target: the last " }" before "protected override void Down"
|
|
138
|
-
sed -i '/protected override void Down/i\
|
|
139
|
-
\ // Apply SQL objects (TVF, Views, SP) from embedded resources\
|
|
140
|
-
\ SqlObjectHelper.ApplyAll(migrationBuilder);' "$MIGRATION_FILE"
|
|
141
|
-
|
|
142
|
-
# VERIFY injection succeeded — FAIL if not
|
|
143
|
-
if ! grep -q "SqlObjectHelper.ApplyAll" "$MIGRATION_FILE"; then
|
|
144
|
-
echo "ERROR: Auto-injection failed. Use Edit tool to manually add:"
|
|
145
|
-
echo " 1. Add 'using SmartStack.Infrastructure.Persistence.SqlObjects;' at top"
|
|
146
|
-
echo " 2. Add 'SqlObjectHelper.ApplyAll(migrationBuilder);' at end of Up() method"
|
|
147
|
-
echo " File: $MIGRATION_FILE"
|
|
148
|
-
# DO NOT CONTINUE — fix this before proceeding
|
|
149
|
-
else
|
|
150
|
-
echo " SqlObjectHelper.ApplyAll(migrationBuilder) injected successfully"
|
|
151
|
-
find "$SQL_OBJECTS_DIR" -name "*.sql" -exec basename {} \; | while read f; do echo " - $f"; done
|
|
152
|
-
fi
|
|
153
|
-
else
|
|
154
|
-
echo "No SQL objects found in SqlObjects/ — skipping injection"
|
|
155
|
-
fi
|
|
156
|
-
```
|
|
128
|
+
SQL objects (TVFs, views, stored procedures) are applied **automatically at application startup** via `SqlObjectHelper.ApplyAllAsync(dbContext)` in `SmartStackExtensions.InitializeSmartStackAsync()`, right after `MigrateAsync()`.
|
|
157
129
|
|
|
158
|
-
|
|
130
|
+
**You do NOT need to add `SqlObjectHelper.ApplyAll(migrationBuilder)` in migrations.** The startup process handles this with `CREATE OR ALTER` (idempotent). Modifying a `.sql` file only requires an application restart — no new migration needed.
|
|
159
131
|
|
|
160
132
|
## Context Detection
|
|
161
133
|
|
|
@@ -223,17 +195,6 @@ After rebase on develop:
|
|
|
223
195
|
3. Report the error with dotnet output
|
|
224
196
|
4. Suggest corrective action
|
|
225
197
|
|
|
226
|
-
## SQL in Migrations (STRICT RULE)
|
|
227
|
-
|
|
228
|
-
**FORBIDDEN:** Raw SQL inline in migrations (e.g., `migrationBuilder.Sql("CREATE FUNCTION ...")`)
|
|
229
|
-
|
|
230
|
-
**ONLY** SQL objects defined in `src/{Project}.Infrastructure/Persistence/SqlObjects/` are authorized.
|
|
231
|
-
|
|
232
|
-
- All SQL objects (functions, views, procedures, TVFs) MUST be `.sql` files in `Persistence/SqlObjects/`
|
|
233
|
-
- Migrations reference these via `SqlObjectHelper.ApplyAll(migrationBuilder)` in the `Up()` method
|
|
234
|
-
- If `Persistence/SqlObjects/*.sql` files exist, inject the call automatically after migration creation
|
|
235
|
-
- **Never** write raw SQL directly inside migration `Up()` or `Down()` methods
|
|
236
|
-
|
|
237
198
|
## Important Notes
|
|
238
199
|
|
|
239
200
|
- **Core migrations** go to `CoreDbContextModelSnapshot.cs`
|
|
@@ -21,12 +21,9 @@ if [ -f .git ] && grep -q '^gitdir: [A-Za-z]:' .git 2>/dev/null && grep -qi micr
|
|
|
21
21
|
fi
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
## SQL
|
|
24
|
+
## SQL Objects
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
**ONLY** SQL objects defined in `src/{Project}.Infrastructure/Persistence/SqlObjects/` are authorized.
|
|
29
|
-
Migrations reference these via `SqlObjectHelper.ApplyAll(migrationBuilder)` — never inline SQL.
|
|
26
|
+
SQL objects (TVFs, views, SPs) are applied **at startup** via `SqlObjectHelper.ApplyAllAsync()` — never inject in migrations.
|
|
30
27
|
|
|
31
28
|
## Workflow
|
|
32
29
|
|
|
@@ -36,75 +33,128 @@ Migrations reference these via `SqlObjectHelper.ApplyAll(migrationBuilder)` —
|
|
|
36
33
|
4. **Regenerate** consolidated migration
|
|
37
34
|
5. **Validate** build OK
|
|
38
35
|
|
|
36
|
+
## Shared Functions
|
|
37
|
+
|
|
38
|
+
> **Reference:** See `references/shared-init-functions.md` for `ensure_dotnet_ef()`, `detect_efcore_project()`, `detect_dbcontext()`, `determine_base_branch()`.
|
|
39
|
+
|
|
39
40
|
## Key Commands
|
|
40
41
|
|
|
42
|
+
> **IMPORTANT:** Steps 1-2 MUST run in a single Bash call to preserve shell variables. Step 2b (confirm) is a separate interactive call. Step 3 requires variables from steps 1-2.
|
|
43
|
+
|
|
44
|
+
### Step 1: Initialize
|
|
45
|
+
|
|
41
46
|
```bash
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
# Determine parent branch
|
|
48
|
+
CURRENT_BRANCH=$(git branch --show-current)
|
|
49
|
+
case "$CURRENT_BRANCH" in
|
|
50
|
+
feature/*) BASE_BRANCH="develop"; BRANCH_TYPE="feature" ;;
|
|
51
|
+
release/*) BASE_BRANCH="main"; BRANCH_TYPE="release" ;;
|
|
52
|
+
hotfix/*) BASE_BRANCH="main"; BRANCH_TYPE="hotfix" ;;
|
|
53
|
+
develop) BASE_BRANCH="main"; BRANCH_TYPE="develop" ;;
|
|
54
|
+
main|master) echo "BLOCKED: Cannot rebase on main/master"; exit 1 ;;
|
|
55
|
+
*) BASE_BRANCH="develop"; BRANCH_TYPE="unknown" ;;
|
|
56
|
+
esac
|
|
57
|
+
|
|
58
|
+
# Detect project
|
|
59
|
+
INFRA_PROJECT=$(find src -name "*Infrastructure.csproj" | head -1)
|
|
60
|
+
INFRA_DIR=$(dirname "$INFRA_PROJECT")
|
|
61
|
+
MIGRATIONS_DIR=$(find "$INFRA_DIR" -type d -name "Migrations" -path "*/Persistence/*" | head -1)
|
|
62
|
+
MIGRATIONS_REL=$(realpath --relative-to="$(git rev-parse --show-toplevel)" "$MIGRATIONS_DIR")
|
|
63
|
+
STARTUP_PROJECT=$(find src -name "*.Api.csproj" | head -1)
|
|
64
|
+
|
|
65
|
+
# Detect DbContext (override via --context flag, else auto-detect)
|
|
66
|
+
CONTEXT_OVERRIDE="${CONTEXT:-}"
|
|
67
|
+
if [ "$CONTEXT_OVERRIDE" = "CoreDbContext" ] || [ "$CONTEXT_OVERRIDE" = "core" ]; then
|
|
68
|
+
DBCONTEXT="CoreDbContext"; DBCONTEXT_TYPE="core"
|
|
69
|
+
elif [ "$CONTEXT_OVERRIDE" = "ExtensionsDbContext" ] || [ "$CONTEXT_OVERRIDE" = "extensions" ]; then
|
|
70
|
+
DBCONTEXT="ExtensionsDbContext"; DBCONTEXT_TYPE="extensions"
|
|
71
|
+
elif find src -type d -name "SmartStack.Domain" | grep -q .; then
|
|
72
|
+
DBCONTEXT="CoreDbContext"; DBCONTEXT_TYPE="core"
|
|
73
|
+
else
|
|
74
|
+
DBCONTEXT="ExtensionsDbContext"; DBCONTEXT_TYPE="extensions"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
echo "Branch: $CURRENT_BRANCH → Base: $BASE_BRANCH"
|
|
78
|
+
echo "Migrations: $MIGRATIONS_DIR"
|
|
79
|
+
echo "DbContext: $DBCONTEXT"
|
|
80
|
+
```
|
|
45
81
|
|
|
46
|
-
|
|
82
|
+
### Step 2: Backup
|
|
83
|
+
|
|
84
|
+
```bash
|
|
47
85
|
BACKUP_DIR=".claude/gitflow/backup/migrations/rebase_$(date +%Y%m%d_%H%M%S)"
|
|
48
86
|
mkdir -p "$BACKUP_DIR"
|
|
49
|
-
cp
|
|
87
|
+
cp "$MIGRATIONS_DIR"/*.cs "$BACKUP_DIR/"
|
|
88
|
+
echo "Backup: $BACKUP_DIR"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Step 2b: Confirm (skip if `--force`)
|
|
92
|
+
|
|
93
|
+
If `--force` flag is NOT set:
|
|
50
94
|
|
|
51
|
-
|
|
95
|
+
```javascript
|
|
96
|
+
AskUserQuestion({
|
|
97
|
+
question: "Rebase snapshot will:\n1. Reset ModelSnapshot to " + BASE_BRANCH + "\n2. Delete branch-specific migrations\n3. Regenerate consolidated migration\n\nBranch-specific migrations to recreate:\n" + BRANCH_ONLY_MIGS + "\n\nProceed?",
|
|
98
|
+
header: "EF Core Rebase-Snapshot",
|
|
99
|
+
options: [
|
|
100
|
+
{ label: "Yes, rebase", description: "Backup + rebase snapshot" },
|
|
101
|
+
{ label: "Cancel", description: "Do nothing" }
|
|
102
|
+
]
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Step 3: Identify branch-specific migrations + Reset
|
|
107
|
+
|
|
108
|
+
```bash
|
|
52
109
|
git fetch origin "$BASE_BRANCH"
|
|
53
|
-
|
|
110
|
+
|
|
111
|
+
# List parent branch migrations (names only)
|
|
112
|
+
BASE_MIGS=$(git ls-tree -r --name-only "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL/" 2>/dev/null | grep "\.cs$" | grep -v "Designer\|Snapshot" | xargs -I{} basename {} .cs)
|
|
113
|
+
|
|
114
|
+
# List local migrations (names only)
|
|
115
|
+
LOCAL_MIGS=$(find "$MIGRATIONS_DIR" -maxdepth 1 -name "*.cs" ! -name "*Designer*" ! -name "*Snapshot*" | xargs -I{} basename {} .cs)
|
|
116
|
+
|
|
117
|
+
# Calculate branch-specific migrations (in local but NOT in base)
|
|
118
|
+
BRANCH_ONLY_MIGS=""
|
|
119
|
+
for mig in $LOCAL_MIGS; do
|
|
120
|
+
if ! echo "$BASE_MIGS" | grep -qx "$mig"; then
|
|
121
|
+
BRANCH_ONLY_MIGS="$BRANCH_ONLY_MIGS $mig"
|
|
122
|
+
fi
|
|
123
|
+
done
|
|
124
|
+
BRANCH_ONLY_MIGS=$(echo "$BRANCH_ONLY_MIGS" | xargs)
|
|
125
|
+
|
|
126
|
+
echo "Branch-specific migrations to recreate:"
|
|
127
|
+
for mig in $BRANCH_ONLY_MIGS; do echo " - $mig"; done
|
|
128
|
+
|
|
129
|
+
# Reset snapshot to parent branch
|
|
130
|
+
git checkout "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL"/*ModelSnapshot.cs
|
|
54
131
|
|
|
55
132
|
# Retrieve parent branch migrations
|
|
56
|
-
BASE_MIGS=$(git ls-tree -r --name-only "origin/$BASE_BRANCH" -- Migrations/ | grep "\.cs$" | grep -v "Designer\|Snapshot")
|
|
57
133
|
for base_mig in $BASE_MIGS; do
|
|
58
|
-
|
|
59
|
-
git checkout "origin/$BASE_BRANCH" -- "
|
|
60
|
-
|
|
134
|
+
git checkout "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL/${base_mig}.cs" 2>/dev/null || true
|
|
135
|
+
git checkout "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL/${base_mig}.Designer.cs" 2>/dev/null || true
|
|
136
|
+
done
|
|
137
|
+
|
|
138
|
+
# Delete branch-specific migrations by exact name (NOT by branch type pattern)
|
|
139
|
+
for mig in $BRANCH_ONLY_MIGS; do
|
|
140
|
+
rm -f "$MIGRATIONS_DIR"/*"${mig}"*.cs
|
|
141
|
+
echo " Deleted: $mig"
|
|
61
142
|
done
|
|
143
|
+
```
|
|
62
144
|
|
|
63
|
-
|
|
64
|
-
rm -f Migrations/*${BRANCH_TYPE}_*.cs
|
|
65
|
-
rm -f Migrations/*${BRANCH_TYPE}_*.Designer.cs
|
|
145
|
+
### Step 4: Regenerate + Validate
|
|
66
146
|
|
|
147
|
+
```bash
|
|
67
148
|
# Regenerate with MCP naming - MANDATORY
|
|
68
149
|
mcp__smartstack__suggest_migration({ description: "...", context: DBCONTEXT_TYPE })
|
|
69
|
-
# Result: core_v1.7.0_001_UserAuthConsolidated
|
|
70
|
-
|
|
71
|
-
dotnet ef migrations add "$MIGRATION_NAME" --context "$DBCONTEXT"
|
|
72
150
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
MIGRATION_FILE=$(find Migrations -name "*${MIGRATION_NAME}.cs" ! -name "*.Designer.cs" | head -1)
|
|
79
|
-
SQL_OBJECTS_DIR="$INFRA_PROJECT_DIR/Persistence/SqlObjects"
|
|
80
|
-
SQL_FILES=$(find "$SQL_OBJECTS_DIR" -name "*.sql" 2>/dev/null | wc -l)
|
|
151
|
+
dotnet ef migrations add "$MCP_MIGRATION_NAME" \
|
|
152
|
+
--context "$DBCONTEXT" \
|
|
153
|
+
--project "$INFRA_PROJECT" \
|
|
154
|
+
--startup-project "$STARTUP_PROJECT" \
|
|
155
|
+
-o Persistence/Migrations
|
|
81
156
|
|
|
82
|
-
|
|
83
|
-
echo "Found $SQL_FILES SQL object(s) — injecting SqlObjectHelper.ApplyAll()..."
|
|
84
|
-
|
|
85
|
-
# Add using directive if not present
|
|
86
|
-
if ! grep -q "using SmartStack.Infrastructure.Persistence.SqlObjects;" "$MIGRATION_FILE"; then
|
|
87
|
-
sed -i '1s/^/using SmartStack.Infrastructure.Persistence.SqlObjects;\n/' "$MIGRATION_FILE"
|
|
88
|
-
fi
|
|
89
|
-
|
|
90
|
-
# Inject SqlObjectHelper.ApplyAll before Down() method
|
|
91
|
-
sed -i '/protected override void Down/i\
|
|
92
|
-
\ // Apply SQL objects (TVF, Views, SP) from embedded resources\
|
|
93
|
-
\ SqlObjectHelper.ApplyAll(migrationBuilder);' "$MIGRATION_FILE"
|
|
94
|
-
|
|
95
|
-
# VERIFY — FAIL if injection didn't work
|
|
96
|
-
if ! grep -q "SqlObjectHelper.ApplyAll" "$MIGRATION_FILE"; then
|
|
97
|
-
echo "ERROR: Auto-injection failed. Use Edit tool to manually add:"
|
|
98
|
-
echo " 1. 'using SmartStack.Infrastructure.Persistence.SqlObjects;' at top"
|
|
99
|
-
echo " 2. 'SqlObjectHelper.ApplyAll(migrationBuilder);' at end of Up()"
|
|
100
|
-
echo " File: $MIGRATION_FILE"
|
|
101
|
-
else
|
|
102
|
-
echo " SqlObjectHelper.ApplyAll(migrationBuilder) injected"
|
|
103
|
-
find "$SQL_OBJECTS_DIR" -name "*.sql" -exec basename {} \; | while read f; do echo " - $f"; done
|
|
104
|
-
fi
|
|
105
|
-
else
|
|
106
|
-
echo "No SQL objects found — skipping injection"
|
|
107
|
-
fi
|
|
157
|
+
# NOTE: SQL objects are applied automatically at startup — no injection needed.
|
|
108
158
|
|
|
109
159
|
# Validate
|
|
110
160
|
dotnet build
|
|
@@ -130,7 +180,6 @@ core_v1.7.0_001_ReleaseInitial
|
|
|
130
180
|
- [ ] Backup created
|
|
131
181
|
- [ ] Build OK after rebase
|
|
132
182
|
- [ ] SQL script can be generated
|
|
133
|
-
- [ ] SQL objects injected (if `Persistence/SqlObjects/*.sql` exists)
|
|
134
183
|
|
|
135
184
|
## Error Recovery
|
|
136
185
|
|
|
@@ -140,7 +189,7 @@ core_v1.7.0_001_ReleaseInitial
|
|
|
140
189
|
| MCP unavailable | Check `.claude/mcp-status.json`, run `/mcp healthcheck` |
|
|
141
190
|
| Snapshot fetch fails | Verify `origin/$BASE_BRANCH` exists: `git fetch origin $BASE_BRANCH` |
|
|
142
191
|
| Migration creation fails | Restore backup, check `dotnet build` output |
|
|
143
|
-
| SQL objects
|
|
192
|
+
| SQL objects missing at runtime | SQL objects are applied at startup — restart the application |
|
|
144
193
|
|
|
145
194
|
**Automatic recovery:** On ANY failure after backup:
|
|
146
195
|
1. Restore all files from backup directory
|
|
@@ -3,7 +3,7 @@ name: efcore-squash
|
|
|
3
3
|
description: EF Core migration squasher - combine multiple migrations into one
|
|
4
4
|
color: magenta
|
|
5
5
|
model: sonnet
|
|
6
|
-
tools: Bash, Glob, Read
|
|
6
|
+
tools: Bash, Glob, Read, Edit
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# EF Core Squash Agent
|
|
@@ -21,12 +21,9 @@ if [ -f .git ] && grep -q '^gitdir: [A-Za-z]:' .git 2>/dev/null && grep -qi micr
|
|
|
21
21
|
fi
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
## SQL
|
|
24
|
+
## SQL Objects
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
**ONLY** SQL objects defined in `src/{Project}.Infrastructure/Persistence/SqlObjects/` are authorized.
|
|
29
|
-
Migrations reference these via `SqlObjectHelper.ApplyAll(migrationBuilder)` — never inline SQL.
|
|
26
|
+
SQL objects (TVFs, views, SPs) are applied **at startup** via `SqlObjectHelper.ApplyAllAsync()` — never inject in migrations.
|
|
30
27
|
|
|
31
28
|
## Core Principle
|
|
32
29
|
|
|
@@ -51,93 +48,160 @@ WARNING: NEVER retrieve only the snapshot without the migrations!
|
|
|
51
48
|
5. **Fetch**: Retrieve **ModelSnapshot AND migrations** from parent branch (CRUCIAL!)
|
|
52
49
|
6. **Delete**: Remove branch-specific migrations ONLY
|
|
53
50
|
7. **Create**: Create consolidated migration (MCP mandatory)
|
|
54
|
-
8. **
|
|
55
|
-
|
|
51
|
+
8. **Validate**: Build + script OK
|
|
52
|
+
|
|
53
|
+
## Shared Functions
|
|
54
|
+
|
|
55
|
+
> **Reference:** See `references/shared-init-functions.md` for `ensure_dotnet_ef()`, `detect_efcore_project()`, `detect_dbcontext()`, `determine_base_branch()`, `block_production()`.
|
|
56
56
|
|
|
57
57
|
## Key Commands
|
|
58
58
|
|
|
59
|
+
> **IMPORTANT:** Steps 1-3 MUST run in a single Bash call to preserve shell variables ($BASE_BRANCH, $MIGRATIONS_DIR, $BRANCH_ONLY_MIGS, etc.). Step 4 (guard) and Step 5 (confirm) are separate calls. Step 6 requires variables from steps 1-3.
|
|
60
|
+
|
|
61
|
+
### Step 1: Initialize
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Determine parent branch
|
|
65
|
+
CURRENT_BRANCH=$(git branch --show-current)
|
|
66
|
+
case "$CURRENT_BRANCH" in
|
|
67
|
+
feature/*) BASE_BRANCH="develop"; BRANCH_TYPE="feature" ;;
|
|
68
|
+
release/*) BASE_BRANCH="main"; BRANCH_TYPE="release" ;;
|
|
69
|
+
hotfix/*) BASE_BRANCH="main"; BRANCH_TYPE="hotfix" ;;
|
|
70
|
+
develop) BASE_BRANCH="main"; BRANCH_TYPE="develop" ;;
|
|
71
|
+
main|master) echo "BLOCKED: Cannot squash on main/master"; exit 1 ;;
|
|
72
|
+
*) BASE_BRANCH="develop"; BRANCH_TYPE="unknown" ;;
|
|
73
|
+
esac
|
|
74
|
+
echo "Branch: $CURRENT_BRANCH ($BRANCH_TYPE) → Base: $BASE_BRANCH"
|
|
75
|
+
|
|
76
|
+
# Context override from --context flag (passed via agent prompt)
|
|
77
|
+
CONTEXT_OVERRIDE="${CONTEXT:-}"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Step 2: Detect project and migrations directory
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Find Infrastructure project and migrations directory
|
|
84
|
+
INFRA_PROJECT=$(find src -name "*Infrastructure.csproj" | head -1)
|
|
85
|
+
INFRA_DIR=$(dirname "$INFRA_PROJECT")
|
|
86
|
+
MIGRATIONS_DIR=$(find "$INFRA_DIR" -type d -name "Migrations" -path "*/Persistence/*" | head -1)
|
|
87
|
+
|
|
88
|
+
# Detect DbContext (override via --context flag, else auto-detect)
|
|
89
|
+
if [ "$CONTEXT_OVERRIDE" = "CoreDbContext" ] || [ "$CONTEXT_OVERRIDE" = "core" ]; then
|
|
90
|
+
DBCONTEXT="CoreDbContext"; DBCONTEXT_TYPE="core"
|
|
91
|
+
elif [ "$CONTEXT_OVERRIDE" = "ExtensionsDbContext" ] || [ "$CONTEXT_OVERRIDE" = "extensions" ]; then
|
|
92
|
+
DBCONTEXT="ExtensionsDbContext"; DBCONTEXT_TYPE="extensions"
|
|
93
|
+
elif find src -type d -name "SmartStack.Domain" | grep -q .; then
|
|
94
|
+
DBCONTEXT="CoreDbContext"; DBCONTEXT_TYPE="core"
|
|
95
|
+
else
|
|
96
|
+
DBCONTEXT="ExtensionsDbContext"; DBCONTEXT_TYPE="extensions"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
echo "Migrations: $MIGRATIONS_DIR"
|
|
100
|
+
echo "DbContext: $DBCONTEXT"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Step 3: Identify branch-specific migrations
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git fetch origin "$BASE_BRANCH"
|
|
107
|
+
|
|
108
|
+
# Get migration file paths RELATIVE TO GIT ROOT
|
|
109
|
+
MIGRATIONS_REL=$(realpath --relative-to="$(git rev-parse --show-toplevel)" "$MIGRATIONS_DIR")
|
|
110
|
+
|
|
111
|
+
# List parent branch migrations (names only, no Designer/Snapshot)
|
|
112
|
+
BASE_MIGS=$(git ls-tree -r --name-only "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL/" 2>/dev/null | grep "\.cs$" | grep -v "Designer\|Snapshot" | xargs -I{} basename {} .cs)
|
|
113
|
+
|
|
114
|
+
# List local migrations (names only)
|
|
115
|
+
LOCAL_MIGS=$(find "$MIGRATIONS_DIR" -maxdepth 1 -name "*.cs" ! -name "*Designer*" ! -name "*Snapshot*" | xargs -I{} basename {} .cs)
|
|
116
|
+
|
|
117
|
+
# Calculate branch-specific migrations (in local but NOT in base)
|
|
118
|
+
BRANCH_ONLY_MIGS=""
|
|
119
|
+
for mig in $LOCAL_MIGS; do
|
|
120
|
+
if ! echo "$BASE_MIGS" | grep -qx "$mig"; then
|
|
121
|
+
BRANCH_ONLY_MIGS="$BRANCH_ONLY_MIGS $mig"
|
|
122
|
+
fi
|
|
123
|
+
done
|
|
124
|
+
BRANCH_ONLY_MIGS=$(echo "$BRANCH_ONLY_MIGS" | xargs) # trim
|
|
125
|
+
|
|
126
|
+
BRANCH_COUNT=$(echo "$BRANCH_ONLY_MIGS" | wc -w)
|
|
127
|
+
BASE_COUNT=$(echo "$BASE_MIGS" | wc -w)
|
|
128
|
+
|
|
129
|
+
echo ""
|
|
130
|
+
echo "Parent ($BASE_BRANCH): $BASE_COUNT migration(s)"
|
|
131
|
+
echo "Branch-specific: $BRANCH_COUNT migration(s)"
|
|
132
|
+
for mig in $BRANCH_ONLY_MIGS; do echo " - $mig"; done
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Step 4: Guard — nothing to squash
|
|
136
|
+
|
|
59
137
|
```bash
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
138
|
+
if [ "$BRANCH_COUNT" -eq 0 ]; then
|
|
139
|
+
echo "Nothing to squash — no branch-specific migrations found."
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
if [ "$BRANCH_COUNT" -eq 1 ]; then
|
|
143
|
+
echo "Only 1 branch-specific migration — squash not needed."
|
|
144
|
+
exit 0
|
|
145
|
+
fi
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Step 5: Confirm (skip if `--force`)
|
|
149
|
+
|
|
150
|
+
If `--force` flag is NOT set, display the plan and ask confirmation:
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
AskUserQuestion({
|
|
154
|
+
question: "Squash will consolidate " + BRANCH_COUNT + " migrations into 1.\n\nMigrations to squash:\n" + BRANCH_ONLY_MIGS_LIST + "\n\nParent: " + BASE_BRANCH + " (" + BASE_COUNT + " migrations will be preserved)\n\nProceed?",
|
|
155
|
+
header: "EF Core Squash",
|
|
156
|
+
options: [
|
|
157
|
+
{ label: "Yes, squash", description: "Backup + consolidate" },
|
|
158
|
+
{ label: "Cancel", description: "Do nothing" }
|
|
159
|
+
]
|
|
160
|
+
})
|
|
161
|
+
```
|
|
63
162
|
|
|
64
|
-
|
|
65
|
-
BASE_MIGS=$(git ls-tree -r --name-only "origin/$BASE_BRANCH" -- Migrations/ | grep "\.cs$" | grep -v "Designer\|Snapshot")
|
|
66
|
-
LOCAL_MIGS=$(find Migrations -name "*.cs" | grep -v Designer | grep -v Snapshot)
|
|
163
|
+
### Step 6: Backup + Fetch + Delete + Create
|
|
67
164
|
|
|
165
|
+
```bash
|
|
68
166
|
# Backup
|
|
69
167
|
BACKUP_DIR=".claude/gitflow/backup/migrations/squash_$(date +%Y%m%d_%H%M%S)"
|
|
70
|
-
mkdir -p "$BACKUP_DIR" && cp
|
|
168
|
+
mkdir -p "$BACKUP_DIR" && cp "$MIGRATIONS_DIR"/*.cs "$BACKUP_DIR/"
|
|
169
|
+
echo "Backup: $BACKUP_DIR"
|
|
71
170
|
|
|
72
171
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
73
172
|
# CRUCIAL: Retrieve SNAPSHOT + MIGRATIONS from parent branch
|
|
74
173
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
-
git fetch origin "$BASE_BRANCH"
|
|
76
174
|
|
|
77
175
|
# 1. Retrieve ModelSnapshot
|
|
78
|
-
git checkout "origin/$BASE_BRANCH" --
|
|
176
|
+
git checkout "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL"/*ModelSnapshot.cs
|
|
79
177
|
echo "Snapshot retrieved from origin/$BASE_BRANCH"
|
|
80
178
|
|
|
81
179
|
# 2. Retrieve ALL migrations from parent branch
|
|
82
|
-
# MANDATORY: Without this, develop/main migrations will be lost!
|
|
83
180
|
for base_mig in $BASE_MIGS; do
|
|
84
|
-
|
|
85
|
-
git checkout "origin/$BASE_BRANCH" -- "
|
|
86
|
-
git checkout "origin/$BASE_BRANCH" -- "Migrations/${base_name}.Designer.cs" 2>/dev/null || true
|
|
87
|
-
echo " $base_name retrieved"
|
|
181
|
+
git checkout "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL/${base_mig}.cs" 2>/dev/null || true
|
|
182
|
+
git checkout "origin/$BASE_BRANCH" -- "$MIGRATIONS_REL/${base_mig}.Designer.cs" 2>/dev/null || true
|
|
88
183
|
done
|
|
89
|
-
echo "$(
|
|
184
|
+
echo "$BASE_COUNT migration(s) retrieved from origin/$BASE_BRANCH"
|
|
90
185
|
|
|
91
|
-
# Delete branch-specific migrations only
|
|
92
|
-
for mig in $
|
|
93
|
-
rm -f "
|
|
186
|
+
# 3. Delete branch-specific migrations only
|
|
187
|
+
for mig in $BRANCH_ONLY_MIGS; do
|
|
188
|
+
rm -f "$MIGRATIONS_DIR"/*"${mig}"*.cs
|
|
189
|
+
echo " Deleted: $mig"
|
|
94
190
|
done
|
|
95
191
|
|
|
96
|
-
# Create consolidated migration - MCP MANDATORY
|
|
97
|
-
# $DBCONTEXT_TYPE = "core" or "extensions" (from detect_efcore_project)
|
|
192
|
+
# 4. Create consolidated migration - MCP MANDATORY
|
|
98
193
|
mcp__smartstack__suggest_migration({ description: "...", context: DBCONTEXT_TYPE })
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# Without this, TVFs (fn_GetUserGroupHierarchy etc.) won't exist in DB
|
|
106
|
-
# and ALL endpoints will return 500 at runtime
|
|
107
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
108
|
-
MIGRATION_FILE=$(find Migrations -name "*${MIGRATION_NAME_FROM_MCP}.cs" ! -name "*.Designer.cs" | head -1)
|
|
109
|
-
SQL_OBJECTS_DIR="$INFRA_PROJECT_DIR/Persistence/SqlObjects"
|
|
110
|
-
SQL_FILES=$(find "$SQL_OBJECTS_DIR" -name "*.sql" 2>/dev/null | wc -l)
|
|
194
|
+
# Use the name returned by MCP in $MCP_MIGRATION_NAME:
|
|
195
|
+
dotnet ef migrations add "$MCP_MIGRATION_NAME" \
|
|
196
|
+
--context "$DBCONTEXT" \
|
|
197
|
+
--project "$INFRA_PROJECT" \
|
|
198
|
+
--startup-project "$(find src -name "*.Api.csproj" | head -1)" \
|
|
199
|
+
-o Persistence/Migrations
|
|
111
200
|
|
|
112
|
-
|
|
113
|
-
echo "Found $SQL_FILES SQL object(s) — injecting SqlObjectHelper.ApplyAll()..."
|
|
114
|
-
|
|
115
|
-
# Add using directive if not present
|
|
116
|
-
if ! grep -q "using SmartStack.Infrastructure.Persistence.SqlObjects;" "$MIGRATION_FILE"; then
|
|
117
|
-
sed -i '1s/^/using SmartStack.Infrastructure.Persistence.SqlObjects;\n/' "$MIGRATION_FILE"
|
|
118
|
-
fi
|
|
119
|
-
|
|
120
|
-
# Inject SqlObjectHelper.ApplyAll before Down() method
|
|
121
|
-
sed -i '/protected override void Down/i\
|
|
122
|
-
\ // Apply SQL objects (TVF, Views, SP) from embedded resources\
|
|
123
|
-
\ SqlObjectHelper.ApplyAll(migrationBuilder);' "$MIGRATION_FILE"
|
|
124
|
-
|
|
125
|
-
# VERIFY — FAIL if injection didn't work
|
|
126
|
-
if ! grep -q "SqlObjectHelper.ApplyAll" "$MIGRATION_FILE"; then
|
|
127
|
-
echo "ERROR: Auto-injection failed. Use Edit tool to manually add:"
|
|
128
|
-
echo " 1. 'using SmartStack.Infrastructure.Persistence.SqlObjects;' at top"
|
|
129
|
-
echo " 2. 'SqlObjectHelper.ApplyAll(migrationBuilder);' at end of Up()"
|
|
130
|
-
echo " File: $MIGRATION_FILE"
|
|
131
|
-
else
|
|
132
|
-
echo " SqlObjectHelper.ApplyAll(migrationBuilder) injected"
|
|
133
|
-
find "$SQL_OBJECTS_DIR" -name "*.sql" -exec basename {} \; | while read f; do echo " - $f"; done
|
|
134
|
-
fi
|
|
135
|
-
else
|
|
136
|
-
echo "No SQL objects found — skipping injection"
|
|
137
|
-
fi
|
|
201
|
+
# NOTE: SQL objects are applied automatically at startup — no injection needed.
|
|
138
202
|
|
|
139
|
-
# Validate
|
|
140
|
-
dotnet build && dotnet ef migrations script --idempotent > /dev/null
|
|
203
|
+
# 5. Validate
|
|
204
|
+
dotnet build && dotnet ef migrations script --idempotent --context "$DBCONTEXT" > /dev/null
|
|
141
205
|
```
|
|
142
206
|
|
|
143
207
|
## Safety Checks
|
|
@@ -149,7 +213,6 @@ dotnet build && dotnet ef migrations script --idempotent > /dev/null
|
|
|
149
213
|
- [ ] **Parent snapshot retrieved** (origin/$BASE_BRANCH)
|
|
150
214
|
- [ ] **Parent migrations retrieved** (.cs AND .Designer.cs files)
|
|
151
215
|
- [ ] Branch-specific migrations deleted (not inherited ones!)
|
|
152
|
-
- [ ] **SQL Objects injected** (if `Persistence/SqlObjects/*.sql` exists)
|
|
153
216
|
- [ ] Build OK after squash
|
|
154
217
|
- [ ] Script can be generated
|
|
155
218
|
|
|
@@ -165,7 +228,7 @@ SQUASH - {current_branch}
|
|
|
165
228
|
|
|
166
229
|
Consolidated:
|
|
167
230
|
Before: {M} migrations (branch-specific)
|
|
168
|
-
After: 1 migration ({
|
|
231
|
+
After: 1 migration ({MCP_MIGRATION_NAME} via MCP)
|
|
169
232
|
|
|
170
233
|
Backup: .claude/gitflow/backup/migrations/squash_{timestamp}/
|
|
171
234
|
|
|
@@ -189,7 +252,7 @@ Next: /efcore db-reset, /efcore db-deploy
|
|
|
189
252
|
| MCP unavailable | Check `.claude/mcp-status.json`, run `/mcp healthcheck` |
|
|
190
253
|
| Snapshot fetch fails | Verify `origin/$BASE_BRANCH` exists: `git fetch origin $BASE_BRANCH` |
|
|
191
254
|
| Script generation fails | Restore backup, then investigate migration content |
|
|
192
|
-
| SQL objects
|
|
255
|
+
| SQL objects missing at runtime | SQL objects are applied at startup — restart the application |
|
|
193
256
|
|
|
194
257
|
**Automatic recovery:** On ANY failure after backup:
|
|
195
258
|
1. Restore all files from backup directory
|
|
@@ -43,9 +43,9 @@ fi
|
|
|
43
43
|
| Categorie | Condition | Action |
|
|
44
44
|
|-----------|-----------|--------|
|
|
45
45
|
| Permanent | main, develop | Protege - jamais supprime |
|
|
46
|
-
| Actif | Branche existe + recent | Conserver |
|
|
47
|
-
| Orphelin |
|
|
48
|
-
| Stale |
|
|
46
|
+
| Actif | Branche existe + non mergee + recent | Conserver |
|
|
47
|
+
| Orphelin | Repertoire manquant ou branche supprimee | Supprimer (worktree + local + remote + parent dir) |
|
|
48
|
+
| Stale | Remote supprime OU branche mergee dans develop/main | Supprimer (worktree + local + remote + parent dir) |
|
|
49
49
|
| Dirty | Modifications non commitees | Warning - confirmation requise |
|
|
50
50
|
|
|
51
51
|
## Commandes
|
|
@@ -64,9 +64,20 @@ git branch --list $BRANCH
|
|
|
64
64
|
# Derniere activite
|
|
65
65
|
git log -1 --format=%ci $BRANCH
|
|
66
66
|
|
|
67
|
-
# Supprimer worktree
|
|
68
|
-
git worktree remove "$WORKTREE_PATH" --force
|
|
69
|
-
git
|
|
67
|
+
# Supprimer worktree (3 etapes obligatoires)
|
|
68
|
+
git worktree remove "$WORKTREE_PATH" --force # 1. Remove worktree
|
|
69
|
+
git push origin --delete "$BRANCH" 2>/dev/null # 2. Delete remote branch
|
|
70
|
+
git branch -D "$BRANCH" 2>/dev/null # 3. Delete local branch
|
|
71
|
+
git worktree prune # 4. Prune stale refs
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Post-cleanup: empty parent directories
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Remove empty parent directories (features/, releases/, hotfixes/)
|
|
78
|
+
for dir in "$FEATURES_DIR" "$RELEASES_DIR" "$HOTFIXES_DIR"; do
|
|
79
|
+
[ -d "$dir" ] && [ -z "$(ls -A "$dir" 2>/dev/null)" ] && rm -rf "$dir"
|
|
80
|
+
done
|
|
70
81
|
```
|
|
71
82
|
|
|
72
83
|
## Output Format
|