mirror of
https://github.com/MAKS-IT-COM/maksit-persistence.git
synced 2026-11-15 06:06:11 +01:00
(refactor): remove schema sync
This commit is contained in:
parent
626cccd34f
commit
4a79a98b98
@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.0.0] - 2026-08-21
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- **Schema sync (breaking):** `RegisterSchemaSync`, `ISchemaSyncService`, `ISchemaSyncOptions`, `SchemaColumn`, desired-schema maps, `DatabaseStartupPhases.SchemaSync`, and add-only `ALTER TABLE` sync. Schema evolution is FluentMigrator only.
|
||||||
|
|
||||||
## [1.0.0] - 2026-08-17
|
## [1.0.0] - 2026-08-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -90,6 +90,8 @@ This project follows [Semantic Versioning](https://semver.org/):
|
|||||||
- **MINOR**: backward-compatible features
|
- **MINOR**: backward-compatible features
|
||||||
- **PATCH**: backward-compatible fixes
|
- **PATCH**: backward-compatible fixes
|
||||||
|
|
||||||
|
Optional SemVer prerelease (`0.1.0-alpha.1`, `0.1.0-beta.1`, `0.1.0-rc.1`) is valid; git tag is `v{version}`. GitHub marks hyphenated versions as prerelease.
|
||||||
|
|
||||||
## Utility Scripts
|
## Utility Scripts
|
||||||
|
|
||||||
Automation lives under `utils/` (Community profile from [maksit-repoutils](https://github.com/MAKS-IT-COM/maksit-repoutils), local-copy sync). Product-specific settings are in each engine’s `scriptSettings.json`.
|
Automation lives under `utils/` (Community profile from [maksit-repoutils](https://github.com/MAKS-IT-COM/maksit-repoutils), local-copy sync). Product-specific settings are in each engine’s `scriptSettings.json`.
|
||||||
|
|||||||
11
README.md
11
README.md
@ -4,7 +4,7 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
NuGet facade over [linq2db](https://linq2db.github.io/) + PostgreSQL for ASP.NET Core Engines: connection factory, FluentMigrator startup, add-only schema sync, and `MaksIT.Results` guards.
|
NuGet facade over [linq2db](https://linq2db.github.io/) + PostgreSQL for ASP.NET Core Engines: connection factory, FluentMigrator startup, and `MaksIT.Results` guards.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
@ -22,13 +22,12 @@ NuGet facade over [linq2db](https://linq2db.github.io/) + PostgreSQL for ASP.NET
|
|||||||
|
|
||||||
`MaksIT.Persistence` wraps linq2db PostgreSQL wiring so Engine code depends on `ILinq2DbConnectionFactory` instead of copying `DataOptions` / `ProviderName.PostgreSQL` / startup wait in every product. Domain tables, Dto, FluentMapping **content**, and FluentMigrator migration classes stay in the product Engine.
|
`MaksIT.Persistence` wraps linq2db PostgreSQL wiring so Engine code depends on `ILinq2DbConnectionFactory` instead of copying `DataOptions` / `ProviderName.PostgreSQL` / startup wait in every product. Domain tables, Dto, FluentMapping **content**, and FluentMigrator migration classes stay in the product Engine.
|
||||||
|
|
||||||
Linq2Db queries stay in the product: persist/query services still call `GetTable<T>()`. This library hides connection lifetime, Postgres transients, migration bootstrap, and add-only schema sync.
|
Linq2Db queries stay in the product: persist/query services still call `GetTable<T>()`. This library hides connection lifetime, Postgres transients, and FluentMigrator bootstrap.
|
||||||
|
|
||||||
| Need | Register | Inject |
|
| Need | Register | Inject |
|
||||||
|------|----------|--------|
|
|------|----------|--------|
|
||||||
| Scoped `DataConnection` | `RegisterLinq2Db(connectionString, mappingSchema?)` | `ILinq2DbConnectionFactory` |
|
| Scoped `DataConnection` | `RegisterLinq2Db(connectionString, mappingSchema?)` | `ILinq2DbConnectionFactory` |
|
||||||
| FluentMigrator wait / create-DB / retry MigrateUp | `RegisterPostgresMigrations(connectionString, assembly, options?)` | `IRunMigrationsService` |
|
| FluentMigrator wait / create-DB / retry MigrateUp | `RegisterPostgresMigrations(connectionString, assembly, options?)` | `IRunMigrationsService` |
|
||||||
| Add-only column sync | `RegisterSchemaSync(connectionString, desiredSchema, autoSync)` | `ISchemaSyncService` |
|
|
||||||
|
|
||||||
Work leases are **not** in this package: use **MaksIT.Dapr** (`RegisterWorkLeases`) for most first-party apps, or **MaksIT.HAMode** for **maksit-nats** only.
|
Work leases are **not** in this package: use **MaksIT.Dapr** (`RegisterWorkLeases`) for most first-party apps, or **MaksIT.HAMode** for **maksit-nats** only.
|
||||||
|
|
||||||
@ -55,7 +54,7 @@ dotnet add package MaksIT.Persistence
|
|||||||
Or in your `.csproj`:
|
Or in your `.csproj`:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<PackageReference Include="MaksIT.Persistence" Version="1.0.0" />
|
<PackageReference Include="MaksIT.Persistence" Version="2.0.0" />
|
||||||
```
|
```
|
||||||
|
|
||||||
## Registering services
|
## Registering services
|
||||||
@ -73,10 +72,9 @@ services.RegisterPostgresMigrations(
|
|||||||
EnsureCoordinationTables = ct => CoordinationTableProvisioner.EnsureAsync(config.ConnectionString, ct),
|
EnsureCoordinationTables = ct => CoordinationTableProvisioner.EnsureAsync(config.ConnectionString, ct),
|
||||||
VerifySchema = ct => VerifyCoreSchemaAsync(config.ConnectionString, ct),
|
VerifySchema = ct => VerifyCoreSchemaAsync(config.ConnectionString, ct),
|
||||||
});
|
});
|
||||||
services.RegisterSchemaSync(config.ConnectionString, ProductDesiredSchema.Map, config.AutoSyncSchema);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Host `Program.cs` before `RunAsync`:
|
Schema changes go in a new FluentMigrator `[Migration]` (`Create.Table` / `Alter.Table.AddColumn`; Drop/rename needs a separate decision). Host `Program.cs` before `RunAsync`:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
await app.Services.EnsureLinq2DbMigratedAsync();
|
await app.Services.EnsureLinq2DbMigratedAsync();
|
||||||
@ -92,7 +90,6 @@ var row = db.GetTable<UserDto>().FirstOrDefault(u => u.Id == id);
|
|||||||
## What stays in the product
|
## What stays in the product
|
||||||
|
|
||||||
- Table names, Dto, FluentMapping entity list, FluentMigrator `[Migration]` classes
|
- Table names, Dto, FluentMapping entity list, FluentMigrator `[Migration]` classes
|
||||||
- Desired schema map for SchemaSync
|
|
||||||
- Coordination DDL unique to the product (hook on `PostgresMigrationOptions`)
|
- Coordination DDL unique to the product (hook on `PostgresMigrationOptions`)
|
||||||
- `I*PersistenceService` / `I*QueryService` and domain services
|
- `I*PersistenceService` / `I*QueryService` and domain services
|
||||||
- HA work leases (Dapr or HAMode)
|
- HA work leases (Dapr or HAMode)
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
using MaksIT.Persistence.Infrastructure;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.Persistence.Tests;
|
|
||||||
|
|
||||||
public class SchemaSyncDdlBuilderTests {
|
|
||||||
[Fact]
|
|
||||||
public void BuildAddOnlyDdl_AddsMissingColumns_SkipsExistingAndMissingTables() {
|
|
||||||
var desired = new Dictionary<string, IReadOnlyList<SchemaColumn>>(StringComparer.OrdinalIgnoreCase) {
|
|
||||||
["users"] = [
|
|
||||||
new SchemaColumn("id", "uuid"),
|
|
||||||
new SchemaColumn("email", "text", Nullable: true),
|
|
||||||
new SchemaColumn("is_active", "boolean"),
|
|
||||||
],
|
|
||||||
["ghost"] = [new SchemaColumn("id", "uuid")],
|
|
||||||
};
|
|
||||||
var current = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase) {
|
|
||||||
["users"] = new HashSet<string>(["id"], StringComparer.OrdinalIgnoreCase),
|
|
||||||
};
|
|
||||||
|
|
||||||
var ddl = SchemaSyncDdlBuilder.BuildAddOnlyDdl(desired, current);
|
|
||||||
|
|
||||||
Assert.Equal(2, ddl.Count);
|
|
||||||
Assert.Contains(ddl, s => s.Contains("email", StringComparison.Ordinal) && !s.Contains("NOT NULL", StringComparison.Ordinal));
|
|
||||||
Assert.Contains(ddl, s => s.Contains("is_active", StringComparison.Ordinal) && s.Contains("NOT NULL", StringComparison.Ordinal));
|
|
||||||
Assert.DoesNotContain(ddl, s => s.Contains("ghost", StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Quote_EscapesEmbeddedQuotes() =>
|
|
||||||
Assert.Equal("\"a\"\"b\"", SchemaSyncDdlBuilder.Quote("a\"b"));
|
|
||||||
}
|
|
||||||
@ -31,19 +31,6 @@ public class ServiceCollectionExtensionsTests {
|
|||||||
Assert.Throws<ArgumentException>(() => services.RegisterLinq2Db(" "));
|
Assert.Throws<ArgumentException>(() => services.RegisterLinq2Db(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RegisterSchemaSync_RegistersService() {
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
services.AddLogging();
|
|
||||||
IReadOnlyDictionary<string, IReadOnlyList<SchemaColumn>> desired = new Dictionary<string, IReadOnlyList<SchemaColumn>> {
|
|
||||||
["users"] = [new SchemaColumn("id", "uuid")],
|
|
||||||
};
|
|
||||||
|
|
||||||
services.RegisterSchemaSync("Host=localhost;Database=app;Username=u;Password=p", desired, autoSync: false);
|
|
||||||
|
|
||||||
Assert.Contains(services, d => d.ServiceType == typeof(ISchemaSyncService));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RegisterPostgresMigrations_RegistersRunner() {
|
public void RegisterPostgresMigrations_RegistersRunner() {
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|||||||
@ -71,26 +71,4 @@ public static class ServiceCollectionExtensions {
|
|||||||
.AddLogging(lb => lb.AddFluentMigratorConsole());
|
.AddLogging(lb => lb.AddFluentMigratorConsole());
|
||||||
services.AddScoped<IRunMigrationsService, RunMigrationsService>();
|
services.AddScoped<IRunMigrationsService, RunMigrationsService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers add-only schema sync. Desired schema maps stay in the product.
|
|
||||||
/// </summary>
|
|
||||||
public static void RegisterSchemaSync(
|
|
||||||
this IServiceCollection services,
|
|
||||||
string connectionString,
|
|
||||||
IReadOnlyDictionary<string, IReadOnlyList<SchemaColumn>> desiredSchema,
|
|
||||||
bool autoSync
|
|
||||||
) {
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionString))
|
|
||||||
throw new ArgumentException("connectionString is required.", nameof(connectionString));
|
|
||||||
ArgumentNullException.ThrowIfNull(desiredSchema);
|
|
||||||
|
|
||||||
services.TryAddSingleton<IDatabaseStartupObserver, NoOpDatabaseStartupObserver>();
|
|
||||||
services.AddSingleton<ISchemaSyncOptions>(new SchemaSyncOptions {
|
|
||||||
ConnectionString = connectionString,
|
|
||||||
AutoSync = autoSync,
|
|
||||||
DesiredSchema = desiredSchema,
|
|
||||||
});
|
|
||||||
services.AddScoped<ISchemaSyncService, SchemaSyncService>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -5,11 +5,11 @@ using MaksIT.Persistence.Infrastructure;
|
|||||||
namespace MaksIT.Persistence.Extensions;
|
namespace MaksIT.Persistence.Extensions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Host startup helpers for migrations and schema sync.
|
/// Host startup helpers for FluentMigrator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ServiceProviderExtensions {
|
public static class ServiceProviderExtensions {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs <see cref="IRunMigrationsService"/> then <see cref="ISchemaSyncService"/> when registered.
|
/// Runs <see cref="IRunMigrationsService"/> when registered.
|
||||||
/// Call from Program.cs before <c>RunAsync</c>.
|
/// Call from Program.cs before <c>RunAsync</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task EnsureLinq2DbMigratedAsync(
|
public static async Task EnsureLinq2DbMigratedAsync(
|
||||||
@ -22,9 +22,5 @@ public static class ServiceProviderExtensions {
|
|||||||
var run = sp.GetService<IRunMigrationsService>();
|
var run = sp.GetService<IRunMigrationsService>();
|
||||||
if (run is not null)
|
if (run is not null)
|
||||||
await run.RunAsync(cancellationToken).ConfigureAwait(false);
|
await run.RunAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var schemaSync = sp.GetService<ISchemaSyncService>();
|
|
||||||
if (schemaSync is not null)
|
|
||||||
await schemaSync.SyncSchemaAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
namespace MaksIT.Persistence.Infrastructure;
|
namespace MaksIT.Persistence.Infrastructure;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional hooks for reporting database startup phase timing (migrations, schema sync).
|
/// Optional hooks for reporting database startup phase timing (migrations).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDatabaseStartupObserver {
|
public interface IDatabaseStartupObserver {
|
||||||
/// <summary>Called when a named startup phase begins.</summary>
|
/// <summary>Called when a named startup phase begins.</summary>
|
||||||
@ -14,7 +14,7 @@ public interface IDatabaseStartupObserver {
|
|||||||
void OnPhaseFailed(string phase, TimeSpan elapsed, Exception exception);
|
void OnPhaseFailed(string phase, TimeSpan elapsed, Exception exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Well-known phase names reported during migrations and schema sync.</summary>
|
/// <summary>Well-known phase names reported during migrations.</summary>
|
||||||
public static class DatabaseStartupPhases {
|
public static class DatabaseStartupPhases {
|
||||||
/// <summary>Maintenance Postgres (database postgres) accepted connections.</summary>
|
/// <summary>Maintenance Postgres (database postgres) accepted connections.</summary>
|
||||||
public const string PostgresMaintenanceReady = "postgres_maintenance_ready";
|
public const string PostgresMaintenanceReady = "postgres_maintenance_ready";
|
||||||
@ -33,9 +33,6 @@ public static class DatabaseStartupPhases {
|
|||||||
|
|
||||||
/// <summary>Product schema verification completed.</summary>
|
/// <summary>Product schema verification completed.</summary>
|
||||||
public const string SchemaVerified = "schema_verified";
|
public const string SchemaVerified = "schema_verified";
|
||||||
|
|
||||||
/// <summary>Add-only schema sync completed.</summary>
|
|
||||||
public const string SchemaSync = "schema_sync";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>No-op observer used when the host does not register one.</summary>
|
/// <summary>No-op observer used when the host does not register one.</summary>
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
namespace MaksIT.Persistence.Infrastructure;
|
|
||||||
|
|
||||||
/// <summary>Options for <see cref="ISchemaSyncService"/>.</summary>
|
|
||||||
public interface ISchemaSyncOptions {
|
|
||||||
/// <summary>PostgreSQL connection string.</summary>
|
|
||||||
string ConnectionString { get; }
|
|
||||||
|
|
||||||
/// <summary>When false, <see cref="ISchemaSyncService.SyncSchemaAsync"/> is a no-op.</summary>
|
|
||||||
bool AutoSync { get; }
|
|
||||||
|
|
||||||
/// <summary>Desired table → columns map (product-owned).</summary>
|
|
||||||
IReadOnlyDictionary<string, IReadOnlyList<SchemaColumn>> DesiredSchema { get; }
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
namespace MaksIT.Persistence.Infrastructure;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Syncs the database schema: add missing columns only (no DROP).
|
|
||||||
/// </summary>
|
|
||||||
public interface ISchemaSyncService {
|
|
||||||
/// <summary>Applies add-only DDL when auto-sync is enabled.</summary>
|
|
||||||
Task SyncSchemaAsync(CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
namespace MaksIT.Persistence.Infrastructure;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Desired column for add-only schema sync.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Name">PostgreSQL column name (snake_case).</param>
|
|
||||||
/// <param name="PgType">PostgreSQL type, e.g. uuid, text, boolean.</param>
|
|
||||||
/// <param name="Nullable">When false, generated ADD COLUMN includes NOT NULL (no default).</param>
|
|
||||||
public sealed record SchemaColumn(string Name, string PgType, bool Nullable = false);
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
namespace MaksIT.Persistence.Infrastructure;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Builds add-only ALTER TABLE statements. Does not CREATE or DROP tables.
|
|
||||||
/// </summary>
|
|
||||||
public static class SchemaSyncDdlBuilder {
|
|
||||||
/// <summary>
|
|
||||||
/// Returns ALTER TABLE ADD COLUMN IF NOT EXISTS for columns present in <paramref name="desired"/>
|
|
||||||
/// and missing from <paramref name="current"/>. Skips tables that do not exist yet.
|
|
||||||
/// </summary>
|
|
||||||
public static IReadOnlyList<string> BuildAddOnlyDdl(
|
|
||||||
IReadOnlyDictionary<string, IReadOnlyList<SchemaColumn>> desired,
|
|
||||||
IReadOnlyDictionary<string, HashSet<string>> current
|
|
||||||
) {
|
|
||||||
List<string> ddl = [];
|
|
||||||
|
|
||||||
foreach (var (table, columns) in desired) {
|
|
||||||
if (!current.TryGetValue(table, out var existingColumns))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
foreach (var column in columns) {
|
|
||||||
if (existingColumns.Contains(column.Name))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var nullable = column.Nullable ? "" : " NOT NULL";
|
|
||||||
ddl.Add($"ALTER TABLE {Quote(table)} ADD COLUMN IF NOT EXISTS {Quote(column.Name)} {column.PgType}{nullable}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ddl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Quotes a PostgreSQL identifier.</summary>
|
|
||||||
public static string Quote(string name) => $"\"{name.Replace("\"", "\"\"", StringComparison.Ordinal)}\"";
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
namespace MaksIT.Persistence.Infrastructure;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public sealed class SchemaSyncOptions : ISchemaSyncOptions {
|
|
||||||
/// <inheritdoc />
|
|
||||||
public required string ConnectionString { get; init; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public required bool AutoSync { get; init; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public required IReadOnlyDictionary<string, IReadOnlyList<SchemaColumn>> DesiredSchema { get; init; }
|
|
||||||
}
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Npgsql;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.Persistence.Infrastructure;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public sealed class SchemaSyncService(
|
|
||||||
ISchemaSyncOptions options,
|
|
||||||
ILogger<SchemaSyncService> logger,
|
|
||||||
IDatabaseStartupObserver startupObserver
|
|
||||||
) : ISchemaSyncService {
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task SyncSchemaAsync(CancellationToken cancellationToken = default) {
|
|
||||||
if (!options.AutoSync) {
|
|
||||||
startupObserver.OnPhaseStarted(DatabaseStartupPhases.SchemaSync);
|
|
||||||
startupObserver.OnPhaseCompleted(DatabaseStartupPhases.SchemaSync, TimeSpan.Zero);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await DatabaseStartupPhaseRunner.RunAsync(
|
|
||||||
startupObserver,
|
|
||||||
DatabaseStartupPhases.SchemaSync,
|
|
||||||
RunSyncCoreAsync,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RunSyncCoreAsync(CancellationToken cancellationToken) {
|
|
||||||
logger.LogInformation("Schema sync (add-only) starting...");
|
|
||||||
|
|
||||||
var current = await GetCurrentSchemaAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
var ddl = SchemaSyncDdlBuilder.BuildAddOnlyDdl(options.DesiredSchema, current);
|
|
||||||
if (ddl.Count == 0) {
|
|
||||||
logger.LogInformation("Schema sync: no changes needed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var conn = new NpgsqlConnection(options.ConnectionString);
|
|
||||||
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
await using var tx = await conn.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
foreach (var sql in ddl) {
|
|
||||||
await using var cmd = new NpgsqlCommand(sql, conn, tx);
|
|
||||||
await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
logger.LogDebug("Executed: {Sql}", sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
await tx.CommitAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
logger.LogInformation("Schema sync completed. Applied {Count} DDL statement(s).", ddl.Count);
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
await tx.RollbackAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<Dictionary<string, HashSet<string>>> GetCurrentSchemaAsync(CancellationToken ct) {
|
|
||||||
var result = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
await using var conn = new NpgsqlConnection(options.ConnectionString);
|
|
||||||
await conn.OpenAsync(ct).ConfigureAwait(false);
|
|
||||||
await using var cmd = new NpgsqlCommand("""
|
|
||||||
SELECT table_name, column_name
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
ORDER BY table_name, ordinal_position
|
|
||||||
""", conn);
|
|
||||||
|
|
||||||
await using var reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
|
||||||
while (await reader.ReadAsync(ct).ConfigureAwait(false)) {
|
|
||||||
var table = reader.GetString(0);
|
|
||||||
var column = reader.GetString(1);
|
|
||||||
|
|
||||||
if (!result.TryGetValue(table, out var columns))
|
|
||||||
result[table] = columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
columns.Add(column);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,11 +7,11 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
|
||||||
<PackageId>MaksIT.Persistence</PackageId>
|
<PackageId>MaksIT.Persistence</PackageId>
|
||||||
<Version>1.0.0</Version>
|
<Version>2.0.0</Version>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
<Company>MAKS-IT</Company>
|
<Company>MAKS-IT</Company>
|
||||||
<Product>MaksIT.Persistence</Product>
|
<Product>MaksIT.Persistence</Product>
|
||||||
<Description>MaksIT.Persistence product-agnostic facades for linq2db PostgreSQL: connection factory, FluentMigrator startup, add-only schema sync, and Result guards.</Description>
|
<Description>MaksIT.Persistence product-agnostic facades for linq2db PostgreSQL: connection factory, FluentMigrator startup, and Result guards.</Description>
|
||||||
<PackageTags>dotnet;linq2db;postgresql;fluentmigrator;</PackageTags>
|
<PackageTags>dotnet;linq2db;postgresql;fluentmigrator;</PackageTags>
|
||||||
<RepositoryUrl>https://github.com/MAKS-IT-COM/maksit-persistence</RepositoryUrl>
|
<RepositoryUrl>https://github.com/MAKS-IT-COM/maksit-persistence</RepositoryUrl>
|
||||||
<License>MIT</License>
|
<License>MIT</License>
|
||||||
|
|||||||
@ -6,16 +6,65 @@
|
|||||||
Keep a Changelog header parsing and section extraction.
|
Keep a Changelog header parsing and section extraction.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Supports only the standard Keep a Changelog version line:
|
Supports Keep a Changelog version lines and shared SemVer checks, including prerelease:
|
||||||
## [1.0.0] - 2026-05-24
|
## [1.0.0] - 2026-05-24
|
||||||
|
## [0.1.0-alpha.1] - 2026-08-21
|
||||||
|
## [0.1.0-beta.1] - 2026-08-21
|
||||||
|
## [0.1.0-rc.1] - 2026-08-21
|
||||||
#>
|
#>
|
||||||
|
|
||||||
|
function Get-ChangelogSemverPattern {
|
||||||
|
return '\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-ReleaseSemver {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
return [bool]($Version -match ('^' + (Get-ChangelogSemverPattern) + '$'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-ReleaseSemverPrerelease {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
return (Test-ReleaseSemver -Version $Version) -and ($Version -match '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ReleaseSemverPrereleaseLabel {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-ReleaseSemverPrerelease -Version $Version)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Version -match '^\d+\.\d+\.\d+-([A-Za-z][0-9A-Za-z]*)') {
|
||||||
|
return $Matches[1].ToLowerInvariant()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'next'
|
||||||
|
}
|
||||||
|
|
||||||
function Get-ChangelogVersionHeaderPattern {
|
function Get-ChangelogVersionHeaderPattern {
|
||||||
return '(?m)^##\s+\[(\d+\.\d+\.\d+)\]\s*-\s*\d{4}-\d{2}-\d{2}\s*$'
|
return '(?m)^##\s+\[(' + (Get-ChangelogSemverPattern) + ')\]\s*-\s*\d{4}-\d{2}-\d{2}\s*$'
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-ChangelogNextVersionHeaderPattern {
|
function Get-ChangelogNextVersionHeaderPattern {
|
||||||
return '(?m)^##\s+\[\d+\.\d+\.\d+\]\s*-\s*\d{4}-\d{2}-\d{2}\s*$'
|
return '(?m)^##\s+\[' + (Get-ChangelogSemverPattern) + '\]\s*-\s*\d{4}-\d{2}-\d{2}\s*$'
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-LatestChangelogVersion {
|
function Get-LatestChangelogVersion {
|
||||||
@ -53,4 +102,4 @@ function Get-ChangelogReleaseNotesSection {
|
|||||||
return $match.Value.Trim()
|
return $match.Value.Trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
Export-ModuleMember -Function Get-ChangelogVersionHeaderPattern, Get-ChangelogNextVersionHeaderPattern, Get-LatestChangelogVersion, Get-ChangelogReleaseNotesSection
|
Export-ModuleMember -Function Get-ChangelogSemverPattern, Test-ReleaseSemver, Test-ReleaseSemverPrerelease, Get-ReleaseSemverPrereleaseLabel, Get-ChangelogVersionHeaderPattern, Get-ChangelogNextVersionHeaderPattern, Get-LatestChangelogVersion, Get-ChangelogReleaseNotesSection
|
||||||
|
|||||||
@ -7,9 +7,11 @@
|
|||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Dedicated version-loading plugin. Reads <Version> from the first configured
|
Dedicated version-loading plugin. Reads <Version> from the first configured
|
||||||
projectFiles entry and writes it (plus the resolved projectFiles) to the
|
projectFiles .csproj, or from the nearest Directory.Build.props when the
|
||||||
shared runtime context. Declares providesVersion = $true so the engine can
|
csproj omits it. Accepts SemVer prerelease (0.1.0-alpha.1 / beta / rc). Writes
|
||||||
discover it as the single release version source.
|
version plus the resolved projectFiles (csproj paths for later pack/publish)
|
||||||
|
to the shared runtime context. Declares providesVersion = $true so the engine
|
||||||
|
can discover it as the single release version source.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
@ -20,6 +22,22 @@ if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ConvertTo-MsbuildPropertyStringInternal {
|
||||||
|
param(
|
||||||
|
$Value
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Value) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Value -is [System.Xml.XmlElement]) {
|
||||||
|
return [string]$Value.InnerText
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$Value
|
||||||
|
}
|
||||||
|
|
||||||
function Get-CsprojPropertyValueInternal {
|
function Get-CsprojPropertyValueInternal {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -36,7 +54,33 @@ function Get-CsprojPropertyValueInternal {
|
|||||||
Select-Object -First 1
|
Select-Object -First 1
|
||||||
|
|
||||||
if ($propNode) {
|
if ($propNode) {
|
||||||
return $propNode.$PropertyName
|
return ConvertTo-MsbuildPropertyStringInternal -Value $propNode.$PropertyName
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-DirectoryBuildPropsVersionInternal {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ProjectPath
|
||||||
|
)
|
||||||
|
|
||||||
|
# MSBuild uses the first Directory.Build.props found walking up from the project directory.
|
||||||
|
$dir = [System.IO.Path]::GetDirectoryName((Resolve-Path -LiteralPath $ProjectPath))
|
||||||
|
while (-not [string]::IsNullOrWhiteSpace($dir)) {
|
||||||
|
$propsPath = Join-Path $dir 'Directory.Build.props'
|
||||||
|
if (Test-Path -LiteralPath $propsPath -PathType Leaf) {
|
||||||
|
[xml]$props = Get-Content -LiteralPath $propsPath
|
||||||
|
return Get-CsprojPropertyValueInternal -Csproj $props -PropertyName 'Version'
|
||||||
|
}
|
||||||
|
|
||||||
|
$parent = [System.IO.Directory]::GetParent($dir)
|
||||||
|
if ($null -eq $parent) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir = $parent.FullName
|
||||||
}
|
}
|
||||||
|
|
||||||
return $null
|
return $null
|
||||||
@ -58,14 +102,18 @@ function Get-CsprojVersionInternal {
|
|||||||
|
|
||||||
[xml]$csproj = Get-Content $ProjectPath
|
[xml]$csproj = Get-Content $ProjectPath
|
||||||
$version = Get-CsprojPropertyValueInternal -Csproj $csproj -PropertyName "Version"
|
$version = Get-CsprojPropertyValueInternal -Csproj $csproj -PropertyName "Version"
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$version)) {
|
||||||
if ([string]::IsNullOrWhiteSpace([string]$version)) {
|
return [string]$version
|
||||||
throw "DotNetReleaseVersion: <Version> not found in '$ProjectPath'."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$version = Get-DirectoryBuildPropsVersionInternal -ProjectPath $ProjectPath
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$version)) {
|
||||||
return [string]$version
|
return [string]$version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw "DotNetReleaseVersion: <Version> not found in '$ProjectPath' or a parent Directory.Build.props."
|
||||||
|
}
|
||||||
|
|
||||||
function Get-PluginMetadata {
|
function Get-PluginMetadata {
|
||||||
return [pscustomobject]@{ providesVersion = $true }
|
return [pscustomobject]@{ providesVersion = $true }
|
||||||
}
|
}
|
||||||
@ -87,6 +135,10 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
Write-Log -Level "INFO" -Message "Reading version from SDK-style project file (projectFiles)..."
|
Write-Log -Level "INFO" -Message "Reading version from SDK-style project file (projectFiles)..."
|
||||||
$version = Get-CsprojVersionInternal -ProjectPath $projectFiles[0]
|
$version = Get-CsprojVersionInternal -ProjectPath $projectFiles[0]
|
||||||
|
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||||
|
if (-not (Test-ReleaseSemver -Version $version)) {
|
||||||
|
throw "DotNetReleaseVersion: version '$version' is not a valid semver (X.Y.Z or X.Y.Z-prerelease)."
|
||||||
|
}
|
||||||
|
|
||||||
Set-EngineState -Context $shared -Name 'version' -Value $version
|
Set-EngineState -Context $shared -Name 'version' -Value $version
|
||||||
Set-EngineFact -Context $shared -Namespace 'dotnet' -Name 'projectFiles' -Value $projectFiles -Overwrite Replace -LegacyProperty 'projectFiles'
|
Set-EngineFact -Context $shared -Namespace 'dotnet' -Name 'projectFiles' -Value $projectFiles -Overwrite Replace -LegacyProperty 'projectFiles'
|
||||||
|
|||||||
@ -28,6 +28,7 @@ function Invoke-Plugin {
|
|||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
||||||
|
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Get-ReleaseSemverPrereleaseLabel"
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$shared = $Settings.context
|
$shared = $Settings.context
|
||||||
@ -62,6 +63,14 @@ function Invoke-Plugin {
|
|||||||
[string]$pluginSettings.access
|
[string]$pluginSettings.access
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$npmDistTag = $null
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.npmDistTag)) {
|
||||||
|
$npmDistTag = [string]$pluginSettings.npmDistTag
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$npmDistTag = Get-ReleaseSemverPrereleaseLabel -Version ([string]$shared.version)
|
||||||
|
}
|
||||||
|
|
||||||
$publishOrder = @()
|
$publishOrder = @()
|
||||||
if ($pluginSettings.publishOrder) {
|
if ($pluginSettings.publishOrder) {
|
||||||
if ($pluginSettings.publishOrder -is [System.Collections.IEnumerable] -and -not ($pluginSettings.publishOrder -is [string])) {
|
if ($pluginSettings.publishOrder -is [System.Collections.IEnumerable] -and -not ($pluginSettings.publishOrder -is [string])) {
|
||||||
@ -84,7 +93,8 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
if ($dryRun) {
|
if ($dryRun) {
|
||||||
foreach ($packageName in $publishOrder) {
|
foreach ($packageName in $publishOrder) {
|
||||||
Write-Log -Level "INFO" -Message "Dry run: would publish npm package '$packageName' to $registry"
|
$tagNote = if ([string]::IsNullOrWhiteSpace($npmDistTag)) { 'latest' } else { $npmDistTag }
|
||||||
|
Write-Log -Level "INFO" -Message "Dry run: would publish npm package '$packageName' to $registry (dist-tag $tagNote)"
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -112,13 +122,20 @@ registry=$registry
|
|||||||
|
|
||||||
foreach ($packageName in $publishOrder) {
|
foreach ($packageName in $publishOrder) {
|
||||||
Write-Log -Level "STEP" -Message "Publishing npm package '$packageName'..."
|
Write-Log -Level "STEP" -Message "Publishing npm package '$packageName'..."
|
||||||
|
$publishArgs = @('publish')
|
||||||
if ($useWorkspaces) {
|
if ($useWorkspaces) {
|
||||||
npm publish -w $packageName --access $access --userconfig $tempNpmRcPath
|
$publishArgs += @('-w', $packageName)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Assert-NpmRootPackageName -WorkspaceRoot $workspaceRoot -ExpectedPackageName $packageName
|
Assert-NpmRootPackageName -WorkspaceRoot $workspaceRoot -ExpectedPackageName $packageName
|
||||||
npm publish --access $access --userconfig $tempNpmRcPath
|
|
||||||
}
|
}
|
||||||
|
$publishArgs += @('--access', $access, '--userconfig', $tempNpmRcPath)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($npmDistTag)) {
|
||||||
|
$publishArgs += @('--tag', $npmDistTag)
|
||||||
|
Write-Log -Level "INFO" -Message " Using npm dist-tag '$npmDistTag' (prerelease)."
|
||||||
|
}
|
||||||
|
|
||||||
|
npm @publishArgs
|
||||||
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "Failed to publish npm package '$packageName'."
|
throw "Failed to publish npm package '$packageName'."
|
||||||
|
|||||||
@ -35,8 +35,9 @@ function Get-PackageJsonVersionInternal {
|
|||||||
throw "NpmReleaseVersion: 'version' is missing in '$PackageJsonPath'."
|
throw "NpmReleaseVersion: 'version' is missing in '$PackageJsonPath'."
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($version -notmatch '^\d+\.\d+\.\d+') {
|
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||||
throw "NpmReleaseVersion: version '$version' in '$PackageJsonPath' is not a valid semver."
|
if (-not (Test-ReleaseSemver -Version $version)) {
|
||||||
|
throw "NpmReleaseVersion: version '$version' in '$PackageJsonPath' is not a valid semver (X.Y.Z or X.Y.Z-prerelease)."
|
||||||
}
|
}
|
||||||
|
|
||||||
return $version
|
return $version
|
||||||
@ -69,6 +70,7 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
|
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
|
||||||
|
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$shared = $Settings.context
|
$shared = $Settings.context
|
||||||
|
|||||||
@ -7,9 +7,10 @@
|
|||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Reads a single-line semver from the configured versionFilePath (default
|
Reads a single-line semver from the configured versionFilePath (default
|
||||||
repo-root VERSION). Useful for repositories without .csproj or package.json
|
repo-root VERSION), including optional prerelease (0.1.0-alpha.1). Useful for
|
||||||
version metadata. Declares providesVersion = $true so the engine can
|
repositories without .csproj or package.json version metadata. Declares
|
||||||
discover it as the single release version source.
|
providesVersion = $true so the engine can discover it as the single release
|
||||||
|
version source.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
@ -36,8 +37,9 @@ function Get-VersionFileSemverInternal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$version = $version -replace '^[vV]', ''
|
$version = $version -replace '^[vV]', ''
|
||||||
if ($version -notmatch '^\d+\.\d+\.\d+') {
|
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||||
throw "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver."
|
if (-not (Test-ReleaseSemver -Version $version)) {
|
||||||
|
throw "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver (X.Y.Z or X.Y.Z-prerelease)."
|
||||||
}
|
}
|
||||||
|
|
||||||
return $version
|
return $version
|
||||||
@ -55,6 +57,7 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
|
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
|
||||||
|
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||||
|
|
||||||
$shared = $Settings.context
|
$shared = $Settings.context
|
||||||
$versionFileSetting = if ($Settings.versionFilePath) {
|
$versionFileSetting = if ($Settings.versionFilePath) {
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
repository, and creates the configured GitHub release using the
|
repository, and creates the configured GitHub release using the
|
||||||
shared release artifacts and release notes from CHANGELOG.md.
|
shared release artifacts and release notes from CHANGELOG.md.
|
||||||
Release notes must use Keep a Changelog headers: ## [semver] - YYYY-MM-DD
|
Release notes must use Keep a Changelog headers: ## [semver] - YYYY-MM-DD
|
||||||
(see ChangelogSupport.psm1).
|
(including optional SemVer prerelease, e.g. ## [0.1.0-alpha.1] / [0.1.0-beta.1] / [0.1.0-rc.1];
|
||||||
|
see ChangelogSupport.psm1). Hyphenated versions are created with gh --prerelease.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
@ -95,6 +96,7 @@ function Invoke-Plugin {
|
|||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Get-LatestChangelogVersion"
|
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Get-LatestChangelogVersion"
|
||||||
|
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemverPrerelease"
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Get-EngineFact"
|
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Get-EngineFact"
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
@ -128,6 +130,9 @@ function Invoke-Plugin {
|
|||||||
}
|
}
|
||||||
$releaseName = $releaseTitlePattern -replace '\{version\}', $version
|
$releaseName = $releaseTitlePattern -replace '\{version\}', $version
|
||||||
Write-Log -Level "INFO" -Message "Dry run: would create GitHub release '$releaseName' ($tag) on $repo"
|
Write-Log -Level "INFO" -Message "Dry run: would create GitHub release '$releaseName' ($tag) on $repo"
|
||||||
|
if (Test-ReleaseSemverPrerelease -Version ([string]$version)) {
|
||||||
|
Write-Log -Level "INFO" -Message "Dry run: release would be marked prerelease."
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,6 +266,10 @@ function Invoke-Plugin {
|
|||||||
"--title", $releaseName,
|
"--title", $releaseName,
|
||||||
"--notes-file", $notesFilePath
|
"--notes-file", $notesFilePath
|
||||||
)
|
)
|
||||||
|
if (Test-ReleaseSemverPrerelease -Version ([string]$version)) {
|
||||||
|
$createReleaseArgs += '--prerelease'
|
||||||
|
}
|
||||||
|
|
||||||
& gh @createReleaseArgs
|
& gh @createReleaseArgs
|
||||||
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
|||||||
@ -11,8 +11,9 @@
|
|||||||
when they do not (whenRequirementsNotMet: skip). Publish plugins no longer use per-plugin
|
when they do not (whenRequirementsNotMet: skip). Publish plugins no longer use per-plugin
|
||||||
branch lists; put allowed branches here instead.
|
branch lists; put allowed branches here instead.
|
||||||
|
|
||||||
Typical checks: allowed branches, optional clean working tree, exact semver tag on HEAD,
|
Typical checks: allowed branches, optional clean working tree, exact semver tag on HEAD
|
||||||
tag version vs DotNetReleaseVersion, optional push tag to remote.
|
(vX.Y.Z or vX.Y.Z-prerelease such as v0.1.0-alpha.1 / v0.1.0-beta.1 / v0.1.0-rc.1),
|
||||||
|
tag version vs release version, optional push tag to remote.
|
||||||
|
|
||||||
The engine preflight no longer reads git tags; this plugin sets context.tag from the
|
The engine preflight no longer reads git tags; this plugin sets context.tag from the
|
||||||
git tag on HEAD when required. Shared context version always remains from DotNetReleaseVersion.
|
git tag on HEAD when required. Shared context version always remains from DotNetReleaseVersion.
|
||||||
@ -77,6 +78,7 @@ function Invoke-Plugin {
|
|||||||
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Get-GitStatusShort"
|
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Get-GitStatusShort"
|
||||||
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Test-RemoteTagExists"
|
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Test-RemoteTagExists"
|
||||||
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Push-TagToRemote"
|
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Push-TagToRemote"
|
||||||
|
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Get-ChangelogSemverPattern"
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$shared = $Settings.context
|
$shared = $Settings.context
|
||||||
@ -123,8 +125,9 @@ function Invoke-Plugin {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($tag -notmatch '^v(\d+\.\d+\.\d+)$') {
|
$tagPattern = '^v(' + (Get-ChangelogSemverPattern) + ')$'
|
||||||
Invoke-NotMetInternal -Shared $shared -When $when -Reason "tag '$tag' must match vX.Y.Z."
|
if ($tag -notmatch $tagPattern) {
|
||||||
|
Invoke-NotMetInternal -Shared $shared -When $when -Reason "tag '$tag' must match vX.Y.Z or vX.Y.Z-prerelease (e.g. v0.1.0-alpha.1, v0.1.0-beta.1, v0.1.0-rc.1)."
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user