1
0
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:
mkincaid-bw
2026-01-15 13:43:23 -08:00
committed by GitHub
parent 029a5f6a2d
commit 51d90cce3d

View File

@@ -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
}