mirror of
https://github.com/bitwarden/server
synced 2026-01-28 23:36:12 +00:00
Add Entity Framework migration validation to verify_migrations script (#6817)
* Add Entity Framework migration validation to verify_migrations script Enhances dev/verify_migrations.ps1 to validate EF migration files in addition to SQL migrations. The script now validates migrations in util/MySqlMigrations, util/PostgresMigrations, and util/SqliteMigrations directories. Validation includes: - Correct naming format (YYYYMMDDHHMMSS_Description.cs) - Both .cs and .Designer.cs files exist as pairs - Chronological ordering of timestamps - Excludes DatabaseContextModelSnapshot.cs files The script provides comprehensive reporting for all migration types with a summary showing which validations passed or failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix: Validate all EF migration files instead of silently ignoring malformed names Previously, migration files that didn't match the expected pattern were silently ignored during validation. This could allow incorrectly named files to slip through. Now the script explicitly tracks and reports any migration files that don't match the required YYYYMMDDHHMMSS_Description.cs format, ensuring all new migration files are properly validated. Addresses feedback from PR review to prevent malformed migration files from being overlooked. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,19 @@
|
||||
Validates that new database migration files follow naming conventions and chronological order.
|
||||
|
||||
.DESCRIPTION
|
||||
This script validates migration files in util/Migrator/DbScripts/ to ensure:
|
||||
This script validates migration files to ensure:
|
||||
|
||||
For SQL migrations in util/Migrator/DbScripts/:
|
||||
1. New migrations follow the naming format: YYYY-MM-DD_NN_Description.sql
|
||||
2. New migrations are chronologically ordered (filename sorts after existing migrations)
|
||||
3. Dates use leading zeros (e.g., 2025-01-05, not 2025-1-5)
|
||||
4. A 2-digit sequence number is included (e.g., _00, _01)
|
||||
|
||||
For Entity Framework migrations in util/MySqlMigrations, util/PostgresMigrations, util/SqliteMigrations:
|
||||
1. New migrations follow the naming format: YYYYMMDDHHMMSS_Description.cs
|
||||
2. Each migration has both .cs and .Designer.cs files
|
||||
3. New migrations are chronologically ordered (timestamp sorts after existing migrations)
|
||||
|
||||
.PARAMETER BaseRef
|
||||
The base git reference to compare against (e.g., 'main', 'HEAD~1')
|
||||
|
||||
@@ -58,75 +65,288 @@ $currentMigrations = git ls-tree -r --name-only $CurrentRef -- "$migrationPath/"
|
||||
# Find added migrations
|
||||
$addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations }
|
||||
|
||||
$sqlValidationFailed = $false
|
||||
|
||||
if ($addedMigrations.Count -eq 0) {
|
||||
Write-Host "No new migration files added."
|
||||
exit 0
|
||||
Write-Host "No new SQL migration files added."
|
||||
Write-Host ""
|
||||
}
|
||||
else {
|
||||
Write-Host "New SQL migration files detected:"
|
||||
$addedMigrations | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ""
|
||||
|
||||
# Get the last migration from base reference
|
||||
if ($baseMigrations.Count -eq 0) {
|
||||
Write-Host "No previous SQL migrations found (initial commit?). Skipping chronological validation."
|
||||
Write-Host ""
|
||||
}
|
||||
else {
|
||||
$lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)
|
||||
Write-Host "Last SQL migration in base reference: $lastBaseMigration"
|
||||
Write-Host ""
|
||||
|
||||
# Required format regex: YYYY-MM-DD_NN_Description.sql
|
||||
$formatRegex = '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}_.+\.sql$'
|
||||
|
||||
foreach ($migration in $addedMigrations) {
|
||||
$migrationName = Split-Path -Leaf $migration
|
||||
|
||||
# Validate NEW migration filename format
|
||||
if ($migrationName -notmatch $formatRegex) {
|
||||
Write-Host "ERROR: Migration '$migrationName' does not match required format"
|
||||
Write-Host "Required format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " - YYYY: 4-digit year"
|
||||
Write-Host " - MM: 2-digit month with leading zero (01-12)"
|
||||
Write-Host " - DD: 2-digit day with leading zero (01-31)"
|
||||
Write-Host " - NN: 2-digit sequence number (00, 01, 02, etc.)"
|
||||
Write-Host "Example: 2025-01-15_00_MyMigration.sql"
|
||||
$sqlValidationFailed = $true
|
||||
continue
|
||||
}
|
||||
|
||||
# Compare migration name with last base migration (using ordinal string comparison)
|
||||
if ([string]::CompareOrdinal($migrationName, $lastBaseMigration) -lt 0) {
|
||||
Write-Host "ERROR: New migration '$migrationName' is not chronologically after '$lastBaseMigration'"
|
||||
$sqlValidationFailed = $true
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$migrationName' is chronologically after '$lastBaseMigration'"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($sqlValidationFailed) {
|
||||
Write-Host "FAILED: One or more SQL migrations are incorrectly named or not in chronological order"
|
||||
Write-Host ""
|
||||
Write-Host "All new SQL migration files must:"
|
||||
Write-Host " 1. Follow the naming format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 2. Use leading zeros in dates (e.g., 2025-01-05, not 2025-1-5)"
|
||||
Write-Host " 3. Include a 2-digit sequence number (e.g., _00, _01)"
|
||||
Write-Host " 4. Have a filename that sorts after the last migration in base"
|
||||
Write-Host ""
|
||||
Write-Host "To fix this issue:"
|
||||
Write-Host " 1. Locate your migration file(s) in util/Migrator/DbScripts/"
|
||||
Write-Host " 2. Rename to follow format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 3. Ensure the date is after $lastBaseMigration"
|
||||
Write-Host ""
|
||||
Write-Host "Example: 2025-01-15_00_AddNewFeature.sql"
|
||||
}
|
||||
else {
|
||||
Write-Host "SUCCESS: All new SQL migrations are correctly named and in chronological order"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Write-Host "New migration files detected:"
|
||||
$addedMigrations | ForEach-Object { Write-Host " $_" }
|
||||
# ===========================================================================================
|
||||
# Validate Entity Framework Migrations
|
||||
# ===========================================================================================
|
||||
|
||||
Write-Host "==================================================================="
|
||||
Write-Host "Validating Entity Framework Migrations"
|
||||
Write-Host "==================================================================="
|
||||
Write-Host ""
|
||||
|
||||
# Get the last migration from base reference
|
||||
if ($baseMigrations.Count -eq 0) {
|
||||
Write-Host "No previous migrations found (initial commit?). Skipping validation."
|
||||
exit 0
|
||||
}
|
||||
$efMigrationPaths = @(
|
||||
@{ Path = "util/MySqlMigrations/Migrations"; Name = "MySQL" },
|
||||
@{ Path = "util/PostgresMigrations/Migrations"; Name = "Postgres" },
|
||||
@{ Path = "util/SqliteMigrations/Migrations"; Name = "SQLite" }
|
||||
)
|
||||
|
||||
$lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)
|
||||
Write-Host "Last migration in base reference: $lastBaseMigration"
|
||||
Write-Host ""
|
||||
$efValidationFailed = $false
|
||||
|
||||
# Required format regex: YYYY-MM-DD_NN_Description.sql
|
||||
$formatRegex = '^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}_.+\.sql$'
|
||||
foreach ($migrationPathInfo in $efMigrationPaths) {
|
||||
$efPath = $migrationPathInfo.Path
|
||||
$dbName = $migrationPathInfo.Name
|
||||
|
||||
$validationFailed = $false
|
||||
Write-Host "-------------------------------------------------------------------"
|
||||
Write-Host "Checking $dbName EF migrations in $efPath"
|
||||
Write-Host "-------------------------------------------------------------------"
|
||||
Write-Host ""
|
||||
|
||||
foreach ($migration in $addedMigrations) {
|
||||
$migrationName = Split-Path -Leaf $migration
|
||||
# Get list of migrations from base reference
|
||||
try {
|
||||
$baseMigrations = git ls-tree -r --name-only $BaseRef -- "$efPath/" 2>$null | Where-Object { $_ -like "*.cs" -and $_ -notlike "*DatabaseContextModelSnapshot.cs" } | Sort-Object
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Warning: Could not retrieve $dbName migrations from base reference '$BaseRef'"
|
||||
$baseMigrations = @()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Host "Warning: Could not retrieve $dbName migrations from base reference '$BaseRef'"
|
||||
$baseMigrations = @()
|
||||
}
|
||||
|
||||
# Validate NEW migration filename format
|
||||
if ($migrationName -notmatch $formatRegex) {
|
||||
Write-Host "ERROR: Migration '$migrationName' does not match required format"
|
||||
Write-Host "Required format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " - YYYY: 4-digit year"
|
||||
Write-Host " - MM: 2-digit month with leading zero (01-12)"
|
||||
Write-Host " - DD: 2-digit day with leading zero (01-31)"
|
||||
Write-Host " - NN: 2-digit sequence number (00, 01, 02, etc.)"
|
||||
Write-Host "Example: 2025-01-15_00_MyMigration.sql"
|
||||
$validationFailed = $true
|
||||
# Get list of migrations from current reference
|
||||
$currentMigrations = git ls-tree -r --name-only $CurrentRef -- "$efPath/" | Where-Object { $_ -like "*.cs" -and $_ -notlike "*DatabaseContextModelSnapshot.cs" } | Sort-Object
|
||||
|
||||
# Find added migrations
|
||||
$addedMigrations = $currentMigrations | Where-Object { $_ -notin $baseMigrations }
|
||||
|
||||
if ($addedMigrations.Count -eq 0) {
|
||||
Write-Host "No new $dbName EF migration files added."
|
||||
Write-Host ""
|
||||
continue
|
||||
}
|
||||
|
||||
# Compare migration name with last base migration (using ordinal string comparison)
|
||||
if ([string]::CompareOrdinal($migrationName, $lastBaseMigration) -lt 0) {
|
||||
Write-Host "ERROR: New migration '$migrationName' is not chronologically after '$lastBaseMigration'"
|
||||
$validationFailed = $true
|
||||
Write-Host "New $dbName EF migration files detected:"
|
||||
$addedMigrations | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ""
|
||||
|
||||
# Get the last migration from base reference
|
||||
if ($baseMigrations.Count -eq 0) {
|
||||
Write-Host "No previous $dbName migrations found. Skipping chronological validation."
|
||||
Write-Host ""
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$migrationName' is chronologically after '$lastBaseMigration'"
|
||||
$lastBaseMigration = Split-Path -Leaf ($baseMigrations | Select-Object -Last 1)
|
||||
Write-Host "Last $dbName migration in base reference: $lastBaseMigration"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Required format regex: YYYYMMDDHHMMSS_Description.cs or YYYYMMDDHHMMSS_Description.Designer.cs
|
||||
$efFormatRegex = '^[0-9]{14}_.+\.cs$'
|
||||
|
||||
# Group migrations by base name (without .Designer.cs suffix)
|
||||
$migrationGroups = @{}
|
||||
$unmatchedFiles = @()
|
||||
|
||||
foreach ($migration in $addedMigrations) {
|
||||
$migrationName = Split-Path -Leaf $migration
|
||||
|
||||
# Extract base name (remove .Designer.cs or .cs)
|
||||
if ($migrationName -match '^([0-9]{14}_.+?)(?:\.Designer)?\.cs$') {
|
||||
$baseName = $matches[1]
|
||||
if (-not $migrationGroups.ContainsKey($baseName)) {
|
||||
$migrationGroups[$baseName] = @()
|
||||
}
|
||||
$migrationGroups[$baseName] += $migrationName
|
||||
}
|
||||
else {
|
||||
# Track files that don't match the expected pattern
|
||||
$unmatchedFiles += $migrationName
|
||||
}
|
||||
}
|
||||
|
||||
# Flag any files that don't match the expected pattern
|
||||
if ($unmatchedFiles.Count -gt 0) {
|
||||
Write-Host "ERROR: The following migration files do not match the required format:"
|
||||
foreach ($unmatchedFile in $unmatchedFiles) {
|
||||
Write-Host " - $unmatchedFile"
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "Required format: YYYYMMDDHHMMSS_Description.cs or YYYYMMDDHHMMSS_Description.Designer.cs"
|
||||
Write-Host " - YYYYMMDDHHMMSS: 14-digit timestamp (Year, Month, Day, Hour, Minute, Second)"
|
||||
Write-Host " - Description: Descriptive name using PascalCase"
|
||||
Write-Host "Example: 20250115120000_AddNewFeature.cs and 20250115120000_AddNewFeature.Designer.cs"
|
||||
Write-Host ""
|
||||
$efValidationFailed = $true
|
||||
}
|
||||
|
||||
foreach ($baseName in $migrationGroups.Keys | Sort-Object) {
|
||||
$files = $migrationGroups[$baseName]
|
||||
|
||||
# Validate format
|
||||
$mainFile = "$baseName.cs"
|
||||
$designerFile = "$baseName.Designer.cs"
|
||||
|
||||
if ($mainFile -notmatch $efFormatRegex) {
|
||||
Write-Host "ERROR: Migration '$mainFile' does not match required format"
|
||||
Write-Host "Required format: YYYYMMDDHHMMSS_Description.cs"
|
||||
Write-Host " - YYYYMMDDHHMMSS: 14-digit timestamp (Year, Month, Day, Hour, Minute, Second)"
|
||||
Write-Host "Example: 20250115120000_AddNewFeature.cs"
|
||||
$efValidationFailed = $true
|
||||
continue
|
||||
}
|
||||
|
||||
# Check that both .cs and .Designer.cs files exist
|
||||
$hasCsFile = $files -contains $mainFile
|
||||
$hasDesignerFile = $files -contains $designerFile
|
||||
|
||||
if (-not $hasCsFile) {
|
||||
Write-Host "ERROR: Missing main migration file: $mainFile"
|
||||
$efValidationFailed = $true
|
||||
}
|
||||
|
||||
if (-not $hasDesignerFile) {
|
||||
Write-Host "ERROR: Missing designer file: $designerFile"
|
||||
Write-Host "Each EF migration must have both a .cs and .Designer.cs file"
|
||||
$efValidationFailed = $true
|
||||
}
|
||||
|
||||
if (-not $hasCsFile -or -not $hasDesignerFile) {
|
||||
continue
|
||||
}
|
||||
|
||||
# Compare migration timestamp with last base migration (using ordinal string comparison)
|
||||
if ($baseMigrations.Count -gt 0) {
|
||||
if ([string]::CompareOrdinal($mainFile, $lastBaseMigration) -lt 0) {
|
||||
Write-Host "ERROR: New migration '$mainFile' is not chronologically after '$lastBaseMigration'"
|
||||
$efValidationFailed = $true
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$mainFile' is chronologically after '$lastBaseMigration'"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "OK: '$mainFile' (no previous migrations to compare)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($efValidationFailed) {
|
||||
Write-Host "FAILED: One or more EF migrations are incorrectly named or not in chronological order"
|
||||
Write-Host ""
|
||||
Write-Host "All new EF migration files must:"
|
||||
Write-Host " 1. Follow the naming format: YYYYMMDDHHMMSS_Description.cs"
|
||||
Write-Host " 2. Include both .cs and .Designer.cs files"
|
||||
Write-Host " 3. Have a timestamp that sorts after the last migration in base"
|
||||
Write-Host ""
|
||||
Write-Host "To fix this issue:"
|
||||
Write-Host " 1. Locate your migration file(s) in the respective Migrations directory"
|
||||
Write-Host " 2. Ensure both .cs and .Designer.cs files exist"
|
||||
Write-Host " 3. Rename to follow format: YYYYMMDDHHMMSS_Description.cs"
|
||||
Write-Host " 4. Ensure the timestamp is after the last migration"
|
||||
Write-Host ""
|
||||
Write-Host "Example: 20250115120000_AddNewFeature.cs and 20250115120000_AddNewFeature.Designer.cs"
|
||||
}
|
||||
else {
|
||||
Write-Host "SUCCESS: All new EF migrations are correctly named and in chronological order"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==================================================================="
|
||||
Write-Host "Validation Summary"
|
||||
Write-Host "==================================================================="
|
||||
|
||||
if ($sqlValidationFailed -or $efValidationFailed) {
|
||||
if ($sqlValidationFailed) {
|
||||
Write-Host "❌ SQL migrations validation FAILED"
|
||||
}
|
||||
else {
|
||||
Write-Host "✓ SQL migrations validation PASSED"
|
||||
}
|
||||
|
||||
if ($efValidationFailed) {
|
||||
Write-Host "❌ EF migrations validation FAILED"
|
||||
}
|
||||
else {
|
||||
Write-Host "✓ EF migrations validation PASSED"
|
||||
}
|
||||
|
||||
if ($validationFailed) {
|
||||
Write-Host "FAILED: One or more migrations are incorrectly named or not in chronological order"
|
||||
Write-Host ""
|
||||
Write-Host "All new migration files must:"
|
||||
Write-Host " 1. Follow the naming format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 2. Use leading zeros in dates (e.g., 2025-01-05, not 2025-1-5)"
|
||||
Write-Host " 3. Include a 2-digit sequence number (e.g., _00, _01)"
|
||||
Write-Host " 4. Have a filename that sorts after the last migration in base"
|
||||
Write-Host ""
|
||||
Write-Host "To fix this issue:"
|
||||
Write-Host " 1. Locate your migration file(s) in util/Migrator/DbScripts/"
|
||||
Write-Host " 2. Rename to follow format: YYYY-MM-DD_NN_Description.sql"
|
||||
Write-Host " 3. Ensure the date is after $lastBaseMigration"
|
||||
Write-Host ""
|
||||
Write-Host "Example: 2025-01-15_00_AddNewFeature.sql"
|
||||
Write-Host "OVERALL RESULT: FAILED"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "SUCCESS: All new migrations are correctly named and in chronological order"
|
||||
exit 0
|
||||
else {
|
||||
Write-Host "✓ SQL migrations validation PASSED"
|
||||
Write-Host "✓ EF migrations validation PASSED"
|
||||
Write-Host ""
|
||||
Write-Host "OVERALL RESULT: SUCCESS"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user