mirror of
https://github.com/MAKS-IT-COM/maksit-persistence.git
synced 2026-11-14 21:56:10 +01:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1103c502ca | ||
|
|
4a79a98b98 |
34
.github/workflows/publish-nuget.yml
vendored
Normal file
34
.github/workflows/publish-nuget.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: Publish to NuGet
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v5
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
|
- name: Pack
|
||||||
|
run: |
|
||||||
|
dotnet pack src/MaksIT.Persistence/MaksIT.Persistence.csproj -c Release -o artifacts
|
||||||
|
|
||||||
|
- name: NuGet login (OIDC)
|
||||||
|
id: login
|
||||||
|
uses: NuGet/login@v1
|
||||||
|
with:
|
||||||
|
user: maks-it
|
||||||
|
|
||||||
|
- name: Push to NuGet
|
||||||
|
run: dotnet nuget push artifacts/*.nupkg --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||||
14
CHANGELOG.md
14
CHANGELOG.md
@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.0.1] - 2026-10-27
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bumped package dependencies: linq2db / linq2db.PostgreSQL 6.5.1, MaksIT.Core 1.6.14, MaksIT.Results 2.0.6, Microsoft.Extensions.* 10.0.14 (tests: coverlet.mtp, xunit.v3).
|
||||||
|
- Synced RepoUtils utils: ContainerRegistry JSON catalog (PascalCase Harbor / InCluster keys); `CleanupArtifacts` replaces `DotNetCleanupArtifacts`; GitHub `githubSecret` is `GitClone`; release `scriptSettings` declares `repoUtilsSecretsShared` / `repoUtilsSecrets`; dropped unused Npm plugins from release settings.
|
||||||
|
- NuGet publish moved to GitHub Actions Trusted Publishing (OIDC on `v*` tags); removed `DotNetNuGet` from the RepoUtils release pipeline.
|
||||||
|
- Docs: CONTRIBUTING points at RepoUtils Origin; README HAMode note is **maksit-buoy**.
|
||||||
|
|
||||||
|
## [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,9 +90,11 @@ 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://git.maks-it.com/MAKS-IT/maksit-repoutils), local-copy sync). Product-specific settings are in each engine’s `scriptSettings.json`.
|
||||||
|
|
||||||
### Run tests and coverage badges
|
### Run tests and coverage badges
|
||||||
|
|
||||||
|
|||||||
19
README.md
19
README.md
@ -1,10 +1,10 @@
|
|||||||
# MaksIT.Persistence
|
# MaksIT.Persistence
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
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,15 +22,14 @@ 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-buoy** only.
|
||||||
|
|
||||||
## Result conventions
|
## Result conventions
|
||||||
|
|
||||||
@ -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.1" />
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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)
|
||||||
|
|||||||
@ -12,11 +12,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.MTP" Version="10.0.1" />
|
<PackageReference Include="coverlet.MTP" Version="10.1.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.14" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.11" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.14" />
|
||||||
<PackageReference Include="Moq" Version="4.*" />
|
<PackageReference Include="Moq" Version="4.*" />
|
||||||
<PackageReference Include="xunit.v3" Version="4.0.0" />
|
<PackageReference Include="xunit.v3" Version="4.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -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.1</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>
|
||||||
@ -23,12 +23,12 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentMigrator" Version="8.0.1" />
|
<PackageReference Include="FluentMigrator" Version="8.0.1" />
|
||||||
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="8.0.1" />
|
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="8.0.1" />
|
||||||
<PackageReference Include="linq2db" Version="6.4.0" />
|
<PackageReference Include="linq2db" Version="6.5.1" />
|
||||||
<PackageReference Include="linq2db.PostgreSQL" Version="6.4.0" />
|
<PackageReference Include="linq2db.PostgreSQL" Version="6.5.1" />
|
||||||
<PackageReference Include="MaksIT.Core" Version="1.6.9" />
|
<PackageReference Include="MaksIT.Core" Version="1.6.14" />
|
||||||
<PackageReference Include="MaksIT.Results" Version="2.0.4" />
|
<PackageReference Include="MaksIT.Results" Version="2.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.11" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.14" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.11" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.14" />
|
||||||
<PackageReference Include="Npgsql" Version="10.0.3" />
|
<PackageReference Include="Npgsql" Version="10.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,10 @@
|
|||||||
.NOTES
|
.NOTES
|
||||||
Per-plugin dryRun on mutatesRemote plugins validates without remote mutation.
|
Per-plugin dryRun on mutatesRemote plugins validates without remote mutation.
|
||||||
There is no engine-wide dryRun switch — set dryRun on each plugin as needed.
|
There is no engine-wide dryRun switch — set dryRun on each plugin as needed.
|
||||||
|
|
||||||
|
-InstallOnly (Install-Helm-*.bat) keeps the version plugin and HelmSelfDeploy.
|
||||||
|
Compile, test, image build, image push, and HelmPush are skipped. The chart
|
||||||
|
must already be in OCI at the version the version plugin reports.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
# Keep a plain param block so unbound launcher arguments remain in $args for
|
# Keep a plain param block so unbound launcher arguments remain in $args for
|
||||||
@ -17,6 +21,17 @@ param()
|
|||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
Set-Location $PSScriptRoot
|
Set-Location $PSScriptRoot
|
||||||
|
|
||||||
|
$installOnly = $false
|
||||||
|
$extensionArgs = [System.Collections.Generic.List[object]]::new()
|
||||||
|
foreach ($argument in @($args)) {
|
||||||
|
if ([string]$argument -eq '-InstallOnly') {
|
||||||
|
$installOnly = $true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$extensionArgs.Add($argument)
|
||||||
|
}
|
||||||
|
|
||||||
$srcDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
$srcDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||||
|
|
||||||
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
|
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
|
||||||
@ -24,7 +39,7 @@ Import-EngineModules -Engine Release
|
|||||||
|
|
||||||
$releaseExtension = $null
|
$releaseExtension = $null
|
||||||
if (Get-Command Initialize-ReleaseExtension -ErrorAction SilentlyContinue) {
|
if (Get-Command Initialize-ReleaseExtension -ErrorAction SilentlyContinue) {
|
||||||
$releaseExtension = Initialize-ReleaseExtension -ScriptDir $PSScriptRoot -ArgumentList $args
|
$releaseExtension = Initialize-ReleaseExtension -ScriptDir $PSScriptRoot -ArgumentList @($extensionArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
$settings = if ($null -ne $releaseExtension) {
|
$settings = if ($null -ne $releaseExtension) {
|
||||||
@ -35,8 +50,29 @@ else {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||||
|
if ($installOnly) {
|
||||||
|
$enabledBefore = @($configuredPlugins | Where-Object { $_.enabled } | ForEach-Object { [string]$_.name })
|
||||||
|
$configuredPlugins = @(Select-HelmInstallPlugins -Plugins $configuredPlugins -ScriptDir $PSScriptRoot)
|
||||||
|
$kept = @($configuredPlugins | ForEach-Object { [string]$_.name })
|
||||||
|
$skipped = @($enabledBefore | Where-Object { $_ -notin $kept })
|
||||||
|
if ($skipped.Count -gt 0) {
|
||||||
|
Write-Log -Level 'INFO' -Message "Install-only: skipping $($skipped -join ', '). Chart and images are taken from OCI."
|
||||||
|
}
|
||||||
|
|
||||||
$releaseBanner = if ($null -ne $releaseExtension) {
|
Write-Log -Level 'INFO' -Message "Install-only: running $($kept -join ', ')."
|
||||||
|
}
|
||||||
|
|
||||||
|
Initialize-RepoUtilsVaultSecrets -Settings $settings -Plugins $configuredPlugins
|
||||||
|
|
||||||
|
$releaseBanner = if ($installOnly) {
|
||||||
|
$modeLabel = 'ha'
|
||||||
|
if ($null -ne $releaseExtension -and $releaseExtension.ContextData.PSObject.Properties.Name -contains 'deployMode') {
|
||||||
|
$modeLabel = [string]$releaseExtension.ContextData.deployMode
|
||||||
|
}
|
||||||
|
|
||||||
|
"HELM INSTALL (deploy mode: $modeLabel; OCI chart, no build or push)"
|
||||||
|
}
|
||||||
|
elseif ($null -ne $releaseExtension) {
|
||||||
$releaseExtension.StepBanner
|
$releaseExtension.StepBanner
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -96,7 +132,11 @@ if (-not $releaseStageInitialized) {
|
|||||||
|
|
||||||
Write-Log -Level 'OK' -Message '=================================================='
|
Write-Log -Level 'OK' -Message '=================================================='
|
||||||
if ($releaseHadPluginFailures) {
|
if ($releaseHadPluginFailures) {
|
||||||
Write-Log -Level 'ERROR' -Message 'RELEASE FAILED'
|
$failureLabel = if ($installOnly) { 'HELM INSTALL FAILED' } else { 'RELEASE FAILED' }
|
||||||
|
Write-Log -Level 'ERROR' -Message $failureLabel
|
||||||
|
}
|
||||||
|
elseif ($installOnly) {
|
||||||
|
Write-Log -Level 'OK' -Message 'HELM INSTALL COMPLETE'
|
||||||
}
|
}
|
||||||
elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins) {
|
elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins) {
|
||||||
Write-Log -Level 'OK' -Message 'RUN COMPLETE (publish skipped by ReleasePublishGuard)'
|
Write-Log -Level 'OK' -Message 'RUN COMPLETE (publish skipped by ReleasePublishGuard)'
|
||||||
@ -109,7 +149,7 @@ else {
|
|||||||
}
|
}
|
||||||
Write-Log -Level 'OK' -Message '=================================================='
|
Write-Log -Level 'OK' -Message '=================================================='
|
||||||
|
|
||||||
if ($engineContext.isNonReleaseBranch -and -not ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins)) {
|
if (-not $installOnly -and $engineContext.isNonReleaseBranch -and -not ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins)) {
|
||||||
$preferredReleaseBranch = Get-PreferredReleaseBranch -EngineContext $engineContext
|
$preferredReleaseBranch = Get-PreferredReleaseBranch -EngineContext $engineContext
|
||||||
Write-Log -Level 'INFO' -Message "For publish, use an allowed branch (see ReleasePublishGuard.branches), e.g. '$preferredReleaseBranch', and satisfy the guard requirements."
|
Write-Log -Level 'INFO' -Message "For publish, use an allowed branch (see ReleasePublishGuard.branches), e.g. '$preferredReleaseBranch', and satisfy the guard requirements."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
"$schema": "https://json-schema.org/draft-07/schema",
|
"$schema": "https://json-schema.org/draft-07/schema",
|
||||||
"title": "Release Package Script Settings",
|
"title": "Release Package Script Settings",
|
||||||
"description": "Invoke-ReleasePackage.ps1 plugin settings for maksit-persistence.",
|
"description": "Invoke-ReleasePackage.ps1 plugin settings for maksit-persistence.",
|
||||||
|
"repoUtilsSecretsShared": "RepoUtilsSecretsShared",
|
||||||
|
"repoUtilsSecrets": "RepoUtilsSecrets",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "DotNetReleaseVersion",
|
"name": "DotNetReleaseVersion",
|
||||||
@ -61,45 +63,13 @@
|
|||||||
"name": "GitHub",
|
"name": "GitHub",
|
||||||
"stageLabel": "release",
|
"stageLabel": "release",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"githubSecret": "GitHub",
|
"githubSecret": "GitClone",
|
||||||
"repository": "https://github.com/MAKS-IT-COM/maksit-persistence",
|
"repository": "https://github.com/MAKS-IT-COM/maksit-persistence",
|
||||||
"releaseNotesFile": "..\\..\\..\\CHANGELOG.md",
|
"releaseNotesFile": "..\\..\\..\\CHANGELOG.md",
|
||||||
"releaseTitlePattern": "Release {version}"
|
"releaseTitlePattern": "Release {version}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "DotNetNuGet",
|
"name": "CleanupArtifacts",
|
||||||
"stageLabel": "release",
|
|
||||||
"enabled": true,
|
|
||||||
"nugetSecret": "NuGet",
|
|
||||||
"source": "https://api.nuget.org/v3/index.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "NpmReleaseVersion",
|
|
||||||
"stageLabel": "build",
|
|
||||||
"enabled": false,
|
|
||||||
"packageJsonPath": "..\\..\\..\\src\\package.json",
|
|
||||||
"syncWorkspaceVersions": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "NpmBuild",
|
|
||||||
"stageLabel": "build",
|
|
||||||
"enabled": false,
|
|
||||||
"workspaceRoot": "..\\..\\..\\src",
|
|
||||||
"useCi": true,
|
|
||||||
"buildScript": "build"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "NpmPublish",
|
|
||||||
"stageLabel": "release",
|
|
||||||
"enabled": false,
|
|
||||||
"npmSecret": "Npm",
|
|
||||||
"registry": "https://registry.npmjs.org",
|
|
||||||
"access": "public",
|
|
||||||
"workspaceRoot": "..\\..\\..\\src",
|
|
||||||
"publishOrder": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "DotNetCleanupArtifacts",
|
|
||||||
"stageLabel": "release",
|
"stageLabel": "release",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"includePatterns": [
|
"includePatterns": [
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -321,6 +321,37 @@ function Get-EngineState {
|
|||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Add-ReleaseAssetPaths {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[psobject]$Context,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[string[]]$Path
|
||||||
|
)
|
||||||
|
|
||||||
|
$list = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$existing = Get-EngineFact -Context $Context -Namespace 'release' -Name 'assetPaths' -Default @() -LegacyProperty @('releaseAssetPaths')
|
||||||
|
foreach ($item in @($existing)) {
|
||||||
|
$value = [string]$item
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($value) -and -not $list.Contains($value)) {
|
||||||
|
$list.Add($value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($item in @($Path)) {
|
||||||
|
$value = [string]$item
|
||||||
|
if ([string]::IsNullOrWhiteSpace($value) -or $list.Contains($value)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$list.Add($value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-EngineFact -Context $Context -Namespace 'release' -Name 'assetPaths' -Value @($list) -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
||||||
|
}
|
||||||
|
|
||||||
Export-ModuleMember -Function `
|
Export-ModuleMember -Function `
|
||||||
Resolve-RelativePaths, `
|
Resolve-RelativePaths, `
|
||||||
Initialize-EngineFactsBag, `
|
Initialize-EngineFactsBag, `
|
||||||
@ -329,4 +360,5 @@ Export-ModuleMember -Function `
|
|||||||
Test-EngineFact, `
|
Test-EngineFact, `
|
||||||
Set-EngineState, `
|
Set-EngineState, `
|
||||||
Add-EnginePublishCompletion, `
|
Add-EnginePublishCompletion, `
|
||||||
|
Add-ReleaseAssetPaths, `
|
||||||
Get-EngineState
|
Get-EngineState
|
||||||
|
|||||||
@ -15,6 +15,7 @@ function Import-EngineModules {
|
|||||||
(Join-Path $modulesDir 'ScriptConfig.psm1'),
|
(Join-Path $modulesDir 'ScriptConfig.psm1'),
|
||||||
(Join-Path $modulesDir 'Logging.psm1'),
|
(Join-Path $modulesDir 'Logging.psm1'),
|
||||||
(Join-Path $engineModuleDir 'PluginSupport.psm1'),
|
(Join-Path $engineModuleDir 'PluginSupport.psm1'),
|
||||||
|
(Join-Path $engineModuleDir 'VaultSupport.psm1'),
|
||||||
(Join-Path $engineModuleDir 'EngineContext.psm1')
|
(Join-Path $engineModuleDir 'EngineContext.psm1')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ function Test-IsEngineRuntimeModuleName {
|
|||||||
'TestRunner',
|
'TestRunner',
|
||||||
'EngineContext',
|
'EngineContext',
|
||||||
'PluginSupport',
|
'PluginSupport',
|
||||||
|
'VaultSupport',
|
||||||
'ReleaseSupport',
|
'ReleaseSupport',
|
||||||
'TestSupport'
|
'TestSupport'
|
||||||
),
|
),
|
||||||
@ -53,7 +54,7 @@ function Get-PluginDependencyGroupDirectories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Prefer Shared (helpers), then stock host groups; any other plugins/{Group}/ is discovered.
|
# Prefer Shared (helpers), then stock host groups; any other plugins/{Group}/ is discovered.
|
||||||
$preferred = @('Shared', 'Platform', 'DotNet', 'Npm')
|
$preferred = @('Shared', 'Platform', 'DotNet', 'Npm', 'Desktop')
|
||||||
$dirs = [System.Collections.Generic.List[string]]::new()
|
$dirs = [System.Collections.Generic.List[string]]::new()
|
||||||
foreach ($name in $preferred) {
|
foreach ($name in $preferred) {
|
||||||
$path = Join-Path $PluginsRoot $name
|
$path = Join-Path $PluginsRoot $name
|
||||||
@ -293,131 +294,588 @@ function Test-PluginMutatesRemote {
|
|||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-SecretEnvironmentValue {
|
function Get-RepoUtilsEnvironmentVariable {
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Reads a secret value from an environment variable by logical name.
|
Reads a named environment variable from Process, then Windows User, then Machine.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Plugins never store secret material in scriptSettings.json. Settings hold a
|
Process wins (CICD sandbox inject, `$env:Name`, explicit session values). When the
|
||||||
logical name (e.g. "GitHub", "NuGet"); the process environment variable with
|
current process was started before a User-level pack was set, User/Machine still
|
||||||
that same name must be set before the engine runs.
|
apply so laptop releases do not require a new shell. An explicit process value —
|
||||||
|
including empty JSON `{}` — shadows User/Machine.
|
||||||
.PARAMETER Name
|
|
||||||
Logical secret name — also the environment variable name to read.
|
|
||||||
|
|
||||||
.OUTPUTS
|
|
||||||
System.String. The environment variable value, or $null when unset.
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
$token = Get-SecretEnvironmentValue -Name 'GitHub'
|
|
||||||
#>
|
#>
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$Name
|
[string]$Name
|
||||||
)
|
)
|
||||||
|
|
||||||
return [Environment]::GetEnvironmentVariable($Name)
|
if ([string]::IsNullOrWhiteSpace($Name)) {
|
||||||
|
throw "Environment variable name is required."
|
||||||
}
|
}
|
||||||
|
|
||||||
function Resolve-PluginSecretName {
|
$processValue = [Environment]::GetEnvironmentVariable($Name, 'Process')
|
||||||
<#
|
# Empty string is what SetEnvironmentVariable($null, Process) leaves behind;
|
||||||
.SYNOPSIS
|
# treat it as unset so Windows User packs still apply. Explicit '{}' shadows User.
|
||||||
Resolves a logical secret name from a plugin's scriptSettings entry.
|
if (-not [string]::IsNullOrWhiteSpace($processValue)) {
|
||||||
|
return $processValue
|
||||||
|
}
|
||||||
|
|
||||||
.DESCRIPTION
|
foreach ($target in @('User', 'Machine')) {
|
||||||
Reads a string property such as githubSecret / nugetSecret / npmSecret /
|
try {
|
||||||
containerRegistrySecret from the plugin settings object. Returns $null when
|
$value = [Environment]::GetEnvironmentVariable($Name, $target)
|
||||||
the property is missing or blank.
|
}
|
||||||
|
catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
.PARAMETER PluginSettings
|
|
||||||
Plugin settings object from scriptSettings.json (the enabled plugin entry).
|
|
||||||
|
|
||||||
.PARAMETER PropertyName
|
|
||||||
Settings property that holds the logical secret name (e.g. 'githubSecret').
|
|
||||||
|
|
||||||
.OUTPUTS
|
|
||||||
System.String. Trimmed logical secret name, or $null.
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
$name = Resolve-PluginSecretName -PluginSettings $plugin -PropertyName 'nugetSecret'
|
|
||||||
$key = Get-SecretEnvironmentValue -Name $name
|
|
||||||
#>
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
$PluginSettings,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$PropertyName
|
|
||||||
)
|
|
||||||
|
|
||||||
if ($PluginSettings.PSObject.Properties.Name -contains $PropertyName) {
|
|
||||||
$value = [string]$PluginSettings.$PropertyName
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||||
return $value.Trim()
|
return $value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-RegistryCredentialsFromRuntime {
|
function Get-RepoUtilsSecretsEnvNames {
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Loads container-registry username/password from a logical secret name.
|
Reads declared pack environment variable names from scriptSettings / engine context.
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Looks up the environment variable named by SecretName. The value must be
|
|
||||||
Base64(UTF8('username:password')). Used by registry login and image-pull
|
|
||||||
secret creation — never pass the password itself as a parameter.
|
|
||||||
|
|
||||||
.PARAMETER SecretName
|
|
||||||
Logical secret name (environment variable name), not a password or token.
|
|
||||||
|
|
||||||
.PARAMETER SharedSettings
|
|
||||||
Optional engine shared context (reserved for callers that thread context).
|
|
||||||
|
|
||||||
.OUTPUTS
|
|
||||||
Hashtable with User and Password keys (decoded credential material).
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
$creds = Get-RegistryCredentialsFromRuntime -SecretName 'ContainerRegistry'
|
|
||||||
# $creds.User / $creds.Password
|
|
||||||
#>
|
#>
|
||||||
# SecretName is a logical env-var name from scriptSettings, not a password value.
|
|
||||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
|
||||||
'PSAvoidUsingPlainTextForPassword',
|
|
||||||
'SecretName',
|
|
||||||
Justification = 'Logical secret name for env lookup (Base64 username:password); not a credential value.'
|
|
||||||
)]
|
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$SecretName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[psobject]$SharedSettings
|
$Settings
|
||||||
)
|
)
|
||||||
|
|
||||||
$raw = Get-SecretEnvironmentValue -Name $SecretName
|
$sharedName = $null
|
||||||
if ([string]::IsNullOrWhiteSpace($raw)) {
|
$packName = $null
|
||||||
throw "Environment variable '$SecretName' is not set."
|
if ($null -ne $Settings) {
|
||||||
|
if ($Settings.PSObject.Properties.Name -contains 'repoUtilsSecretsShared') {
|
||||||
|
$sharedName = [string]$Settings.repoUtilsSecretsShared
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Settings.PSObject.Properties.Name -contains 'repoUtilsSecrets') {
|
||||||
|
$packName = [string]$Settings.repoUtilsSecrets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedName = if ($null -eq $sharedName) { '' } else { $sharedName.Trim() }
|
||||||
|
$packName = if ($null -eq $packName) { '' } else { $packName.Trim() }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($sharedName) -or [string]::IsNullOrWhiteSpace($packName)) {
|
||||||
|
throw "scriptSettings.json must declare repoUtilsSecretsShared and repoUtilsSecrets (environment variable names, e.g. RepoUtilsSecretsShared / RepoUtilsSecrets)."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
SharedEnv = $sharedName
|
||||||
|
PackEnv = $packName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertFrom-RepoUtilsSecretsPackJson {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Parses a RepoUtilsSecrets JSON object. Empty input is {}. Bare non-JSON throws.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Raw,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SourceName
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Raw)) {
|
||||||
|
return [pscustomobject]@{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$trimmed = $Raw.Trim()
|
||||||
|
if (-not $trimmed.StartsWith('{')) {
|
||||||
|
throw "${SourceName} must be a JSON object (RepoUtilsSecrets pack), not a bare string."
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($raw))
|
$parsed = $trimmed | ConvertFrom-Json -ErrorAction Stop
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
throw "Failed to decode '$SecretName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
|
throw "${SourceName} is not valid JSON: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $parsed -or $parsed -is [System.Collections.IEnumerable]) {
|
||||||
|
throw "${SourceName} JSON must be an object."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-OrdinalPropertyMap {
|
||||||
|
param($Object)
|
||||||
|
|
||||||
|
$map = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal)
|
||||||
|
if ($null -eq $Object) {
|
||||||
|
return $map
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Object -is [System.Collections.IDictionary]) {
|
||||||
|
foreach ($key in @($Object.Keys)) {
|
||||||
|
$name = [string]$key
|
||||||
|
if ([string]::IsNullOrWhiteSpace($name)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$map[$name] = $Object[$key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($property in $Object.PSObject.Properties) {
|
||||||
|
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$map[[string]$property.Name] = $property.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertFrom-OrdinalPropertyMap {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[System.Collections.Generic.Dictionary[string, object]]$Map
|
||||||
|
)
|
||||||
|
|
||||||
|
$properties = [ordered]@{}
|
||||||
|
foreach ($key in $Map.Keys) {
|
||||||
|
$properties[$key] = $Map[$key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]$properties
|
||||||
|
}
|
||||||
|
|
||||||
|
function Merge-RepoUtilsSecretsPackObjects {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Appsettings-style merge: org-pack first, slug overlay. Nested merge for any object-valued slot (typically ContainerRegistry).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
$Base,
|
||||||
|
$Overlay
|
||||||
|
)
|
||||||
|
|
||||||
|
$result = ConvertTo-OrdinalPropertyMap -Object $Base
|
||||||
|
$overlayMap = ConvertTo-OrdinalPropertyMap -Object $Overlay
|
||||||
|
foreach ($key in @($overlayMap.Keys)) {
|
||||||
|
if (-not (Test-ContainerRegistryCatalogKey -Key $key)) {
|
||||||
|
throw "RepoUtilsSecrets pack key '$key' must be PascalCase (e.g. GitClone, ContainerRegistry)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$overlayValue = $overlayMap[$key]
|
||||||
|
$overlayIsObject = ($null -ne $overlayValue) -and -not ($overlayValue -is [string]) -and -not ($overlayValue -is [ValueType]) -and -not ($overlayValue -is [System.Collections.IEnumerable])
|
||||||
|
if ($overlayIsObject) {
|
||||||
|
$baseCatalog = $null
|
||||||
|
if ($result.ContainsKey($key)) {
|
||||||
|
$baseCatalog = $result[$key]
|
||||||
|
}
|
||||||
|
|
||||||
|
$mergedCatalog = ConvertTo-OrdinalPropertyMap -Object $baseCatalog
|
||||||
|
$overlayCatalogMap = ConvertTo-OrdinalPropertyMap -Object $overlayValue
|
||||||
|
foreach ($catalogKey in @($overlayCatalogMap.Keys)) {
|
||||||
|
if (-not (Test-ContainerRegistryCatalogKey -Key $catalogKey)) {
|
||||||
|
throw "Catalog key '$catalogKey' in pack slot '$key' must be PascalCase (e.g. Harbor, InCluster)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$mergedCatalog[$catalogKey] = $overlayCatalogMap[$catalogKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[$key] = ConvertFrom-OrdinalPropertyMap -Map $mergedCatalog
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$result[$key] = $overlayValue
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($key in @($result.Keys)) {
|
||||||
|
if (-not (Test-ContainerRegistryCatalogKey -Key $key)) {
|
||||||
|
throw "RepoUtilsSecrets pack key '$key' must be PascalCase (e.g. GitClone, ContainerRegistry)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertFrom-OrdinalPropertyMap -Map $result
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-MergedRepoUtilsSecretsPack {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Merges $env:RepoUtilsSecretsShared then $env:RepoUtilsSecrets (names from settings).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
$names = Get-RepoUtilsSecretsEnvNames -Settings $Settings
|
||||||
|
$sharedRaw = Get-RepoUtilsEnvironmentVariable -Name $names.SharedEnv
|
||||||
|
$packRaw = Get-RepoUtilsEnvironmentVariable -Name $names.PackEnv
|
||||||
|
$sharedObject = ConvertFrom-RepoUtilsSecretsPackJson -Raw $sharedRaw -SourceName $names.SharedEnv
|
||||||
|
$packObject = ConvertFrom-RepoUtilsSecretsPackJson -Raw $packRaw -SourceName $names.PackEnv
|
||||||
|
return Merge-RepoUtilsSecretsPackObjects -Base $sharedObject -Overlay $packObject
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepoUtilsSecretSlot {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Reads a scalar pack slot (GitClone, NuGet, Npm, CosignKey, …) after merge.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Name,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
$Settings,
|
||||||
|
|
||||||
|
[switch]$AllowMissing
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Name) -or -not (Test-ContainerRegistryCatalogKey -Key $Name)) {
|
||||||
|
throw "RepoUtilsSecrets slot '$Name' must be PascalCase (e.g. GitClone, NuGet)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$merged = Get-MergedRepoUtilsSecretsPack -Settings $Settings
|
||||||
|
$match = $null
|
||||||
|
foreach ($property in $merged.PSObject.Properties) {
|
||||||
|
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::Equals([string]$property.Name, $Name, [System.StringComparison]::Ordinal)) {
|
||||||
|
$match = $property
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $match) {
|
||||||
|
if ($AllowMissing) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "RepoUtilsSecrets slot '$Name' is missing after merging org-pack and slug-pack."
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $match.Value
|
||||||
|
if ($value -is [string] -or $null -eq $value -or $value -is [ValueType]) {
|
||||||
|
$text = if ($null -eq $value) { '' } else { [string]$value }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($text) -and -not $AllowMissing) {
|
||||||
|
throw "RepoUtilsSecrets slot '$Name' is empty."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($text)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $text
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "RepoUtilsSecrets slot '$Name' must be a string (nested maps are only allowed for ContainerRegistry)."
|
||||||
|
}
|
||||||
|
|
||||||
|
function Copy-RepoUtilsSecretsEnvNamesToContext {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Context,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Settings) {
|
||||||
|
return $Context
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($name in @('repoUtilsSecretsShared', 'repoUtilsSecrets', 'vaultRepoUtilsApplication')) {
|
||||||
|
if ($Settings.PSObject.Properties.Name -contains $name -and -not [string]::IsNullOrWhiteSpace([string]$Settings.$name)) {
|
||||||
|
$Context | Add-Member -NotePropertyName $name -NotePropertyValue ([string]$Settings.$name).Trim() -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $Context
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-ContainerRegistryCatalogKey {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
True when Key is PascalCase (Harbor, InCluster), matching env-slot names.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Key
|
||||||
|
)
|
||||||
|
|
||||||
|
return $Key -cmatch '^[A-Z][A-Za-z0-9]*$'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-PluginSecretName {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Reads a plugin setting that names a RepoUtilsSecrets pack subkey.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Settings such as githubSecret / nugetSecret / npmSecret / containerRegistrySecret /
|
||||||
|
cosignKeySecret hold the PascalCase pack slot to read (e.g. GitClone, NuGet).
|
||||||
|
They are not environment variable names.
|
||||||
|
|
||||||
|
.PARAMETER PluginSettings
|
||||||
|
Plugin settings object from scriptSettings.json.
|
||||||
|
|
||||||
|
.PARAMETER PropertyName
|
||||||
|
Settings property that holds the pack subkey name (e.g. 'githubSecret').
|
||||||
|
|
||||||
|
.PARAMETER PluginDisplayName
|
||||||
|
Plugin name used in required/validation errors.
|
||||||
|
|
||||||
|
.PARAMETER Required
|
||||||
|
Throw when the property is missing or blank.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$PluginSettings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PropertyName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$PluginDisplayName,
|
||||||
|
|
||||||
|
[switch]$Required
|
||||||
|
)
|
||||||
|
|
||||||
|
$display = if ([string]::IsNullOrWhiteSpace($PluginDisplayName)) { 'Plugin' } else { $PluginDisplayName }
|
||||||
|
$value = $null
|
||||||
|
if ($null -ne $PluginSettings -and $PluginSettings.PSObject.Properties.Name -contains $PropertyName) {
|
||||||
|
$raw = [string]$PluginSettings.$PropertyName
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($raw)) {
|
||||||
|
$value = $raw.Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
if ($Required) {
|
||||||
|
throw "$display requires '$PropertyName' (PascalCase pack subkey, e.g. GitClone, NuGet, ContainerRegistry)."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-ContainerRegistryCatalogKey -Key $value)) {
|
||||||
|
throw "$display '$PropertyName' '$value' must be PascalCase (e.g. GitClone, NuGet, ContainerRegistry)."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-RetiredContainerRegistrySettingsAbsent {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Throws when obsolete per-registry secret settings are present.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
$Object,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Context
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Object) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$retired = @(
|
||||||
|
'imagesCredentialsSecret',
|
||||||
|
'additionalImageRegistries',
|
||||||
|
'additionalImageRegistryUrls',
|
||||||
|
'additionalImagesCredentialsSecret',
|
||||||
|
'helmOciCredentialsSecret'
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($name in $retired) {
|
||||||
|
$present = $false
|
||||||
|
if ($Object -is [System.Collections.IDictionary]) {
|
||||||
|
$present = $Object.Contains($name)
|
||||||
|
}
|
||||||
|
elseif ($Object.PSObject.Properties.Name -contains $name) {
|
||||||
|
$present = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($present) {
|
||||||
|
throw "${Context}: '$name' is not supported. Use the ContainerRegistry JSON catalog with PascalCase containerRegistryKey / helmRegistryKey."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertFrom-RegistryCredentialPayload {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Payload,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ContextName
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Payload.Trim()))
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "Failed to decode '$ContextName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
|
||||||
}
|
}
|
||||||
|
|
||||||
$parts = $decoded -split ':', 2
|
$parts = $decoded -split ':', 2
|
||||||
if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) {
|
if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) {
|
||||||
throw "Decoded '$SecretName' must be in the form 'username:password'."
|
throw "Decoded '$ContextName' must be in the form 'username:password'."
|
||||||
}
|
}
|
||||||
|
|
||||||
return @{ User = $parts[0]; Password = $parts[1] }
|
return @{ User = $parts[0]; Password = $parts[1] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-ContainerRegistryCatalogObject {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Validates a PascalCase JSON map of Base64(username:password) values.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Catalog,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[string]$SourceName = 'ContainerRegistry'
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Catalog -is [string]) {
|
||||||
|
$raw = [string]$Catalog
|
||||||
|
$trimmed = $raw.Trim()
|
||||||
|
if (-not $trimmed.StartsWith('{')) {
|
||||||
|
throw "$SourceName must be a JSON catalog object { `"Harbor`": `"<Base64>`", `"InCluster`": `"<Base64>`" }."
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$Catalog = $trimmed | ConvertFrom-Json -ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
throw "$SourceName is not valid JSON: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $Catalog -or $Catalog -is [string] -or $Catalog -is [ValueType] -or $Catalog -is [System.Collections.IEnumerable]) {
|
||||||
|
throw "$SourceName JSON catalog must be an object of PascalCase keys to Base64(username:password)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$keyCount = 0
|
||||||
|
foreach ($property in $Catalog.PSObject.Properties) {
|
||||||
|
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$keyCount++
|
||||||
|
$keyName = [string]$property.Name
|
||||||
|
if (-not (Test-ContainerRegistryCatalogKey -Key $keyName)) {
|
||||||
|
throw "Catalog key '$keyName' in '$SourceName' must be PascalCase (e.g. Harbor, InCluster)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($keyCount -eq 0) {
|
||||||
|
throw "$SourceName JSON catalog has no PascalCase keys."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $Catalog
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RegistryCredentialsFromRuntime {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Loads container-registry username/password from the merged RepoUtilsSecrets pack.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Reads a nested catalog slot (named by -Slot, typically ContainerRegistry) after
|
||||||
|
org-pack + slug-pack merge. -Key (PascalCase, ordinal match) selects an entry.
|
||||||
|
|
||||||
|
.PARAMETER Key
|
||||||
|
PascalCase catalog key (e.g. Harbor). Required.
|
||||||
|
|
||||||
|
.PARAMETER Slot
|
||||||
|
PascalCase pack subkey that holds the catalog object (from containerRegistrySecret).
|
||||||
|
|
||||||
|
.PARAMETER SharedSettings
|
||||||
|
Engine shared context (must declare repoUtilsSecretsShared / repoUtilsSecrets).
|
||||||
|
|
||||||
|
.OUTPUTS
|
||||||
|
Hashtable with User and Password keys (decoded credential material).
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
# Omitted arguments must throw. Mandatory prompts on an interactive host and stalls tests and release runs.
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Key,
|
||||||
|
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]$Slot,
|
||||||
|
|
||||||
|
[psobject]$SharedSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Slot) -or -not (Test-ContainerRegistryCatalogKey -Key $Slot)) {
|
||||||
|
throw "containerRegistrySecret '$Slot' must be PascalCase (e.g. ContainerRegistry)."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Key)) {
|
||||||
|
throw "Pass -Key (PascalCase, e.g. Harbor) to select an entry in pack slot '$Slot'."
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-ContainerRegistryCatalogKey -Key $Key)) {
|
||||||
|
throw "containerRegistryKey '$Key' must be PascalCase (e.g. Harbor, InCluster)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$merged = Get-MergedRepoUtilsSecretsPack -Settings $SharedSettings
|
||||||
|
$catalogProperty = $null
|
||||||
|
foreach ($property in $merged.PSObject.Properties) {
|
||||||
|
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::Equals([string]$property.Name, $Slot, [System.StringComparison]::Ordinal)) {
|
||||||
|
$catalogProperty = $property
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $catalogProperty -or $null -eq $catalogProperty.Value) {
|
||||||
|
throw "RepoUtilsSecrets slot '$Slot' is missing after merging org-pack and slug-pack."
|
||||||
|
}
|
||||||
|
|
||||||
|
$catalog = Get-ContainerRegistryCatalogObject -Catalog $catalogProperty.Value -SourceName $Slot
|
||||||
|
|
||||||
|
$match = $null
|
||||||
|
foreach ($property in $catalog.PSObject.Properties) {
|
||||||
|
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::Equals([string]$property.Name, $Key, [System.StringComparison]::Ordinal)) {
|
||||||
|
$match = $property
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $match) {
|
||||||
|
throw "Catalog key '$Key' was not found in '$Slot' (ordinal PascalCase match)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [string]$match.Value
|
||||||
|
if ([string]::IsNullOrWhiteSpace($payload)) {
|
||||||
|
throw "Catalog key '$Key' in '$Slot' is empty."
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConvertFrom-RegistryCredentialPayload -Payload $payload -ContextName "$Slot.$Key"
|
||||||
|
}
|
||||||
|
|
||||||
function Resolve-EngineDirectoryFromSharedSettings {
|
function Resolve-EngineDirectoryFromSharedSettings {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -596,7 +1054,7 @@ function Resolve-PluginModulePath {
|
|||||||
$candidatePaths = [System.Collections.Generic.List[string]]::new()
|
$candidatePaths = [System.Collections.Generic.List[string]]::new()
|
||||||
$candidatePaths.Add((Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName))
|
$candidatePaths.Add((Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName))
|
||||||
|
|
||||||
$preferredGroups = @('Platform', 'DotNet', 'Npm')
|
$preferredGroups = @('Platform', 'DotNet', 'Npm', 'Desktop')
|
||||||
$candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $preferredGroups[0]) $pluginFileName))
|
$candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $preferredGroups[0]) $pluginFileName))
|
||||||
|
|
||||||
if (Get-Command Get-ExtensionPluginModulePaths -ErrorAction SilentlyContinue) {
|
if (Get-Command Get-ExtensionPluginModulePaths -ErrorAction SilentlyContinue) {
|
||||||
@ -756,4 +1214,4 @@ function Invoke-ConfiguredPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStageLabel, Get-PluginBranches, Get-PluginMetadataObject, Test-PluginCompatible, Test-PluginMutatesRemote, Resolve-PluginSecretName, Get-SecretEnvironmentValue, Get-RegistryCredentialsFromRuntime, Test-PluginSkipsRemoteMutation, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin
|
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStageLabel, Get-PluginBranches, Get-PluginMetadataObject, Test-PluginCompatible, Test-PluginMutatesRemote, Get-RepoUtilsEnvironmentVariable, Get-RepoUtilsSecretsEnvNames, ConvertFrom-RepoUtilsSecretsPackJson, Merge-RepoUtilsSecretsPackObjects, Get-MergedRepoUtilsSecretsPack, Get-RepoUtilsSecretSlot, Copy-RepoUtilsSecretsEnvNamesToContext, Resolve-PluginSecretName, Test-ContainerRegistryCatalogKey, Assert-RetiredContainerRegistrySettingsAbsent, Get-ContainerRegistryCatalogObject, Get-RegistryCredentialsFromRuntime, Test-PluginSkipsRemoteMutation, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin
|
||||||
|
|||||||
@ -213,6 +213,7 @@ function New-EngineContext {
|
|||||||
skipPublishPlugins = $false
|
skipPublishPlugins = $false
|
||||||
facts = [ordered]@{}
|
facts = [ordered]@{}
|
||||||
}
|
}
|
||||||
|
$context = Copy-RepoUtilsSecretsEnvNamesToContext -Context $context -Settings $Settings
|
||||||
|
|
||||||
$versionSource = Resolve-EngineContextVersion -Plugins $Plugins -Context $context -ScriptDir $ScriptDir
|
$versionSource = Resolve-EngineContextVersion -Plugins $Plugins -Context $context -ScriptDir $ScriptDir
|
||||||
$version = [string](Get-EngineState -Context $context -Name 'version' -Required)
|
$version = [string](Get-EngineState -Context $context -Name 'version' -Required)
|
||||||
@ -265,4 +266,61 @@ function Get-PreferredReleaseBranch {
|
|||||||
return "main"
|
return "main"
|
||||||
}
|
}
|
||||||
|
|
||||||
Export-ModuleMember -Function Assert-WorkingTreeClean, Initialize-ReleaseStageContext, New-EngineContext, Get-PreferredReleaseBranch
|
function Select-HelmInstallPlugins {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Keeps the release version plugin and HelmSelfDeploy for an OCI install without build or push.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Install-Helm-*.bat passes -InstallOnly. The chart and images are already in OCI, so compile,
|
||||||
|
test, image build, image push, and HelmPush are not run. The version plugin still supplies
|
||||||
|
the chart tag (shared.version). HelmSelfDeploy dryRun is rejected: that path needs the local
|
||||||
|
package HelmPush keeps in the same run.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[object[]]$Plugins,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ScriptDir
|
||||||
|
)
|
||||||
|
|
||||||
|
$versionPlugins = @(Get-EnabledVersionPlugins -Plugins $Plugins -ScriptDir $ScriptDir)
|
||||||
|
if ($versionPlugins.Count -eq 0) {
|
||||||
|
throw "Install-only Helm requires exactly one enabled release version plugin (providesVersion) so the OCI chart tag can be resolved. Build and push plugins are not run."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($versionPlugins.Count -gt 1) {
|
||||||
|
$names = ($versionPlugins | ForEach-Object { [string]$_.name }) -join ', '
|
||||||
|
throw "Install-only Helm requires exactly one enabled release version plugin. Found: $names."
|
||||||
|
}
|
||||||
|
|
||||||
|
$deployPlugins = @($Plugins | Where-Object {
|
||||||
|
$null -ne $_ -and
|
||||||
|
$_.enabled -and
|
||||||
|
[string]$_.name -eq 'HelmSelfDeploy'
|
||||||
|
})
|
||||||
|
|
||||||
|
if ($deployPlugins.Count -eq 0) {
|
||||||
|
throw "Install-only Helm requires an enabled HelmSelfDeploy plugin. The chart is installed from OCI; image build, image push, and HelmPush are not run."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($deployPlugins.Count -gt 1) {
|
||||||
|
throw "Install-only Helm found multiple enabled HelmSelfDeploy plugins."
|
||||||
|
}
|
||||||
|
|
||||||
|
$deployPlugin = $deployPlugins[0]
|
||||||
|
if ($deployPlugin.PSObject.Properties.Name -contains 'dryRun' -and $null -ne $deployPlugin.dryRun -and [bool]$deployPlugin.dryRun) {
|
||||||
|
throw "Install-only Helm cannot use HelmSelfDeploy dryRun. That path needs the local package from HelmPush in the same run. Omit dryRun to install the chart already published to OCI."
|
||||||
|
}
|
||||||
|
|
||||||
|
$versionName = [string]$versionPlugins[0].name
|
||||||
|
return @($Plugins | Where-Object {
|
||||||
|
$null -ne $_ -and
|
||||||
|
$_.enabled -and
|
||||||
|
([string]$_.name -eq $versionName -or [string]$_.name -eq 'HelmSelfDeploy')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Assert-WorkingTreeClean, Initialize-ReleaseStageContext, New-EngineContext, Get-PreferredReleaseBranch, Select-HelmInstallPlugins
|
||||||
|
|||||||
@ -38,6 +38,7 @@ function New-EngineContext {
|
|||||||
utilsDir = $SrcDir
|
utilsDir = $SrcDir
|
||||||
facts = [ordered]@{}
|
facts = [ordered]@{}
|
||||||
}
|
}
|
||||||
|
$context = Copy-RepoUtilsSecretsEnvNamesToContext -Context $context -Settings $Settings
|
||||||
|
|
||||||
$expandContext = Get-Command Expand-ExtensionEngineContext -ErrorAction SilentlyContinue
|
$expandContext = Get-Command Expand-ExtensionEngineContext -ErrorAction SilentlyContinue
|
||||||
if ($expandContext) {
|
if ($expandContext) {
|
||||||
|
|||||||
373
utils/modules/Engine/VaultSupport.psm1
Normal file
373
utils/modules/Engine/VaultSupport.psm1
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
function Test-RepoUtilsUseVaultEnabled {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($null -eq $Settings -or -not ($Settings.PSObject.Properties.Name -contains 'useVault')) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $Settings.useVault
|
||||||
|
if ($value -is [bool]) {
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]$value -eq 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertFrom-VaultConnectionSecret {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Raw
|
||||||
|
)
|
||||||
|
|
||||||
|
$trimmed = $Raw.Trim()
|
||||||
|
$separator = $trimmed.IndexOf('|')
|
||||||
|
if ($separator -lt 1 -or $separator -ge ($trimmed.Length - 1)) {
|
||||||
|
throw "Vault connection must be 'baseAddress|apiKey' (pipe separator). Example: http://172.16.0.14|your-key"
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseAddress = $trimmed.Substring(0, $separator).Trim().TrimEnd('/')
|
||||||
|
$apiKey = $trimmed.Substring($separator + 1).Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($baseAddress) -or [string]::IsNullOrWhiteSpace($apiKey)) {
|
||||||
|
throw "Vault connection is missing base address or API key."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
BaseAddress = $baseAddress
|
||||||
|
ApiKey = $apiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepoUtilsVaultScope {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
$organization = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_ORGANIZATION')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($organization) -and ($Settings.PSObject.Properties.Name -contains 'vaultOrganization')) {
|
||||||
|
$organization = [string]$Settings.vaultOrganization
|
||||||
|
}
|
||||||
|
|
||||||
|
$application = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_APPLICATION')
|
||||||
|
if ([string]::IsNullOrWhiteSpace($application) -and ($Settings.PSObject.Properties.Name -contains 'vaultApplication')) {
|
||||||
|
$application = [string]$Settings.vaultApplication
|
||||||
|
}
|
||||||
|
|
||||||
|
$organization = if ($null -eq $organization) { '' } else { $organization.Trim() }
|
||||||
|
$application = if ($null -eq $application) { '' } else { $application.Trim() }
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($organization) -or [string]::IsNullOrWhiteSpace($application)) {
|
||||||
|
throw "useVault is true but vault organization/application are not set. Set vaultOrganization/vaultApplication in scriptSettings.json or MAKSIT_VAULT_ORGANIZATION / MAKSIT_VAULT_APPLICATION."
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Organization = $organization
|
||||||
|
Application = $application
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepoUtilsVaultConnectionSecretName {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
if (($Settings.PSObject.Properties.Name -contains 'vaultConnectionSecret') -and
|
||||||
|
-not [string]::IsNullOrWhiteSpace([string]$Settings.vaultConnectionSecret)) {
|
||||||
|
return ([string]$Settings.vaultConnectionSecret).Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'MAKSIT_VAULT'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepoUtilsVaultOrgPackApplicationName {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
$application = $null
|
||||||
|
if ($Settings.PSObject.Properties.Name -contains 'vaultRepoUtilsApplication') {
|
||||||
|
$application = [string]$Settings.vaultRepoUtilsApplication
|
||||||
|
}
|
||||||
|
|
||||||
|
$application = if ($null -eq $application) { '' } else { $application.Trim() }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($application)) {
|
||||||
|
throw "useVault is true but vaultRepoUtilsApplication is not set in scriptSettings.json (Vault application for the org-pack, e.g. Shared)."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $application
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-VaultNameFilterExpression {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
$escaped = $Name.Replace('\', '\\').Replace('"', '\"')
|
||||||
|
return "Name == `"$escaped`""
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-RepoUtilsVaultClientModule {
|
||||||
|
$modulePath = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_MODULE_PATH')
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($modulePath)) {
|
||||||
|
Import-Module $modulePath -Force -ErrorAction Stop
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Get-Command Connect-Vault -ErrorAction SilentlyContinue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-Module 'MaksIT.Vault.Client.PowerShell' -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-RepoUtilsVaultSecretMatch {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$OrganizationName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ApplicationName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[pscustomobject]$Connection,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet('Cmdlet', 'Rest')]
|
||||||
|
[string]$Mode
|
||||||
|
)
|
||||||
|
|
||||||
|
$orgFilter = Get-VaultNameFilterExpression -Name $OrganizationName
|
||||||
|
$appFilter = Get-VaultNameFilterExpression -Name $ApplicationName
|
||||||
|
$pageNumber = 1
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$items = @()
|
||||||
|
$hasNext = $false
|
||||||
|
|
||||||
|
if ($Mode -eq 'Cmdlet') {
|
||||||
|
$response = Search-VaultSecrets -PageNumber $pageNumber -PageSize 100 `
|
||||||
|
-OrganizationFilters $orgFilter -ApplicationFilters $appFilter
|
||||||
|
if ($null -ne $response -and $null -ne $response.Items) {
|
||||||
|
$items = @($response.Items)
|
||||||
|
}
|
||||||
|
if ($null -ne $response) {
|
||||||
|
$hasNext = [bool]$response.HasNextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$body = @{
|
||||||
|
pageNumber = $pageNumber
|
||||||
|
pageSize = 100
|
||||||
|
organizationFilters = $orgFilter
|
||||||
|
applicationFilters = $appFilter
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
$uri = "$($Connection.BaseAddress)/api/vault/secrets"
|
||||||
|
$headers = @{ 'X-API-KEY' = $Connection.ApiKey }
|
||||||
|
$response = Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -ContentType 'application/json' -Body $body
|
||||||
|
if ($null -ne $response.items) {
|
||||||
|
$items = @($response.items)
|
||||||
|
}
|
||||||
|
elseif ($null -ne $response.Items) {
|
||||||
|
$items = @($response.Items)
|
||||||
|
}
|
||||||
|
$hasNext = [bool]($response.hasNextPage)
|
||||||
|
if (-not $hasNext) {
|
||||||
|
$hasNext = [bool]($response.HasNextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($item in $items) {
|
||||||
|
$itemName = [string]$(if ($item.PSObject.Properties.Name -contains 'Name') { $item.Name } else { $item.name })
|
||||||
|
if ([string]::Equals($itemName, $SecretName, [System.StringComparison]::Ordinal)) {
|
||||||
|
return $item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $hasNext) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageNumber++
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RepoUtilsVaultSecretValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Match,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[pscustomobject]$Connection,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateSet('Cmdlet', 'Rest')]
|
||||||
|
[string]$Mode
|
||||||
|
)
|
||||||
|
|
||||||
|
$organizationId = if ($Match.PSObject.Properties.Name -contains 'OrganizationId') { $Match.OrganizationId } else { $Match.organizationId }
|
||||||
|
$applicationId = if ($Match.PSObject.Properties.Name -contains 'ApplicationId') { $Match.ApplicationId } else { $Match.applicationId }
|
||||||
|
$secretId = if ($Match.PSObject.Properties.Name -contains 'Id') { $Match.Id } else { $Match.id }
|
||||||
|
|
||||||
|
if ($Mode -eq 'Cmdlet') {
|
||||||
|
$version = Get-VaultSecret -OrganizationId $organizationId -ApplicationId $applicationId -SecretId $secretId -SecretVersion 'current'
|
||||||
|
if ($null -eq $version -or [string]::IsNullOrWhiteSpace([string]$version.Value)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
return [string]$version.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = "$($Connection.BaseAddress)/api/vault/organization/$organizationId/application/$applicationId/secret/$secretId" +
|
||||||
|
'?secretVersion=current'
|
||||||
|
$headers = @{ 'X-API-KEY' = $Connection.ApiKey }
|
||||||
|
$version = Invoke-RestMethod -Method Get -Uri $uri -Headers $headers
|
||||||
|
$value = if ($version.PSObject.Properties.Name -contains 'Value') { $version.Value } else { $version.value }
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$value)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
return [string]$value
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-RepoUtilsVaultSecretValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$OrganizationName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ApplicationName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[pscustomobject]$Connection,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Mode
|
||||||
|
)
|
||||||
|
|
||||||
|
$match = Find-RepoUtilsVaultSecretMatch `
|
||||||
|
-OrganizationName $OrganizationName `
|
||||||
|
-ApplicationName $ApplicationName `
|
||||||
|
-SecretName $SecretName `
|
||||||
|
-Connection $Connection `
|
||||||
|
-Mode $Mode
|
||||||
|
if ($null -eq $match) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = Get-RepoUtilsVaultSecretValue -Match $match -Connection $Connection -Mode $Mode
|
||||||
|
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level 'INFO' -Message "Resolved Vault secret '$SecretName' from $OrganizationName/$ApplicationName"
|
||||||
|
return $value
|
||||||
|
}
|
||||||
|
|
||||||
|
function Initialize-RepoUtilsVaultSecrets {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Plugins
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-RepoUtilsUseVaultEnabled -Settings $Settings)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$names = Get-RepoUtilsSecretsEnvNames -Settings $Settings
|
||||||
|
$sharedRaw = Get-RepoUtilsEnvironmentVariable -Name $names.SharedEnv
|
||||||
|
$packRaw = Get-RepoUtilsEnvironmentVariable -Name $names.PackEnv
|
||||||
|
$sharedPresent = -not [string]::IsNullOrWhiteSpace($sharedRaw)
|
||||||
|
$packPresent = -not [string]::IsNullOrWhiteSpace($packRaw)
|
||||||
|
if ($sharedPresent -and $packPresent) {
|
||||||
|
[void](ConvertFrom-RepoUtilsSecretsPackJson -Raw $sharedRaw -SourceName $names.SharedEnv)
|
||||||
|
[void](ConvertFrom-RepoUtilsSecretsPackJson -Raw $packRaw -SourceName $names.PackEnv)
|
||||||
|
Write-Log -Level 'INFO' -Message "Vault mode: pack env vars '$($names.SharedEnv)' and '$($names.PackEnv)' already set; skipping Vault fetch."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$connectionName = Get-RepoUtilsVaultConnectionSecretName -Settings $Settings
|
||||||
|
$raw = [Environment]::GetEnvironmentVariable($connectionName)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||||
|
throw "useVault is true but environment variable '$connectionName' is not set (expected baseAddress|apiKey)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection = ConvertFrom-VaultConnectionSecret -Raw $raw
|
||||||
|
$scope = Get-RepoUtilsVaultScope -Settings $Settings
|
||||||
|
$orgPackApplication = Get-RepoUtilsVaultOrgPackApplicationName -Settings $Settings
|
||||||
|
|
||||||
|
Write-Log -Level 'INFO' -Message "Vault mode: loading RepoUtilsSecrets packs for $($scope.Organization)/$orgPackApplication and $($scope.Organization)/$($scope.Application)"
|
||||||
|
|
||||||
|
$mode = 'Rest'
|
||||||
|
try {
|
||||||
|
Import-RepoUtilsVaultClientModule
|
||||||
|
if (Get-Command Connect-Vault -ErrorAction SilentlyContinue) {
|
||||||
|
Connect-Vault -BaseAddress $connection.BaseAddress -ApiKey $connection.ApiKey
|
||||||
|
$mode = 'Cmdlet'
|
||||||
|
Write-Log -Level 'INFO' -Message 'Connected to Vault with MaksIT.Vault.Client.PowerShell'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level 'INFO' -Message 'MaksIT.Vault.Client.PowerShell not found; using Vault HTTP API'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Log -Level 'INFO' -Message "Vault PowerShell module not loaded ($($_.Exception.Message)); using Vault HTTP API"
|
||||||
|
$mode = 'Rest'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $sharedPresent) {
|
||||||
|
$sharedValue = Resolve-RepoUtilsVaultSecretValue `
|
||||||
|
-OrganizationName $scope.Organization `
|
||||||
|
-ApplicationName $orgPackApplication `
|
||||||
|
-SecretName $names.SharedEnv `
|
||||||
|
-Connection $connection `
|
||||||
|
-Mode $mode
|
||||||
|
if ([string]::IsNullOrWhiteSpace($sharedValue)) {
|
||||||
|
$sharedValue = '{}'
|
||||||
|
Write-Log -Level 'INFO' -Message "Vault secret '$($names.SharedEnv)' was not found for $($scope.Organization)/$orgPackApplication; using empty org-pack."
|
||||||
|
}
|
||||||
|
|
||||||
|
[Environment]::SetEnvironmentVariable($names.SharedEnv, $sharedValue, 'Process')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $packPresent) {
|
||||||
|
$packValue = Resolve-RepoUtilsVaultSecretValue `
|
||||||
|
-OrganizationName $scope.Organization `
|
||||||
|
-ApplicationName $scope.Application `
|
||||||
|
-SecretName $names.PackEnv `
|
||||||
|
-Connection $connection `
|
||||||
|
-Mode $mode
|
||||||
|
if ([string]::IsNullOrWhiteSpace($packValue)) {
|
||||||
|
$packValue = '{}'
|
||||||
|
Write-Log -Level 'INFO' -Message "Vault secret '$($names.PackEnv)' was not found for $($scope.Organization)/$($scope.Application); using empty slug-pack."
|
||||||
|
}
|
||||||
|
|
||||||
|
[Environment]::SetEnvironmentVariable($names.PackEnv, $packValue, 'Process')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function @(
|
||||||
|
'Test-RepoUtilsUseVaultEnabled',
|
||||||
|
'ConvertFrom-VaultConnectionSecret',
|
||||||
|
'Get-RepoUtilsVaultScope',
|
||||||
|
'Get-RepoUtilsVaultConnectionSecretName',
|
||||||
|
'Get-RepoUtilsVaultOrgPackApplicationName',
|
||||||
|
'Initialize-RepoUtilsVaultSecrets'
|
||||||
|
)
|
||||||
@ -27,6 +27,10 @@ function Clear-ExternalCommandTestHandler {
|
|||||||
$script:ExternalCommandTestHandler = $null
|
$script:ExternalCommandTestHandler = $null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test-ExternalCommandTestHandler {
|
||||||
|
return ($null -ne $script:ExternalCommandTestHandler)
|
||||||
|
}
|
||||||
|
|
||||||
function Set-ExternalCommandAvailability {
|
function Set-ExternalCommandAvailability {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -107,7 +111,7 @@ function Invoke-ExternalCommand {
|
|||||||
if ($ThrowOnError -and $exitCode -ne 0) {
|
if ($ThrowOnError -and $exitCode -ne 0) {
|
||||||
$preview = ($output | ForEach-Object {
|
$preview = ($output | ForEach-Object {
|
||||||
if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { [string]$_ }
|
if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { [string]$_ }
|
||||||
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 8) -join ' '
|
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Last 12) -join ' '
|
||||||
if ([string]::IsNullOrWhiteSpace($preview)) {
|
if ([string]::IsNullOrWhiteSpace($preview)) {
|
||||||
throw "External command '$Name' failed with exit code $exitCode."
|
throw "External command '$Name' failed with exit code $exitCode."
|
||||||
}
|
}
|
||||||
@ -128,4 +132,5 @@ Export-ModuleMember -Function `
|
|||||||
Invoke-ExternalCommand, `
|
Invoke-ExternalCommand, `
|
||||||
Set-ExternalCommandTestHandler, `
|
Set-ExternalCommandTestHandler, `
|
||||||
Clear-ExternalCommandTestHandler, `
|
Clear-ExternalCommandTestHandler, `
|
||||||
|
Test-ExternalCommandTestHandler, `
|
||||||
Set-ExternalCommandAvailability
|
Set-ExternalCommandAvailability
|
||||||
|
|||||||
@ -1,122 +0,0 @@
|
|||||||
#requires -Version 7.0
|
|
||||||
#requires -PSEdition Core
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
.NET artifact cleanup plugin — remove NuGet build outputs after release.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Removes files from the configured artifacts directory using glob patterns.
|
|
||||||
Defaults target NuGet outputs (*.nupkg, *.snupkg). Typically placed at the
|
|
||||||
end of the Release stage after DotNetCreateArchive or publish plugins.
|
|
||||||
#>
|
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
|
||||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
||||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
|
||||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
|
||||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-CleanupPatternsInternal {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
$ConfiguredPatterns
|
|
||||||
)
|
|
||||||
|
|
||||||
if ($null -eq $ConfiguredPatterns) {
|
|
||||||
return @('*.nupkg', '*.snupkg')
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
|
|
||||||
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
|
|
||||||
return @('*.nupkg', '*.snupkg')
|
|
||||||
}
|
|
||||||
|
|
||||||
return @([string]$ConfiguredPatterns)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-ExcludePatternsInternal {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $false)]
|
|
||||||
$ConfiguredPatterns
|
|
||||||
)
|
|
||||||
|
|
||||||
if ($null -eq $ConfiguredPatterns) {
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
|
|
||||||
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
return @([string]$ConfiguredPatterns)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Plugin {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
$Settings
|
|
||||||
)
|
|
||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
|
||||||
$sharedSettings = $Settings.context
|
|
||||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
|
||||||
$patterns = Get-CleanupPatternsInternal -ConfiguredPatterns $pluginSettings.includePatterns
|
|
||||||
$excludePatterns = Get-ExcludePatternsInternal -ConfiguredPatterns $pluginSettings.excludePatterns
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
|
||||||
throw "DotNetCleanupArtifacts plugin requires an artifacts directory in the shared context."
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
|
||||||
Write-Log -Level "WARN" -Message " Artifacts directory not found: $artifactsDirectory"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Cleaning generated artifacts..."
|
|
||||||
|
|
||||||
$itemsToRemove = @()
|
|
||||||
foreach ($pattern in $patterns) {
|
|
||||||
$matchedItems = @(
|
|
||||||
Get-ChildItem -Path $artifactsDirectory -Force -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object { $_.Name -like $pattern }
|
|
||||||
)
|
|
||||||
|
|
||||||
if ($excludePatterns.Count -gt 0) {
|
|
||||||
$matchedItems = @(
|
|
||||||
$matchedItems |
|
|
||||||
Where-Object {
|
|
||||||
$item = $_
|
|
||||||
-not ($excludePatterns | Where-Object { $item.Name -like $_ } | Select-Object -First 1)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
$itemsToRemove += @($matchedItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
$itemsToRemove = @($itemsToRemove | Sort-Object FullName -Unique)
|
|
||||||
|
|
||||||
if ($itemsToRemove.Count -eq 0) {
|
|
||||||
Write-Log -Level "INFO" -Message " No artifacts matched cleanup rules."
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($item in $itemsToRemove) {
|
|
||||||
Remove-Item -Path $item.FullName -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
Write-Log -Level "OK" -Message " Removed: $($item.Name)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Export-ModuleMember -Function Invoke-Plugin
|
|
||||||
@ -9,6 +9,10 @@
|
|||||||
This plugin compresses .NET release artifact inputs prepared by an earlier
|
This plugin compresses .NET release artifact inputs prepared by an earlier
|
||||||
DotNet plugin (DotNetPack or DotNetPublish) into a zip file
|
DotNet plugin (DotNetPack or DotNetPublish) into a zip file
|
||||||
and exposes the resulting release assets for later publisher plugins.
|
and exposes the resulting release assets for later publisher plugins.
|
||||||
|
|
||||||
|
For desktop apps with per-RID publish folders, the zip is the portable
|
||||||
|
build only (default win-x64). Windows installer and Flatpak artifacts are
|
||||||
|
separate GitHub assets and are never added to this zip.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
@ -26,7 +30,7 @@ function Invoke-Plugin {
|
|||||||
)
|
)
|
||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
|
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-ReleaseAssetPaths"
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$sharedSettings = $Settings.context
|
$sharedSettings = $Settings.context
|
||||||
@ -49,6 +53,34 @@ function Invoke-Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$portableRid = $null
|
||||||
|
if ($pluginSettings.PSObject.Properties['portableRuntimeIdentifier'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.portableRuntimeIdentifier)) {
|
||||||
|
$portableRid = ([string]$pluginSettings.portableRuntimeIdentifier).Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
$publishOutputs = @(Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'publishOutputs' -Default @())
|
||||||
|
if ($publishOutputs.Count -gt 0) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($portableRid)) {
|
||||||
|
$portableRid = 'win-x64'
|
||||||
|
}
|
||||||
|
|
||||||
|
$portableOutputs = @(
|
||||||
|
$publishOutputs |
|
||||||
|
Where-Object { [string]$_.runtimeIdentifier -eq $portableRid }
|
||||||
|
)
|
||||||
|
if ($portableOutputs.Count -eq 0) {
|
||||||
|
$portableOutputs = @(
|
||||||
|
$publishOutputs |
|
||||||
|
Where-Object { ([string]$_.runtimeIdentifier).StartsWith('win-', [System.StringComparison]::OrdinalIgnoreCase) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($portableOutputs.Count -gt 0) {
|
||||||
|
$archiveInputs = @($portableOutputs | ForEach-Object { [string]$_.directory })
|
||||||
|
Write-Log -Level "INFO" -Message " Portable zip uses runtime '$portableRid' only (installer/Flatpak stay out of the zip)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($archiveInputs.Count -eq 0) {
|
if ($archiveInputs.Count -eq 0) {
|
||||||
throw "DotNetCreateArchive plugin requires prepared artifacts. Run DotNetPack or DotNetPublish first."
|
throw "DotNetCreateArchive plugin requires prepared artifacts. Run DotNetPack or DotNetPublish first."
|
||||||
}
|
}
|
||||||
@ -84,19 +116,20 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
Write-Log -Level "OK" -Message " Release archive ready: $zipPath"
|
Write-Log -Level "OK" -Message " Release archive ready: $zipPath"
|
||||||
|
|
||||||
$releaseAssetPaths = @($zipPath)
|
$newAssets = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$newAssets.Add($zipPath)
|
||||||
$packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile')
|
$packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile')
|
||||||
if ($null -ne $packageFile) {
|
if ($null -ne $packageFile) {
|
||||||
$releaseAssetPaths += $packageFile.FullName
|
$newAssets.Add($packageFile.FullName)
|
||||||
}
|
}
|
||||||
$symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile')
|
$symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile')
|
||||||
if ($null -ne $symbolsPackageFile) {
|
if ($null -ne $symbolsPackageFile) {
|
||||||
$releaseAssetPaths += $symbolsPackageFile.FullName
|
$newAssets.Add($symbolsPackageFile.FullName)
|
||||||
}
|
}
|
||||||
|
|
||||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archivePath' -Value $zipPath -Overwrite Replace -LegacyProperty 'releaseArchivePath'
|
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archivePath' -Value $zipPath -Overwrite Replace -LegacyProperty 'releaseArchivePath'
|
||||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $releaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
Add-ReleaseAssetPaths -Context $sharedSettings -Path @($newAssets)
|
||||||
}
|
}
|
||||||
|
|
||||||
Export-ModuleMember -Function Invoke-Plugin
|
Export-ModuleMember -Function Invoke-Plugin
|
||||||
|
|||||||
@ -29,10 +29,10 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$sharedSettings = $Settings.context
|
$sharedSettings = $Settings.context
|
||||||
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret'
|
|
||||||
$packageFile = $sharedSettings.packageFile
|
$packageFile = $sharedSettings.packageFile
|
||||||
|
|
||||||
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
||||||
|
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret' -PluginDisplayName 'DotNetNuGet' -Required
|
||||||
|
|
||||||
Assert-Command dotnet
|
Assert-Command dotnet
|
||||||
|
|
||||||
@ -52,13 +52,9 @@ function Invoke-Plugin {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($nugetSecret)) {
|
$nugetKey = Get-RepoUtilsSecretSlot -Name $nugetSecret -Settings $sharedSettings
|
||||||
throw "DotNetNuGet plugin requires 'nugetSecret' in scriptSettings.json (logical secret name, e.g. NuGet)."
|
|
||||||
}
|
|
||||||
|
|
||||||
$nugetKey = Get-SecretEnvironmentValue -Name $nugetSecret
|
|
||||||
if ([string]::IsNullOrWhiteSpace($nugetKey)) {
|
if ([string]::IsNullOrWhiteSpace($nugetKey)) {
|
||||||
throw "NuGet API key is not set. Set environment variable '$nugetSecret'."
|
throw "NuGet API key is not set. Set RepoUtilsSecrets slot '$nugetSecret' (nugetSecret)."
|
||||||
}
|
}
|
||||||
|
|
||||||
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
|
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
|
||||||
|
|||||||
@ -6,9 +6,14 @@
|
|||||||
.NET publish plugin for producing application release artifacts.
|
.NET publish plugin for producing application release artifacts.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
This plugin publishes the configured .NET project into a release output
|
This plugin publishes configured .NET projects into the artifacts directory
|
||||||
directory and exposes that published directory to the shared release
|
and appends those publish folders to shared archive inputs so later plugins
|
||||||
context so later release-stage plugins can archive and publish it.
|
can zip them next to any earlier pack outputs. Existing NuGet package facts
|
||||||
|
(packageFile) are left unchanged.
|
||||||
|
|
||||||
|
Optional runtimeIdentifiers (or runtimeIdentifier) produce per-RID folders
|
||||||
|
under artifactsDir/<ProjectName>/<rid> for desktop installers (Windows MSI /
|
||||||
|
Linux Flatpak). When RIDs are set, selfContained defaults to true.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
@ -19,6 +24,28 @@ if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-DotNetPublishRuntimeIdentifiers {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$PluginSettings
|
||||||
|
)
|
||||||
|
|
||||||
|
$rids = [System.Collections.Generic.List[string]]::new()
|
||||||
|
if ($PluginSettings.PSObject.Properties['runtimeIdentifiers'] -and $null -ne $PluginSettings.runtimeIdentifiers) {
|
||||||
|
foreach ($item in @($PluginSettings.runtimeIdentifiers)) {
|
||||||
|
$value = [string]$item
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||||
|
$rids.Add($value.Trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($PluginSettings.PSObject.Properties['runtimeIdentifier'] -and -not [string]::IsNullOrWhiteSpace([string]$PluginSettings.runtimeIdentifier)) {
|
||||||
|
$rids.Add(([string]$PluginSettings.runtimeIdentifier).Trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
return @($rids)
|
||||||
|
}
|
||||||
|
|
||||||
function Invoke-Plugin {
|
function Invoke-Plugin {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -28,48 +55,145 @@ 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 "Set-EngineFact"
|
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
|
||||||
|
Import-PluginDependency -ModuleName "ExternalCommandSupport" -RequiredCommand "Invoke-ExternalCommand"
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
$sharedSettings = $Settings.context
|
$sharedSettings = $Settings.context
|
||||||
$projectFiles = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles')
|
$scriptDir = $sharedSettings.scriptDir
|
||||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
$projectFiles = @()
|
||||||
$publishProjectPath = $null
|
|
||||||
|
|
||||||
Assert-Command dotnet
|
Assert-Command dotnet
|
||||||
|
|
||||||
if ($null -eq $projectFiles -or @($projectFiles).Count -eq 0) {
|
if ($pluginSettings.PSObject.Properties['projectFiles'] -and $null -ne $pluginSettings.projectFiles) {
|
||||||
throw "DotNetPublish plugin requires project files in the shared context."
|
$projectFiles = @(Resolve-RelativePaths -Value $pluginSettings.projectFiles -BasePath $scriptDir)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$fromFact = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles')
|
||||||
|
if ($null -ne $fromFact) {
|
||||||
|
$projectFiles = @($fromFact)
|
||||||
|
}
|
||||||
|
elseif ($sharedSettings.PSObject.Properties['projectFiles'] -and $null -ne $sharedSettings.projectFiles) {
|
||||||
|
$projectFiles = @($sharedSettings.projectFiles)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$projectFiles = @($projectFiles)
|
if ($projectFiles.Count -eq 0) {
|
||||||
|
throw "DotNetPublish plugin requires projectFiles in plugin settings or projectFiles on shared context."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pluginSettings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.artifactsDir)) {
|
||||||
|
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$pluginSettings.artifactsDir)))
|
||||||
|
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
|
||||||
|
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$artifactsDirectory)) {
|
||||||
|
throw "DotNetPublish plugin requires artifactsDir in plugin settings or artifactsDirectory on shared context."
|
||||||
|
}
|
||||||
|
|
||||||
if (!(Test-Path $artifactsDirectory)) {
|
if (!(Test-Path $artifactsDirectory)) {
|
||||||
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
# The first configured project remains the canonical release artifact source.
|
$existing = Get-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -LegacyProperty @('releaseArchiveInputs')
|
||||||
$publishProjectPath = $projectFiles[0]
|
$archiveInputs = [System.Collections.Generic.List[object]]::new()
|
||||||
$publishDir = Join-Path $artifactsDirectory ([System.IO.Path]::GetFileNameWithoutExtension($publishProjectPath))
|
if ($null -ne $existing) {
|
||||||
|
foreach ($item in @($existing)) {
|
||||||
|
if ($null -ne $item) {
|
||||||
|
$archiveInputs.Add($item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$runtimeIdentifiers = @(Get-DotNetPublishRuntimeIdentifiers -PluginSettings $pluginSettings)
|
||||||
|
$selfContained = $null
|
||||||
|
if ($pluginSettings.PSObject.Properties['selfContained'] -and $null -ne $pluginSettings.selfContained) {
|
||||||
|
$selfContained = [bool]$pluginSettings.selfContained
|
||||||
|
}
|
||||||
|
elseif ($runtimeIdentifiers.Count -gt 0) {
|
||||||
|
$selfContained = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$publishSingleFile = $false
|
||||||
|
if ($pluginSettings.PSObject.Properties['publishSingleFile'] -and $null -ne $pluginSettings.publishSingleFile) {
|
||||||
|
$publishSingleFile = [bool]$pluginSettings.publishSingleFile
|
||||||
|
}
|
||||||
|
|
||||||
|
$additionalArguments = @()
|
||||||
|
if ($pluginSettings.PSObject.Properties['additionalPublishArguments'] -and $null -ne $pluginSettings.additionalPublishArguments) {
|
||||||
|
$additionalArguments = @($pluginSettings.additionalPublishArguments | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||||
|
}
|
||||||
|
|
||||||
|
$publishOutputs = [System.Collections.Generic.List[object]]::new()
|
||||||
|
$ridLoop = @($null)
|
||||||
|
if ($runtimeIdentifiers.Count -gt 0) {
|
||||||
|
$ridLoop = @($runtimeIdentifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($publishProjectPath in $projectFiles) {
|
||||||
|
$projectName = [System.IO.Path]::GetFileNameWithoutExtension($publishProjectPath)
|
||||||
|
$projectRoot = Join-Path $artifactsDirectory $projectName
|
||||||
|
|
||||||
|
foreach ($runtimeIdentifier in $ridLoop) {
|
||||||
|
$publishDir = if ([string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) {
|
||||||
|
$projectRoot
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Join-Path $projectRoot ([string]$runtimeIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
if (Test-Path $publishDir) {
|
if (Test-Path $publishDir) {
|
||||||
Remove-Item -Path $publishDir -Recurse -Force
|
Remove-Item -Path $publishDir -Recurse -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Publishing release artifact..."
|
$ridLabel = if ([string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) { 'current runtime' } else { [string]$runtimeIdentifier }
|
||||||
dotnet publish $publishProjectPath -c Release -o $publishDir --nologo
|
Write-Log -Level "STEP" -Message "Publishing release artifact ($projectName / $ridLabel)..."
|
||||||
if ($LASTEXITCODE -ne 0) {
|
$dotnetPublishArguments = @(
|
||||||
throw "dotnet publish failed for $publishProjectPath."
|
'publish', $publishProjectPath, '-c', 'Release', '-o', $publishDir, '--nologo'
|
||||||
|
)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) {
|
||||||
|
$dotnetPublishArguments += @('-r', [string]$runtimeIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($null -ne $selfContained) {
|
||||||
|
if ($selfContained) {
|
||||||
|
$dotnetPublishArguments += '--self-contained'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$dotnetPublishArguments += '--no-self-contained'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($publishSingleFile) {
|
||||||
|
$dotnetPublishArguments += '-p:PublishSingleFile=true'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($additionalArguments.Count -gt 0) {
|
||||||
|
$dotnetPublishArguments += $additionalArguments
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetPublishArguments | Out-Null
|
||||||
|
|
||||||
$publishedItems = @(Get-ChildItem -Path $publishDir -Force -ErrorAction SilentlyContinue)
|
$publishedItems = @(Get-ChildItem -Path $publishDir -Force -ErrorAction SilentlyContinue)
|
||||||
if ($publishedItems.Count -eq 0) {
|
if ($publishedItems.Count -eq 0) {
|
||||||
throw "dotnet publish completed, but no files were produced in: $publishDir"
|
throw "dotnet publish completed, but no files were produced in: $publishDir"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir"
|
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir"
|
||||||
|
$archiveInputs.Add($publishDir)
|
||||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $null -Overwrite Replace -LegacyProperty 'packageFile'
|
$publishOutputs.Add([pscustomobject]@{
|
||||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $null -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
|
projectName = $projectName
|
||||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($publishDir) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
runtimeIdentifier = [string]$runtimeIdentifier
|
||||||
|
directory = $publishDir
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Export-ModuleMember -Function Invoke-Plugin
|
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($archiveInputs) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||||
|
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'publishOutputs' -Value @($publishOutputs) -Overwrite Replace
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin, Get-DotNetPublishRuntimeIdentifiers
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -7,12 +7,15 @@
|
|||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Resolves one or more .NET test projects (`project` or `projects`), runs tests once
|
Resolves one or more .NET test projects (`project` or `projects`), runs tests once
|
||||||
via TestRunner, then publishes metrics on the shared engine context for any later
|
via TestRunner (Microsoft Testing Platform + coverlet.MTP only — no VSTest /
|
||||||
|
coverlet.collector), then publishes metrics on the shared engine context for any later
|
||||||
plugin: `qualityLineCoverage`, `testResult`, `coverageLineRate` / `coverageBranchRate` / `coverageMethodRate`,
|
plugin: `qualityLineCoverage`, `testResult`, `coverageLineRate` / `coverageBranchRate` / `coverageMethodRate`,
|
||||||
method counts, `testResultsDirectory`, `coverageCoberturaPaths`. Quality gates read
|
method counts, `testResultsDirectory`, `coverageCoberturaPaths`. Quality gates read
|
||||||
those keys generically (not tied to this plugin by name). When `resultsDir` (or the
|
those keys generically (not tied to this plugin by name). When `resultsDir` (or the
|
||||||
multi-project default TestResults folder) is used, Cobertura output is kept on disk
|
multi-project default TestResults folder) is used, Cobertura output is kept on disk
|
||||||
via TestRunner `-KeepResults` so repo-root `test-results/` persists after the run.
|
via TestRunner `-KeepResults` so repo-root `test-results/` persists after the run.
|
||||||
|
Product solutions must place `src/global.json` with `test.runner` =
|
||||||
|
Microsoft.Testing.Platform next to the `.sln`/`.slnx`.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
#requires -Version 7.0
|
|
||||||
#requires -PSEdition Core
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Builds an npm workspace (install + build script).
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Runs npm ci (or npm install when useCi is false) and npm run build in the
|
|
||||||
configured workspace root. Requires NpmReleaseVersion to have set
|
|
||||||
shared npmWorkspaceRoot unless workspaceRoot is configured explicitly.
|
|
||||||
#>
|
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
|
||||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
||||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
|
||||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
|
||||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Plugin {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
$Settings
|
|
||||||
)
|
|
||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
|
||||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
|
||||||
$shared = $Settings.context
|
|
||||||
|
|
||||||
Assert-Command npm
|
|
||||||
|
|
||||||
$workspaceRoot = $null
|
|
||||||
if ($pluginSettings.workspaceRoot) {
|
|
||||||
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $shared.scriptDir)
|
|
||||||
$workspaceRoot = $workspaceRoots[0]
|
|
||||||
}
|
|
||||||
elseif ($shared.PSObject.Properties['npmWorkspaceRoot'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.npmWorkspaceRoot)) {
|
|
||||||
$workspaceRoot = [string]$shared.npmWorkspaceRoot
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw "NpmBuild plugin requires 'workspaceRoot' or a prior NpmReleaseVersion plugin run."
|
|
||||||
}
|
|
||||||
|
|
||||||
$useCi = $true
|
|
||||||
if ($null -ne $pluginSettings.useCi) {
|
|
||||||
$useCi = [bool]$pluginSettings.useCi
|
|
||||||
}
|
|
||||||
|
|
||||||
$buildScript = 'build'
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.buildScript)) {
|
|
||||||
$buildScript = [string]$pluginSettings.buildScript
|
|
||||||
}
|
|
||||||
|
|
||||||
Push-Location $workspaceRoot
|
|
||||||
try {
|
|
||||||
if ($useCi) {
|
|
||||||
Write-Log -Level "STEP" -Message "Running npm ci in '$workspaceRoot'..."
|
|
||||||
npm ci
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "npm ci failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Log -Level "STEP" -Message "Running npm install in '$workspaceRoot'..."
|
|
||||||
npm install
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "npm install failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Running npm run $buildScript..."
|
|
||||||
npm run $buildScript
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "npm run $buildScript failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "OK" -Message " npm build completed."
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Export-ModuleMember -Function Invoke-Plugin
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
#requires -Version 7.0
|
|
||||||
#requires -PSEdition Core
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
npm/Jest test plugin for the test engine.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Runs Jest with coverage via TestRunner.Invoke-NpmJestTestsWithCoverage and publishes
|
|
||||||
normalized metrics on the shared engine context for downstream plugins.
|
|
||||||
#>
|
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
|
||||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
||||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
|
||||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
|
||||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Plugin {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
$Settings
|
|
||||||
)
|
|
||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
|
||||||
Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Invoke-NpmJestTestsWithCoverage"
|
|
||||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
|
||||||
$sharedSettings = $Settings.context
|
|
||||||
$scriptDir = $sharedSettings.scriptDir
|
|
||||||
|
|
||||||
Assert-Command npm
|
|
||||||
|
|
||||||
if (-not $pluginSettings.workspaceRoot) {
|
|
||||||
throw "NpmJestTest plugin requires 'workspaceRoot' in scriptSettings.json."
|
|
||||||
}
|
|
||||||
|
|
||||||
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $scriptDir)
|
|
||||||
$workspaceRoot = $workspaceRoots[0]
|
|
||||||
|
|
||||||
$testScript = 'test'
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.testScript)) {
|
|
||||||
$testScript = [string]$pluginSettings.testScript
|
|
||||||
}
|
|
||||||
|
|
||||||
$coverageDirectory = 'coverage'
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.coverageDirectory)) {
|
|
||||||
$coverageDirectory = [string]$pluginSettings.coverageDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
$testResult = Invoke-NpmJestTestsWithCoverage -WorkspaceRoot $workspaceRoot -TestScript $testScript -CoverageDirectory $coverageDirectory
|
|
||||||
|
|
||||||
if (-not $testResult.Success) {
|
|
||||||
throw "Tests failed. $($testResult.Error)"
|
|
||||||
}
|
|
||||||
|
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
|
|
||||||
Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Publish-CoverageMetricsToSharedContext"
|
|
||||||
Set-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'workspaceRoot' -Value $workspaceRoot -Overwrite Replace -LegacyProperty 'npmWorkspaceRoot'
|
|
||||||
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
|
|
||||||
if (($testResult.PSObject.Properties.Name -contains 'CoverageSummaryFile') -and $testResult.CoverageSummaryFile) {
|
|
||||||
Set-EngineFact -Context $sharedSettings -Namespace 'test' -Name 'coverageSummaryFile' -Value $testResult.CoverageSummaryFile -Overwrite Replace -LegacyProperty 'coverageSummaryFile'
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "OK" -Message " All tests passed!"
|
|
||||||
Write-Log -Level "INFO" -Message " Line Coverage: $($testResult.LineRate)%"
|
|
||||||
Write-Log -Level "INFO" -Message " Branch Coverage: $($testResult.BranchRate)%"
|
|
||||||
Write-Log -Level "INFO" -Message " Method Coverage: $($testResult.MethodRate)%"
|
|
||||||
}
|
|
||||||
|
|
||||||
Export-ModuleMember -Function Invoke-Plugin
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
#requires -Version 7.0
|
|
||||||
#requires -PSEdition Core
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Packs npm workspace packages into .tgz release artifacts.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Runs npm pack for each configured workspace package and publishes the
|
|
||||||
resulting tarball paths into shared release context (releaseAssetPaths)
|
|
||||||
for the GitHub release plugin.
|
|
||||||
#>
|
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
|
||||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
||||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
|
||||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
|
||||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Plugin {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
$Settings
|
|
||||||
)
|
|
||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
|
||||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
|
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
|
||||||
$shared = $Settings.context
|
|
||||||
|
|
||||||
Assert-Command npm
|
|
||||||
|
|
||||||
$workspaceRoot = $null
|
|
||||||
if ($pluginSettings.workspaceRoot) {
|
|
||||||
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $shared.scriptDir)
|
|
||||||
$workspaceRoot = $workspaceRoots[0]
|
|
||||||
}
|
|
||||||
elseif ($shared.PSObject.Properties['npmWorkspaceRoot'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.npmWorkspaceRoot)) {
|
|
||||||
$workspaceRoot = [string]$shared.npmWorkspaceRoot
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw "NpmPack plugin requires 'workspaceRoot' or a prior NpmReleaseVersion plugin run."
|
|
||||||
}
|
|
||||||
|
|
||||||
$artifactsDirectory = $null
|
|
||||||
if ($pluginSettings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.artifactsDir)) {
|
|
||||||
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $shared.scriptDir ([string]$pluginSettings.artifactsDir)))
|
|
||||||
Set-EngineState -Context $shared -Name 'artifactsDirectory' -Value $artifactsDirectory
|
|
||||||
}
|
|
||||||
elseif ($shared.PSObject.Properties['artifactsDirectory'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.artifactsDirectory)) {
|
|
||||||
$artifactsDirectory = [string]$shared.artifactsDirectory
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw "NpmPack plugin requires release-stage artifactsDirectory."
|
|
||||||
}
|
|
||||||
|
|
||||||
$packOrder = @()
|
|
||||||
if ($pluginSettings.publishOrder) {
|
|
||||||
if ($pluginSettings.publishOrder -is [System.Collections.IEnumerable] -and -not ($pluginSettings.publishOrder -is [string])) {
|
|
||||||
$packOrder = @($pluginSettings.publishOrder | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
|
||||||
}
|
|
||||||
elseif (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.publishOrder)) {
|
|
||||||
$packOrder = @([string]$pluginSettings.publishOrder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elseif ($pluginSettings.packOrder) {
|
|
||||||
if ($pluginSettings.packOrder -is [System.Collections.IEnumerable] -and -not ($pluginSettings.packOrder -is [string])) {
|
|
||||||
$packOrder = @($pluginSettings.packOrder | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
|
||||||
}
|
|
||||||
elseif (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.packOrder)) {
|
|
||||||
$packOrder = @([string]$pluginSettings.packOrder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($packOrder.Count -eq 0) {
|
|
||||||
throw "NpmPack plugin requires non-empty 'publishOrder' or 'packOrder' (workspace package names)."
|
|
||||||
}
|
|
||||||
|
|
||||||
Import-Module (Join-Path $PSScriptRoot 'NpmPackageSupport.psm1') -Force
|
|
||||||
$useWorkspaces = Test-NpmWorkspacesConfigured -WorkspaceRoot $workspaceRoot
|
|
||||||
if (-not $useWorkspaces -and $packOrder.Count -gt 1) {
|
|
||||||
throw "NpmPack plugin requires npm workspaces when packing more than one package."
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
|
||||||
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$releaseAssetPaths = @()
|
|
||||||
|
|
||||||
Push-Location $workspaceRoot
|
|
||||||
try {
|
|
||||||
foreach ($packageName in $packOrder) {
|
|
||||||
Write-Log -Level "STEP" -Message "Packing npm package '$packageName'..."
|
|
||||||
if ($useWorkspaces) {
|
|
||||||
$tarballName = (npm pack -w $packageName --pack-destination $artifactsDirectory 2>$null | Select-Object -Last 1)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Assert-NpmRootPackageName -WorkspaceRoot $workspaceRoot -ExpectedPackageName $packageName
|
|
||||||
$tarballName = (npm pack --pack-destination $artifactsDirectory 2>$null | Select-Object -Last 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace([string]$tarballName)) {
|
|
||||||
throw "npm pack failed for '$packageName'."
|
|
||||||
}
|
|
||||||
|
|
||||||
$tarballPath = Join-Path $artifactsDirectory ([string]$tarballName).Trim()
|
|
||||||
if (-not (Test-Path $tarballPath -PathType Leaf)) {
|
|
||||||
throw "Could not locate pack output for '$packageName' at: $tarballPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$strayTarballPath = Join-Path $workspaceRoot ([string]$tarballName).Trim()
|
|
||||||
if ((Test-Path $strayTarballPath -PathType Leaf) -and $strayTarballPath -ne $tarballPath) {
|
|
||||||
Remove-Item -LiteralPath $strayTarballPath -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "OK" -Message " Package ready: $tarballPath"
|
|
||||||
$releaseAssetPaths += $tarballPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
|
|
||||||
Set-EngineState -Context $shared -Name 'releaseDir' -Value $artifactsDirectory
|
|
||||||
Set-EngineFact -Context $shared -Namespace 'release' -Name 'assetPaths' -Value $releaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
|
||||||
if ($releaseAssetPaths.Count -gt 0) {
|
|
||||||
$packageItem = Get-Item -LiteralPath $releaseAssetPaths[0]
|
|
||||||
Set-EngineFact -Context $shared -Namespace 'npm' -Name 'packageFile' -Value $packageItem -Overwrite Replace -LegacyProperty 'packageFile'
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "OK" -Message " npm pack completed ($($releaseAssetPaths.Count) tarball(s))."
|
|
||||||
}
|
|
||||||
|
|
||||||
Export-ModuleMember -Function Invoke-Plugin
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
#requires -Version 7.0
|
|
||||||
#requires -PSEdition Core
|
|
||||||
|
|
||||||
function Get-NpmRootPackageJson {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$WorkspaceRoot
|
|
||||||
)
|
|
||||||
|
|
||||||
$packageJsonPath = Join-Path $WorkspaceRoot 'package.json'
|
|
||||||
if (-not (Test-Path $packageJsonPath -PathType Leaf)) {
|
|
||||||
throw "package.json not found at '$packageJsonPath'."
|
|
||||||
}
|
|
||||||
|
|
||||||
return Get-Content -Path $packageJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-NpmWorkspacesConfigured {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$WorkspaceRoot
|
|
||||||
)
|
|
||||||
|
|
||||||
$json = Get-NpmRootPackageJson -WorkspaceRoot $WorkspaceRoot
|
|
||||||
if (-not $json.PSObject.Properties['workspaces'] -or $null -eq $json.workspaces) {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
$workspaces = $json.workspaces
|
|
||||||
if ($workspaces -is [string]) {
|
|
||||||
return -not [string]::IsNullOrWhiteSpace($workspaces)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($workspaces -is [System.Collections.IEnumerable]) {
|
|
||||||
$entries = @($workspaces | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
|
||||||
return $entries.Count -gt 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Assert-NpmRootPackageName {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$WorkspaceRoot,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$ExpectedPackageName
|
|
||||||
)
|
|
||||||
|
|
||||||
$json = Get-NpmRootPackageJson -WorkspaceRoot $WorkspaceRoot
|
|
||||||
$rootPackageName = [string]$json.name
|
|
||||||
if ([string]::IsNullOrWhiteSpace($rootPackageName)) {
|
|
||||||
throw "Root package.json at '$WorkspaceRoot' is missing 'name'."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($rootPackageName -ne $ExpectedPackageName) {
|
|
||||||
throw "publishOrder package '$ExpectedPackageName' does not match root package name '$rootPackageName' (npm workspaces not configured)."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Export-ModuleMember -Function Test-NpmWorkspacesConfigured, Assert-NpmRootPackageName, Get-NpmRootPackageJson
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
#requires -Version 7.0
|
|
||||||
#requires -PSEdition Core
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Publishes npm workspace packages to the npm registry.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Publishes packages in configured order using npmSecret (logical secret name).
|
|
||||||
Pass the token via an environment variable named like the configured npm secret (e.g. Npm).
|
|
||||||
Uses a temporary .npmrc in the workspace root.
|
|
||||||
#>
|
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
|
||||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
||||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
|
||||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
|
||||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Plugin {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
$Settings
|
|
||||||
)
|
|
||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
|
||||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
|
||||||
$shared = $Settings.context
|
|
||||||
|
|
||||||
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
|
|
||||||
|
|
||||||
Assert-Command npm
|
|
||||||
|
|
||||||
$workspaceRoot = $null
|
|
||||||
if ($pluginSettings.workspaceRoot) {
|
|
||||||
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $shared.scriptDir)
|
|
||||||
$workspaceRoot = $workspaceRoots[0]
|
|
||||||
}
|
|
||||||
elseif ($shared.PSObject.Properties['npmWorkspaceRoot'] -and -not [string]::IsNullOrWhiteSpace([string]$shared.npmWorkspaceRoot)) {
|
|
||||||
$workspaceRoot = [string]$shared.npmWorkspaceRoot
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw "NpmPublish plugin requires 'workspaceRoot' or a prior NpmReleaseVersion plugin run."
|
|
||||||
}
|
|
||||||
|
|
||||||
$registry = if ([string]::IsNullOrWhiteSpace([string]$pluginSettings.registry)) {
|
|
||||||
'https://registry.npmjs.org'
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[string]$pluginSettings.registry
|
|
||||||
}
|
|
||||||
|
|
||||||
$access = if ([string]::IsNullOrWhiteSpace([string]$pluginSettings.access)) {
|
|
||||||
'public'
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[string]$pluginSettings.access
|
|
||||||
}
|
|
||||||
|
|
||||||
$publishOrder = @()
|
|
||||||
if ($pluginSettings.publishOrder) {
|
|
||||||
if ($pluginSettings.publishOrder -is [System.Collections.IEnumerable] -and -not ($pluginSettings.publishOrder -is [string])) {
|
|
||||||
$publishOrder = @($pluginSettings.publishOrder | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
|
||||||
}
|
|
||||||
elseif (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.publishOrder)) {
|
|
||||||
$publishOrder = @([string]$pluginSettings.publishOrder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($publishOrder.Count -eq 0) {
|
|
||||||
throw "NpmPublish plugin requires non-empty 'publishOrder' (workspace package names)."
|
|
||||||
}
|
|
||||||
|
|
||||||
Import-Module (Join-Path $PSScriptRoot 'NpmPackageSupport.psm1') -Force
|
|
||||||
$useWorkspaces = Test-NpmWorkspacesConfigured -WorkspaceRoot $workspaceRoot
|
|
||||||
if (-not $useWorkspaces -and $publishOrder.Count -gt 1) {
|
|
||||||
throw "NpmPublish plugin requires npm workspaces when publishing more than one package."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($dryRun) {
|
|
||||||
foreach ($packageName in $publishOrder) {
|
|
||||||
Write-Log -Level "INFO" -Message "Dry run: would publish npm package '$packageName' to $registry"
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$npmSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'npmSecret'
|
|
||||||
if ([string]::IsNullOrWhiteSpace($npmSecret)) {
|
|
||||||
throw "NpmPublish plugin requires 'npmSecret' in scriptSettings.json (logical secret name, e.g. Npm)."
|
|
||||||
}
|
|
||||||
|
|
||||||
$npmToken = Get-SecretEnvironmentValue -Name $npmSecret
|
|
||||||
if ([string]::IsNullOrWhiteSpace($npmToken)) {
|
|
||||||
throw "npm API key is not set. Set environment variable '$npmSecret'."
|
|
||||||
}
|
|
||||||
|
|
||||||
$registryHost = ([uri]$registry).Host
|
|
||||||
$tempNpmRcPath = Join-Path $workspaceRoot ".npmrc.release-temp"
|
|
||||||
$npmRcContent = @"
|
|
||||||
registry=$registry
|
|
||||||
//$registryHost/:_authToken=$npmToken
|
|
||||||
"@
|
|
||||||
|
|
||||||
Push-Location $workspaceRoot
|
|
||||||
try {
|
|
||||||
Set-Content -Path $tempNpmRcPath -Value $npmRcContent -Encoding UTF8 -NoNewline
|
|
||||||
|
|
||||||
foreach ($packageName in $publishOrder) {
|
|
||||||
Write-Log -Level "STEP" -Message "Publishing npm package '$packageName'..."
|
|
||||||
if ($useWorkspaces) {
|
|
||||||
npm publish -w $packageName --access $access --userconfig $tempNpmRcPath
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Assert-NpmRootPackageName -WorkspaceRoot $workspaceRoot -ExpectedPackageName $packageName
|
|
||||||
npm publish --access $access --userconfig $tempNpmRcPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "Failed to publish npm package '$packageName'."
|
|
||||||
}
|
|
||||||
Write-Log -Level "OK" -Message " Published $packageName."
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Log -Level "OK" -Message " npm publish completed."
|
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-EnginePublishCompletion"
|
|
||||||
Add-EnginePublishCompletion -Context $shared -Publisher 'NpmPublish'
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if (Test-Path $tempNpmRcPath -PathType Leaf) {
|
|
||||||
Remove-Item -Path $tempNpmRcPath -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-PluginMetadata {
|
|
||||||
[pscustomobject]@{ mutatesRemote = $true }
|
|
||||||
}
|
|
||||||
|
|
||||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
#requires -Version 7.0
|
|
||||||
#requires -PSEdition Core
|
|
||||||
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Loads npm workspace release version into shared context.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Reads semver from the configured workspace package.json and writes it to
|
|
||||||
shared context version. Optionally synchronizes version fields across
|
|
||||||
workspace package manifests before build/publish.
|
|
||||||
#>
|
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
|
||||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
||||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
|
||||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
|
||||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-PackageJsonVersionInternal {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$PackageJsonPath
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not (Test-Path $PackageJsonPath -PathType Leaf)) {
|
|
||||||
throw "NpmReleaseVersion: package.json not found at '$PackageJsonPath'."
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = Get-Content -Path $PackageJsonPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
$version = [string]$json.version
|
|
||||||
if ([string]::IsNullOrWhiteSpace($version)) {
|
|
||||||
throw "NpmReleaseVersion: 'version' is missing in '$PackageJsonPath'."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($version -notmatch '^\d+\.\d+\.\d+') {
|
|
||||||
throw "NpmReleaseVersion: version '$version' in '$PackageJsonPath' is not a valid semver."
|
|
||||||
}
|
|
||||||
|
|
||||||
return $version
|
|
||||||
}
|
|
||||||
|
|
||||||
function Set-PackageJsonVersionInternal {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$PackageJsonPath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Version
|
|
||||||
)
|
|
||||||
|
|
||||||
$raw = Get-Content -Path $PackageJsonPath -Raw -Encoding UTF8
|
|
||||||
$json = $raw | ConvertFrom-Json
|
|
||||||
$json.version = $Version
|
|
||||||
($json | ConvertTo-Json -Depth 100) + [Environment]::NewLine | Set-Content -Path $PackageJsonPath -Encoding UTF8 -NoNewline
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-PluginMetadata {
|
|
||||||
return [pscustomobject]@{ providesVersion = $true }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Plugin {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
$Settings
|
|
||||||
)
|
|
||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
|
||||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
|
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
|
||||||
$shared = $Settings.context
|
|
||||||
|
|
||||||
$packageJsonPaths = @(Resolve-RelativePaths -Value $pluginSettings.packageJsonPath -BasePath $shared.scriptDir)
|
|
||||||
if ($packageJsonPaths.Count -eq 0) {
|
|
||||||
throw "NpmReleaseVersion plugin requires 'packageJsonPath' in scriptSettings.json."
|
|
||||||
}
|
|
||||||
$packageJsonPath = $packageJsonPaths[0]
|
|
||||||
|
|
||||||
$version = Get-PackageJsonVersionInternal -PackageJsonPath $packageJsonPath
|
|
||||||
$syncWorkspaceVersions = $false
|
|
||||||
if ($null -ne $pluginSettings.syncWorkspaceVersions) {
|
|
||||||
$syncWorkspaceVersions = [bool]$pluginSettings.syncWorkspaceVersions
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($syncWorkspaceVersions) {
|
|
||||||
$workspaceRoot = Split-Path -Parent $packageJsonPath
|
|
||||||
$packageManifests = Get-ChildItem -Path (Join-Path $workspaceRoot 'packages') -Recurse -Filter package.json -File -ErrorAction SilentlyContinue
|
|
||||||
foreach ($manifest in $packageManifests) {
|
|
||||||
Set-PackageJsonVersionInternal -PackageJsonPath $manifest.FullName -Version $version
|
|
||||||
}
|
|
||||||
Write-Log -Level "OK" -Message " Synchronized workspace package versions to $version."
|
|
||||||
}
|
|
||||||
|
|
||||||
$npmWorkspaceRoot = Split-Path -Parent $packageJsonPath
|
|
||||||
Set-EngineState -Context $shared -Name 'version' -Value $version
|
|
||||||
Set-EngineFact -Context $shared -Namespace 'npm' -Name 'workspaceRoot' -Value $npmWorkspaceRoot -Overwrite Replace -LegacyProperty 'npmWorkspaceRoot'
|
|
||||||
Set-EngineFact -Context $shared -Namespace 'npm' -Name 'packageJsonPath' -Value $packageJsonPath -Overwrite Replace -LegacyProperty 'npmPackageJsonPath'
|
|
||||||
Write-Log -Level "OK" -Message " Release version loaded by NpmReleaseVersion plugin: $version"
|
|
||||||
}
|
|
||||||
|
|
||||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
|
||||||
78
utils/plugins/Platform/CollectCoverage.psm1
Normal file
78
utils/plugins/Platform/CollectCoverage.psm1
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Collects coverage metrics from an existing test results directory.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Stack-agnostic parser for recovered container or host test output. Auto-detects .NET
|
||||||
|
Cobertura (coverage.cobertura.xml) or Jest (coverage-summary.json) and publishes the
|
||||||
|
same shared-context keys as DotNetTest / NpmJestTest for QualityGate.
|
||||||
|
Use after DockerContainerBuilder / PodmanContainerBuilder when tests run inside a container image.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||||
|
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||||
|
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Get-CoverageFromResultsDirectory'
|
||||||
|
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Publish-CoverageMetricsToSharedContext'
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.context
|
||||||
|
$scriptDir = $sharedSettings.scriptDir
|
||||||
|
|
||||||
|
$resultsDirSetting = $pluginSettings.resultsDir
|
||||||
|
if ([string]::IsNullOrWhiteSpace($resultsDirSetting)) {
|
||||||
|
if ($sharedSettings.PSObject.Properties.Name -contains 'testResultsDirectory' -and $sharedSettings.testResultsDirectory) {
|
||||||
|
$resultsDirSetting = $sharedSettings.testResultsDirectory
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw "CollectCoverage requires 'resultsDir' in plugin settings or testResultsDirectory on shared context (from ContainerBuilder)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$resultsDirectory = if ([System.IO.Path]::IsPathRooted([string]$resultsDirSetting)) {
|
||||||
|
[string]$resultsDirSetting
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$resultsDirSetting)))
|
||||||
|
}
|
||||||
|
|
||||||
|
$format = 'auto'
|
||||||
|
if ($pluginSettings.PSObject.Properties.Name -contains 'format' -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.format)) {
|
||||||
|
$format = [string]$pluginSettings.format
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level 'STEP' -Message "Collecting coverage from $resultsDirectory ..."
|
||||||
|
$testResult = Get-CoverageFromResultsDirectory -ResultsDirectory $resultsDirectory -Format $format -Silent:$true
|
||||||
|
if (-not $testResult.Success) {
|
||||||
|
throw $testResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
|
||||||
|
|
||||||
|
$formatLabel = if ($testResult.CoverageFormat) { $testResult.CoverageFormat } else { 'unknown' }
|
||||||
|
Write-Log -Level 'OK' -Message " Coverage collected ($formatLabel)."
|
||||||
|
Write-Log -Level 'INFO' -Message " Line Coverage: $($testResult.LineRate)%"
|
||||||
|
Write-Log -Level 'INFO' -Message " Branch Coverage: $($testResult.BranchRate)%"
|
||||||
|
Write-Log -Level 'INFO' -Message " Method Coverage: $($testResult.MethodRate)%"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginMetadata {
|
||||||
|
[pscustomobject]@{ mutatesRemote = $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||||
52
utils/plugins/Platform/ContainerEngineProbe.psm1
Normal file
52
utils/plugins/Platform/ContainerEngineProbe.psm1
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Probes docker/podman availability and sets mandatory shared engine state for the pipeline.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Must be the first enabled plugin. Sets containerEngineProbeCompleted, activeContainerEngine,
|
||||||
|
dockerEngineAvailable, podmanEngineAvailable, and compose CLI fields on shared context.
|
||||||
|
Fails the pipeline when no usable engine is found.
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||||
|
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||||
|
Import-PluginDependency -ModuleName 'ContainerEngineSupport' -RequiredCommand 'Resolve-ActiveContainerEngineState'
|
||||||
|
|
||||||
|
$shared = $Settings.context
|
||||||
|
|
||||||
|
Write-Log -Level 'STEP' -Message 'Probing container engine availability...'
|
||||||
|
$state = Resolve-ActiveContainerEngineState
|
||||||
|
Set-ContainerEngineSharedState -SharedSettings $shared -State $state
|
||||||
|
|
||||||
|
$display = Get-ContainerEngineDisplayName -Engine (Get-SharedContainerEngineInvocation -SharedSettings $shared)
|
||||||
|
Write-Log -Level 'OK' -Message " Active engine: $display"
|
||||||
|
Write-Log -Level 'INFO' -Message " docker CLI available: $($state.DockerEngineAvailable); podman CLI available: $($state.PodmanEngineAvailable)"
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$state.ComposeExecutable)) {
|
||||||
|
Write-Log -Level 'INFO' -Message ' Compose CLI: not available (Compose plugins will fail if enabled).'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level 'INFO' -Message " Compose CLI: $($state.ComposeExecutable) $($state.ComposeBaseArgs -join ' ')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginMetadata {
|
||||||
|
[pscustomobject]@{ mutatesRemote = $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||||
124
utils/plugins/Platform/DiscoverPackageArtifacts.psm1
Normal file
124
utils/plugins/Platform/DiscoverPackageArtifacts.psm1
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
#requires -Version 7.0
|
||||||
|
#requires -PSEdition Core
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Discovers package artifacts produced by pack plugins or container build pipelines.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Stack-agnostic scanner for artifactsDir. Detects .NET .nupkg/.snupkg or npm .tgz files
|
||||||
|
and populates shared context (packageFile, releaseAssetPaths, releaseArchiveInputs).
|
||||||
|
#>
|
||||||
|
|
||||||
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
|
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||||
|
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||||
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
|
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Plugin {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
$Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||||
|
Import-PluginDependency -ModuleName 'DotNetArtifactSupport' -RequiredCommand 'Resolve-DotNetPackageArtifacts'
|
||||||
|
|
||||||
|
$pluginSettings = $Settings
|
||||||
|
$sharedSettings = $Settings.context
|
||||||
|
$scriptDir = $sharedSettings.scriptDir
|
||||||
|
$version = $sharedSettings.version
|
||||||
|
|
||||||
|
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
|
||||||
|
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir)))
|
||||||
|
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
|
||||||
|
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
|
||||||
|
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName artifactsDirectory -NotePropertyValue $artifactsDirectory -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||||
|
throw 'DiscoverPackageArtifacts requires artifactsDir in plugin settings or artifactsDirectory on shared context.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$nupkgCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.nupkg' -ErrorAction SilentlyContinue | Where-Object {
|
||||||
|
$_.Name -notlike '*.symbols.nupkg' -and $_.Name -notlike '*.snupkg'
|
||||||
|
})
|
||||||
|
$tgzCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.tgz' -ErrorAction SilentlyContinue)
|
||||||
|
|
||||||
|
if ($nupkgCandidates.Count -gt 0 -and $tgzCandidates.Count -gt 0) {
|
||||||
|
throw "DiscoverPackageArtifacts found both NuGet and npm artifacts in $artifactsDirectory. Use one package stack per repository."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($nupkgCandidates.Count -gt 0) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$version)) {
|
||||||
|
throw 'DiscoverPackageArtifacts requires release version on shared context when discovering .nupkg artifacts.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Level 'STEP' -Message "Discovering NuGet package artifacts in $artifactsDirectory ..."
|
||||||
|
$resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
|
||||||
|
|
||||||
|
Write-Log -Level 'OK' -Message " Package ready: $($resolved.PackageFile.FullName)"
|
||||||
|
if ($resolved.SymbolsPackageFile) {
|
||||||
|
Write-Log -Level 'OK' -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Level 'WARN' -Message " Symbols package (.snupkg) not found for version $version."
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName symbolsPackageFile -NotePropertyValue $resolved.SymbolsPackageFile -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
|
||||||
|
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
|
||||||
|
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
|
||||||
|
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
|
||||||
|
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||||
|
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($tgzCandidates.Count -gt 0) {
|
||||||
|
Import-PluginDependency -ModuleName 'NpmArtifactSupport' -RequiredCommand 'Resolve-NpmPackageArtifacts'
|
||||||
|
|
||||||
|
Write-Log -Level 'STEP' -Message "Discovering npm package artifacts in $artifactsDirectory ..."
|
||||||
|
$resolved = Resolve-NpmPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
|
||||||
|
|
||||||
|
foreach ($tarball in $resolved.PackageFiles) {
|
||||||
|
Write-Log -Level 'OK' -Message " Package ready: $($tarball.FullName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseAssetPaths -Force
|
||||||
|
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
|
||||||
|
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
|
||||||
|
Set-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
|
||||||
|
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||||
|
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
||||||
|
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
|
||||||
|
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "DiscoverPackageArtifacts found no .nupkg or .tgz files in: $artifactsDirectory"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PluginMetadata {
|
||||||
|
[pscustomobject]@{ mutatesRemote = $false }
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||||
@ -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,11 +96,11 @@ 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
|
||||||
$sharedSettings = $Settings.context
|
$sharedSettings = $Settings.context
|
||||||
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret'
|
|
||||||
$configuredRepository = $pluginSettings.repository
|
$configuredRepository = $pluginSettings.repository
|
||||||
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
|
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
|
||||||
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
|
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
|
||||||
@ -110,6 +111,7 @@ function Invoke-Plugin {
|
|||||||
$releaseAssetPaths = @()
|
$releaseAssetPaths = @()
|
||||||
|
|
||||||
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
||||||
|
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret' -PluginDisplayName 'GitHub' -Required
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||||
throw "GitHub plugin requires 'releaseNotesFile' in scriptSettings.json."
|
throw "GitHub plugin requires 'releaseNotesFile' in scriptSettings.json."
|
||||||
@ -128,18 +130,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert-Command gh
|
Assert-Command gh
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($githubSecret)) {
|
$ghToken = Get-RepoUtilsSecretSlot -Name $githubSecret -Settings $sharedSettings
|
||||||
throw "GitHub plugin requires 'githubSecret' in scriptSettings.json (logical secret name, e.g. GitHub)."
|
|
||||||
}
|
|
||||||
|
|
||||||
$ghToken = Get-SecretEnvironmentValue -Name $githubSecret
|
|
||||||
if ([string]::IsNullOrWhiteSpace($ghToken)) {
|
if ([string]::IsNullOrWhiteSpace($ghToken)) {
|
||||||
throw "GitHub token is not set. Set environment variable '$githubSecret'."
|
throw "GitHub token is not set. Set RepoUtilsSecrets slot '$githubSecret' (githubSecret)."
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||||
@ -230,7 +231,7 @@ function Invoke-Plugin {
|
|||||||
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
||||||
}
|
}
|
||||||
|
|
||||||
throw "GitHub CLI authentication failed for repository '$repo'. Ensure secret '$githubSecret' is valid and has access to this repository."
|
throw "GitHub CLI authentication failed for repository '$repo'. Ensure RepoUtilsSecrets slot '$githubSecret' is valid and has access to this repository."
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Log -Level "OK" -Message " GitHub token validated for repository: $($authOutput | Select-Object -First 1)"
|
Write-Log -Level "OK" -Message " GitHub token validated for repository: $($authOutput | Select-Object -First 1)"
|
||||||
@ -261,6 +262,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