(bugfix): HOSTNAMES_ARRAY_POLICY and deepDelta fixes
Some checks are pending
Storybook tests / storybook-tests (push) Waiting to run

This commit is contained in:
Maksym Sadovnychyy 2026-07-06 21:15:17 +02:00
parent 0f2638d439
commit b74e9250a7
85 changed files with 4234 additions and 391 deletions

View File

@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.4] - 2026-07-06
### Added
- **OAuth / external identity providers** (extracted from FiPlan.Anywhere):
- `@maks-it.com/webui-contracts`: `LoginProviderExternal`, `EmailProtocol`, `ServerAuthenticationMethod`, `MsalViewModel`, `IdpRedeemLoginResponse`, `MailboxOAuthPending`, and mailbox OAuth session/query constants.
- `@maks-it.com/webui-core`: `startIdpRedirect`, `shouldSkipIdpLoginCallback`, `useIdpLoginCallback`, mailbox OAuth redirect helpers (`collectMailboxOAuthProtocols`, `startMailboxOAuthRedirect`, `useMailboxOAuthReturn`), and `removeParamFromUrl` / `removeParamsFromUrl`.
- `@maks-it.com/webui-components`: `ExternalLoginButtons`, `IdpLoginCallbackHandler`, `MailboxOAuthReturnHandler`.
- `@maks-it.com/webui-core`: `HOSTNAMES_ARRAY_POLICY` for Certs UI account hostname PATCH collections (`identityKey` / `idFieldKey`: `hostname`).
- `deepDelta` regression tests for Certs/Vault consumer forms — `HOSTNAMES_ARRAY_POLICY`, `ENTITY_SCOPES_ARRAY_POLICY`, `VERSIONS_ARRAY_POLICY`, advanced `ArrayPolicy` options (`rootKey`, `childArrayKeys`, `deleteItemWhenRoleRemoved`).
### Changed
- All `@maks-it.com/webui-*` packages versioned at **0.3.4** with aligned workspace dependency ranges.
- RepoUtils entry points and docs: `utils/src/` → `utils/` (`Invoke-TestEngine.bat`, `Invoke-ReleasePackage-Single.bat`, `Update-RepoUtils.bat`; README and `assets/docs/NPM_PUBLISH.md` updated).
- Release secret env vars documented as logical names **`Npm`** and **`GitHub`** (see `utils/engines/release/scriptSettings.json`).
- Package READMEs document OAuth exports and PATCH collection policies.
### Fixed
- `deepDelta` identifiable-array diff: role-null removals emit the correct identity field (`id` vs `_deltaId`); item deltas are omitted when only `operations` changed (no spurious id-only placeholders).
## [0.3.3] - 2026-05-31
### Changed

View File

@ -12,7 +12,7 @@ Shared React UI library for **maksit-certs-ui** and **maksit-vault** WebUI apps.
| `@maks-it.com/webui-core` | Utilities (`deepDelta`, enum helpers, ACL parsers) and `useFormState` |
| `@maks-it.com/webui-components` | React components, layout, editors, DataTable, auth shell |
Source lives under `src/` (npm workspaces). Release automation lives under `utils/src/` (from [maksit-repoutils](https://github.com/MAKS-IT-COM/maksit-repoutils)).
Source lives under `src/` (npm workspaces). Release automation lives under `utils/` (from [maksit-repoutils](https://github.com/MAKS-IT-COM/maksit-repoutils)).
## Local development
@ -26,25 +26,25 @@ npm run storybook
**Storybook** (`npm run storybook`) runs a local catalog of `@maks-it.com/webui-components` with Tailwind, React Router, autodocs, a11y checks, and **Vitest component tests** (testing widget + `npm run test-storybook`). Stories live under `src/stories/components/` (mirroring component folders); see `src/stories/README.md` for story conventions and testing.
Tests and coverage badges: **`utils/src/Invoke-TestEngine.bat`** (plugin config in `utils/src/engines/test/scriptSettings.json`; uses `NpmJestTest`).
Tests and coverage badges: **`utils\Invoke-TestEngine.bat`** (plugin config in `utils\engines\test\scriptSettings.json`; uses `NpmJestTest`).
## Release to npmjs
1. Set **`NPMJS_MAKS_IT`** to your npm automation token (same pattern as `NUGET_MAKS_IT` for NuGet).
1. Set **`Npm`** environment variable to your npm automation token (logical secret name in `scriptSettings.json`).
2. Bump **`src/package.json`** `version` (and tag `vX.Y.Z` on `main` when using the publish guard).
3. Run **`utils/src/Invoke-ReleasePackage.bat`** (or `pwsh utils/src/engines/release/Invoke-ReleasePackage.ps1`).
3. Run **`utils\Invoke-ReleasePackage-Single.bat`** (or `pwsh utils\engines\release\Invoke-ReleasePackage.ps1`).
Configured plugins (see `utils/src/engines/release/scriptSettings.json`):
Configured plugins (see `utils\engines\release\scriptSettings.json`):
| Plugin | Role |
|--------|------|
| `NpmReleaseVersion` | Read semver from `src/package.json`; sync `packages/*/package.json` |
| `NpmBuild` | `npm ci` + `npm run build` |
| `ReleasePublishGuard` | Branch/tag checks before publish |
| `GitHub` | GitHub release (optional; needs `GITHUB_MAKS_IT_COM`) |
| `GitHub` | GitHub release (optional; set `GitHub` env var) |
| `NpmPublish` | Publish workspace packages in dependency order |
Refresh shared utils from repoutils: **`utils/src/Update-RepoUtils.bat`**.
Refresh shared utils from repoutils: **`utils\Update-RepoUtils.bat`**.
## Consume in product repos

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 72%">
<title>Branch Coverage: 72%</title>
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 78%">
<title>Branch Coverage: 78%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@ -15,7 +15,7 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">72%</text>
<text x="127.5" y="14" fill="#fff">72%</text>
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">78%</text>
<text x="127.5" y="14" fill="#fff">78%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 75.2%">
<title>Line Coverage: 75.2%</title>
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 82.2%">
<title>Line Coverage: 82.2%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@ -9,13 +9,13 @@
</clipPath>
<g clip-path="url(#r)">
<rect width="94.5" height="20" fill="#555"/>
<rect x="94.5" width="42.5" height="20" fill="#97ca00"/>
<rect x="94.5" width="42.5" height="20" fill="#4c1"/>
<rect width="137" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">75.2%</text>
<text x="115.75" y="14" fill="#fff">75.2%</text>
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">82.2%</text>
<text x="115.75" y="14" fill="#fff">82.2%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 66.7%">
<title>Method Coverage: 66.7%</title>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 62.4%">
<title>Method Coverage: 62.4%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
@ -15,7 +15,7 @@
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">66.7%</text>
<text x="128.75" y="14" fill="#fff">66.7%</text>
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">62.4%</text>
<text x="128.75" y="14" fill="#fff">62.4%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -17,7 +17,7 @@ Published packages:
3. Create an **Automation** token (recommended) or Granular Access token with **Publish** on `@maks-it.com/*`:
- https://www.npmjs.com/settings/maks-it.com/tokens
4. Store the token for release tooling:
- **CI / Release-Package:** set env var `NPMJS_MAKS_IT` to the token value (same pattern as `NUGET_MAKS_IT`).
- **CI / release engine:** set environment variable **`Npm`** (logical secret name in `scriptSettings.json`).
- **Local one-off publish:** `npm login` or a user-level `~/.npmrc` entry:
```
//registry.npmjs.org/:_authToken=YOUR_TOKEN
@ -50,13 +50,13 @@ npm view @maks-it.com/webui-components version
## Release pipeline (recommended)
Use **`utils/src/Invoke-ReleasePackage.bat`** (or `pwsh utils/src/engines/release/Invoke-ReleasePackage.ps1`):
Use **`utils\Invoke-ReleasePackage-Single.bat`** (or `pwsh utils\engines\release\Invoke-ReleasePackage.ps1`):
1. Bump version in `src/package.json` (or tag drives `NpmReleaseVersion`).
2. Tag `HEAD` with exact semver, e.g. `git tag v0.2.0 && git push origin v0.2.0`.
3. Set `NPMJS_MAKS_IT` and run the release engine.
3. Set `$env:Npm` and run the release engine.
`utils/src/engines/release/scriptSettings.json` runs `NpmReleaseVersion`, `NpmBuild`, `ReleasePublishGuard`, optional `GitHub`, then `NpmPublish` in dependency order.
`utils\engines\release\scriptSettings.json` runs `NpmReleaseVersion`, `NpmBuild`, `ReleasePublishGuard`, optional `GitHub`, then `NpmPublish` in dependency order.
## After publish — Certs UI / Vault

View File

@ -1,20 +1,26 @@
{"total": {"lines":{"total":481,"covered":362,"skipped":0,"pct":75.25},"statements":{"total":514,"covered":389,"skipped":0,"pct":75.68},"functions":{"total":66,"covered":44,"skipped":0,"pct":66.66},"branches":{"total":279,"covered":201,"skipped":0,"pct":72.04},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}}
{"total": {"lines":{"total":535,"covered":440,"skipped":0,"pct":82.24},"statements":{"total":572,"covered":471,"skipped":0,"pct":82.34},"functions":{"total":85,"covered":53,"skipped":0,"pct":62.35},"branches":{"total":309,"covered":241,"skipped":0,"pct":77.99},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\PagedRequest.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\PatchOperation.ts": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\PatchRequestModelBase.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\RequestModelBase.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\index.ts": {"lines":{"total":10,"covered":10,"skipped":0,"pct":100},"functions":{"total":9,"covered":2,"skipped":0,"pct":22.22},"statements":{"total":17,"covered":17,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\index.ts": {"lines":{"total":11,"covered":11,"skipped":0,"pct":100},"functions":{"total":9,"covered":2,"skipped":0,"pct":22.22},"statements":{"total":18,"covered":18,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\Claims.ts": {"lines":{"total":56,"covered":56,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":56,"covered":56,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\login\\LoginRequest.ts": {"lines":{"total":9,"covered":9,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":9,"covered":9,"skipped":0,"pct":100},"branches":{"total":8,"covered":8,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\login\\RefreshTokenRequest.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\logout\\LogoutRequest.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\EmailProtocol.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\LoginProviderExternal.ts": {"lines":{"total":11,"covered":5,"skipped":0,"pct":45.45},"functions":{"total":3,"covered":1,"skipped":0,"pct":33.33},"statements":{"total":14,"covered":7,"skipped":0,"pct":50},"branches":{"total":8,"covered":2,"skipped":0,"pct":25}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\ServerAuthenticationMethod.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\index.ts": {"lines":{"total":11,"covered":11,"skipped":0,"pct":100},"functions":{"total":9,"covered":0,"skipped":0,"pct":0},"statements":{"total":13,"covered":13,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\oauth\\oauthConstants.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\acl\\parseAclEntry.ts": {"lines":{"total":17,"covered":17,"skipped":0,"pct":100},"functions":{"total":4,"covered":4,"skipped":0,"pct":100},"statements":{"total":18,"covered":18,"skipped":0,"pct":100},"branches":{"total":6,"covered":6,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\dataTable\\dataTableFilters.ts": {"lines":{"total":5,"covered":5,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\dataTable\\dataTablePaged.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":16,"covered":16,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\date\\isValidDateString.ts": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":7,"covered":7,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepCopy.ts": {"lines":{"total":17,"covered":16,"skipped":0,"pct":94.11},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":17,"covered":16,"skipped":0,"pct":94.11},"branches":{"total":12,"covered":11,"skipped":0,"pct":91.66}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepDelta.ts": {"lines":{"total":183,"covered":119,"skipped":0,"pct":65.02},"functions":{"total":18,"covered":14,"skipped":0,"pct":77.77},"statements":{"total":199,"covered":130,"skipped":0,"pct":65.32},"branches":{"total":137,"covered":92,"skipped":0,"pct":67.15}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepDelta.ts": {"lines":{"total":185,"covered":153,"skipped":0,"pct":82.7},"functions":{"total":18,"covered":15,"skipped":0,"pct":83.33},"statements":{"total":200,"covered":164,"skipped":0,"pct":82},"branches":{"total":139,"covered":116,"skipped":0,"pct":83.45}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepEqual.ts": {"lines":{"total":36,"covered":35,"skipped":0,"pct":97.22},"functions":{"total":3,"covered":3,"skipped":0,"pct":100},"statements":{"total":38,"covered":37,"skipped":0,"pct":97.36},"branches":{"total":26,"covered":25,"skipped":0,"pct":96.15}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\patchCollectionPolicies.ts": {"lines":{"total":17,"covered":15,"skipped":0,"pct":88.23},"functions":{"total":5,"covered":5,"skipped":0,"pct":100},"statements":{"total":17,"covered":15,"skipped":0,"pct":88.23},"branches":{"total":16,"covered":10,"skipped":0,"pct":62.5}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\hasAnyFlag.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":1,"covered":0,"skipped":0,"pct":0}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\hasFlag.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":1,"covered":0,"skipped":0,"pct":0}}
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\toggleFlag.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":3,"covered":2,"skipped":0,"pct":66.66}}

62
src/package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "maksit-webui",
"version": "0.3.3",
"version": "0.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "maksit-webui",
"version": "0.3.3",
"version": "0.3.4",
"license": "MIT",
"workspaces": [
"packages/*"
@ -2326,6 +2326,29 @@
"node": ">=14.0.0"
}
},
"node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@oxc-resolver/binding-win32-arm64-msvc": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.20.0.tgz",
@ -3988,6 +4011,29 @@
"node": ">=14.0.0"
}
},
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz",
@ -10498,10 +10544,10 @@
},
"packages/components": {
"name": "@maks-it.com/webui-components",
"version": "0.3.3",
"version": "0.3.4",
"dependencies": {
"@maks-it.com/webui-contracts": "^0.3.3",
"@maks-it.com/webui-core": "^0.3.3"
"@maks-it.com/webui-contracts": "^0.3.4",
"@maks-it.com/webui-core": "^0.3.4"
},
"devDependencies": {
"@tanstack/react-table": "^8.21.3",
@ -10529,7 +10575,7 @@
},
"packages/contracts": {
"name": "@maks-it.com/webui-contracts",
"version": "0.3.3",
"version": "0.3.4",
"devDependencies": {
"tsup": "^8.5.1",
"typescript": "^6.0.3",
@ -10541,9 +10587,9 @@
},
"packages/core": {
"name": "@maks-it.com/webui-core",
"version": "0.3.3",
"version": "0.3.4",
"dependencies": {
"@maks-it.com/webui-contracts": "^0.3.3",
"@maks-it.com/webui-contracts": "^0.3.4",
"date-fns": "^4.3.0"
},
"devDependencies": {

View File

@ -1,7 +1,7 @@
{
"name": "maksit-webui",
"private": true,
"version": "0.3.3",
"version": "0.3.4",
"description": "Shared React UI library for MaksIT Certs UI and Vault WebUI",
"workspaces": [
"packages/*"

View File

@ -25,6 +25,9 @@ npm install react react-dom react-router-dom lucide-react @tanstack/react-table
| `EntityScopesSummary` | Entity scope permissions summary |
| `Toast`, `addToast` | Toast notifications |
| `FieldContainer` | Label + validation wrapper for fields |
| `ExternalLoginButtons` | Google/Microsoft (or custom) external login buttons |
| `IdpLoginCallbackHandler` | Headless handler for `?code=` app login OAuth return |
| `MailboxOAuthReturnHandler` | Headless handler for mailbox OAuth multi-protocol return |
## Example — DataTable

View File

@ -1,6 +1,6 @@
{
"name": "@maks-it.com/webui-components",
"version": "0.3.3",
"version": "0.3.4",
"description": "Shared React components for MaksIT WebUI apps",
"type": "module",
"main": "./dist/index.cjs",
@ -33,8 +33,8 @@
"directory": "src/packages/components"
},
"dependencies": {
"@maks-it.com/webui-contracts": "^0.3.3",
"@maks-it.com/webui-core": "^0.3.3"
"@maks-it.com/webui-contracts": "^0.3.4",
"@maks-it.com/webui-core": "^0.3.4"
},
"peerDependencies": {
"@tanstack/react-table": "^8.0.0",

View File

@ -30,3 +30,4 @@ export { Toast as ToastContainer } from './components/Toast'
export { EntityScopesSummary } from './components/Scopes'
export type { EntityScopesSummaryProps } from './components/Scopes'
export { Layout } from './components/Layout'
export * from './oauth'

View File

@ -19,6 +19,7 @@ npm install @maks-it.com/webui-contracts zod
| API errors | `ProblemDetails` |
| Search | `SearchResponseBase`, `SearchEntityScopeEntry` |
| Identity | `LoginRequest` / `LoginRequestSchema`, `LoginResponse`, `RefreshTokenRequest`, `LogoutRequest`, `Claims` |
| OAuth / IdP | `LoginProviderExternal`, `EmailProtocol`, `ServerAuthenticationMethod`, `IdpRedeemLoginResponse`, `MailboxOAuthPending`, mailbox OAuth constants |
| Misc | `TrngResponse`, `RequestModelBase`, `ResponseModelBase` |
## Example

View File

@ -1,6 +1,6 @@
{
"name": "@maks-it.com/webui-contracts",
"version": "0.3.3",
"version": "0.3.4",
"description": "Shared TypeScript contracts for MaksIT WebUI apps",
"type": "module",
"main": "./dist/index.cjs",

View File

@ -32,3 +32,5 @@ export { RefreshTokenRequestSchema } from './identity/login/RefreshTokenRequest'
export type { LogoutRequest } from './identity/logout/LogoutRequest'
export { LogoutRequestSchema } from './identity/logout/LogoutRequest'
export type { LogoutResponse } from './identity/logout/LogoutResponse'
export * from './oauth'

View File

@ -22,7 +22,9 @@ npm install @maks-it.com/webui-core @maks-it.com/webui-contracts @microsoft/sign
| HTTP | `createWebUiHttpClient`, auth interceptors, Problem Details helpers |
| SignalR | `useWebUiHub` |
| Enum / flags | `enumToArr`, `flagsToString`, `hasFlag`, `toggleFlag` |
| PATCH collections | `ENTITY_SCOPES_ARRAY_POLICY`, `HOSTNAMES_ARRAY_POLICY`, `VERSIONS_ARRAY_POLICY` |
| Identity storage | `readIdentity`, `writeIdentity`, `removeIdentity` |
| OAuth / IdP | `startIdpRedirect`, `useIdpLoginCallback`, `collectMailboxOAuthProtocols`, `startMailboxOAuthRedirect`, `useMailboxOAuthReturn` |
## Example — form state

View File

@ -1,6 +1,6 @@
{
"name": "@maks-it.com/webui-core",
"version": "0.3.3",
"version": "0.3.4",
"description": "Shared utilities and hooks for MaksIT WebUI apps",
"type": "module",
"main": "./dist/index.cjs",
@ -34,7 +34,7 @@
"directory": "src/packages/core"
},
"dependencies": {
"@maks-it.com/webui-contracts": "^0.3.3",
"@maks-it.com/webui-contracts": "^0.3.4",
"date-fns": "^4.3.0"
},
"peerDependencies": {

View File

@ -437,7 +437,7 @@ export const deepDelta = <T extends Record<string, unknown>>(
const roleBecameNull = backupRole !== null && formRole === null
if (roleBecameNull) {
const removeItem = {} as DeltaArrayItem<U>
;(removeItem as PlainObject)[idFieldKey] = fid as IdLike
;(removeItem as PlainObject)[getIdFieldForItem(backupItem)] = fid as IdLike
removeItem.operations = { [COLLECTION_ITEM_OPERATION]: PatchOperation.RemoveFromCollection }
arrayDelta.push(removeItem)
continue
@ -446,7 +446,8 @@ export const deepDelta = <T extends Record<string, unknown>>(
// 1.e) Field-level diff
const itemDeltaBase = {} as (PlainObject & OperationBag & { id?: U['id'] })
;(itemDeltaBase as PlainObject)[getIdFieldForItem(formItem)] = fid as IdLike
const identityField = getIdFieldForItem(formItem)
;(itemDeltaBase as PlainObject)[identityField] = fid as IdLike
calculateDelta(
formItem as PlainObject,
@ -454,7 +455,9 @@ export const deepDelta = <T extends Record<string, unknown>>(
itemDeltaBase
)
const hasMeaningfulChanges = Object.keys(itemDeltaBase).some(k => k !== idFieldKey)
const hasMeaningfulChanges = Object.keys(itemDeltaBase).some(
k => k !== identityField && k !== 'operations'
)
if (hasMeaningfulChanges) {
arrayDelta.push(itemDeltaBase as DeltaArrayItem<U>)
}

View File

@ -5,6 +5,7 @@ import {
} from './deepDelta'
import {
ENTITY_SCOPES_ARRAY_POLICY,
HOSTNAMES_ARRAY_POLICY,
VERSIONS_ARRAY_POLICY,
} from './patchCollectionPolicies'
import { deepEqualArrays, deepEqual } from './deepEqual'
@ -17,6 +18,7 @@ export {
deepDelta,
deltaHasOperations,
ENTITY_SCOPES_ARRAY_POLICY,
HOSTNAMES_ARRAY_POLICY,
VERSIONS_ARRAY_POLICY,
deepEqualArrays,
deepEqual,

View File

@ -36,7 +36,17 @@ function entityScopeFallback(item: Record<string, unknown>): string {
}
/**
* Array policy for user/API key `entityScopes` payloads passed to {@link deepDelta}.
* Array policy for account `hostnames` collections (Certs UI `EditAccount`).
*
* Items are matched by `hostname` rather than server `id`; the delta payload uses
* `hostname` as the identity field.
*/
export const HOSTNAMES_ARRAY_POLICY: ArrayPolicy = {
identityKey: 'hostname',
idFieldKey: 'hostname',
}
/**
*
* Uses `_deltaId` in the delta payload when items lack a server `id`, avoiding
* invalid Guid values in the `id` field expected by the API.

View File

@ -15,6 +15,7 @@ import {
deepDelta,
deltaHasOperations,
ENTITY_SCOPES_ARRAY_POLICY,
HOSTNAMES_ARRAY_POLICY,
VERSIONS_ARRAY_POLICY,
deepEqual,
deepEqualArrays,
@ -63,6 +64,7 @@ export {
deepDelta,
deltaHasOperations,
ENTITY_SCOPES_ARRAY_POLICY,
HOSTNAMES_ARRAY_POLICY,
VERSIONS_ARRAY_POLICY,
deepEqual,
deepEqualArrays,

View File

@ -4,3 +4,4 @@ export * from './http'
export * from './signalr'
export { readIdentity, writeIdentity, removeIdentity } from './localStorage/identity'
export { useFormState } from './hooks/useFormState'
export * from './oauth'

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
@echo off
setlocal
pwsh -NoProfile -File "%~dp0engines\release\Invoke-ReleasePackage.ps1" -Mode ha %*
exit /b %ERRORLEVEL%

View File

@ -0,0 +1,4 @@
@echo off
setlocal
pwsh -NoProfile -File "%~dp0engines\release\Invoke-ReleasePackage.ps1" -Mode single %*
exit /b %ERRORLEVEL%

View File

@ -0,0 +1,4 @@
@echo off
setlocal
pwsh -NoProfile -File "%~dp0engines\test\Invoke-TestEngine.ps1" %*
exit /b %ERRORLEVEL%

View File

@ -0,0 +1,4 @@
@echo off
setlocal
pwsh -NoProfile -File "%~dp0engines\uninstall\Invoke-UninstallEngine.ps1" -Mode ha %*
exit /b %ERRORLEVEL%

View File

@ -0,0 +1,4 @@
@echo off
setlocal
pwsh -NoProfile -File "%~dp0engines\uninstall\Invoke-UninstallEngine.ps1" -Mode single %*
exit /b %ERRORLEVEL%

View File

@ -4,23 +4,46 @@
<#
.SYNOPSIS
Plugin-driven release engine entry script.
.PARAMETER DryRun
When set, plugins that declare mutatesRemote in Get-PluginMetadata validate only (no registry push, GitHub release, or cluster deploy).
.PARAMETER Mode
Optional override for HelmSelfDeploy values resolution (single or ha).
When omitted, HelmSelfDeploy.deployMode from scriptSettings.json is used (CI/CD default).
Manual installs: Invoke-ReleasePackage-Single.bat / Invoke-ReleasePackage-HA.bat.
#>
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$srcDir = (Resolve-Path (Join-Path $scriptDir '..\..')).Path
[CmdletBinding()]
param(
[switch]$DryRun,
[ValidateSet('single', 'ha')]
[string]$Mode
)
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
$srcDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
Import-EngineModules -Engine Release
$settings = Get-ScriptSettings -ScriptDir $scriptDir
$settings = Get-ScriptSettings -ScriptDir $PSScriptRoot
$resolveModeParams = @{ Settings = $settings }
if ($PSBoundParameters.ContainsKey('Mode')) {
$resolveModeParams['ModeOverride'] = $Mode
}
$deployMode = Get-DeployModeFromSettings @resolveModeParams
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
Write-Log -Level 'STEP' -Message '=================================================='
Write-Log -Level 'STEP' -Message 'RELEASE ENGINE'
Write-Log -Level 'STEP' -Message "RELEASE ENGINE (deploy mode: $deployMode)"
Write-Log -Level 'STEP' -Message '=================================================='
$plugins = $configuredPlugins
$engineContext = New-EngineContext -Plugins $plugins -ScriptDir $scriptDir -SrcDir $srcDir -Settings $settings
$engineContext = New-EngineContext -Plugins $plugins -ScriptDir $PSScriptRoot -SrcDir $srcDir -Settings $settings -DryRun:$DryRun -DeployMode $deployMode
Write-Log -Level 'OK' -Message 'All pre-flight checks passed!'
$sharedPluginSettings = $engineContext
@ -34,15 +57,15 @@ else {
for ($pluginIndex = 0; $pluginIndex -lt $plugins.Count; $pluginIndex++) {
$plugin = $plugins[$pluginIndex]
if ((Test-IsPublishPlugin -Plugin $plugin) -and -not $releaseStageInitialized) {
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $scriptDir -WriteLogs:$false) {
if ((Test-IsPublishPlugin -Plugin $plugin -EngineDirectory $PSScriptRoot) -and -not $releaseStageInitialized) {
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -WriteLogs:$false) {
$remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)])
Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory -Version $engineContext.version
$releaseStageInitialized = $true
}
}
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $scriptDir -ContinueOnError:$false
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -ContinueOnError:$false
if (-not $pluginSucceeded) {
$releaseHadPluginFailures = $true
break
@ -52,7 +75,7 @@ else {
if (-not $releaseStageInitialized) {
$noReleasePluginsLogLevel = if ($engineContext.isNonReleaseBranch) { 'INFO' } else { 'WARN' }
Write-Log -Level $noReleasePluginsLogLevel -Message 'No release-stage initialization ran (no enabled publish plugins reached, or none runnable).'
Write-Log -Level $noReleasePluginsLogLevel -Message 'No release-stage initialization ran (no enabled remote-mutation plugins reached, or none runnable).'
}
Write-Log -Level 'OK' -Message '=================================================='
@ -65,6 +88,9 @@ elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -
elseif ($engineContext.isNonReleaseBranch) {
Write-Log -Level 'OK' -Message 'NON-RELEASE RUN COMPLETE'
}
elseif ($engineContext.dryRun) {
Write-Log -Level 'OK' -Message 'DRY RUN COMPLETE'
}
else {
Write-Log -Level 'OK' -Message 'RELEASE COMPLETE'
}

View File

@ -0,0 +1,70 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"title": "Release Package Script Settings",
"description": "maksit-webui: npm workspace publish (@maks-it.com/webui-*). Semver from NpmReleaseVersion (src/package.json).",
"plugins": [
{
"name": "NpmReleaseVersion",
"stageLabel": "build",
"enabled": true,
"packageJsonPath": "..\\..\\..\\src\\package.json",
"syncWorkspaceVersions": true
},
{
"name": "NpmBuild",
"stageLabel": "build",
"enabled": true,
"workspaceRoot": "..\\..\\..\\src",
"useCi": true,
"buildScript": "build"
},
{
"name": "ReleasePublishGuard",
"stageLabel": "release",
"enabled": true,
"branches": [
"main"
],
"requireExactTagOnHead": true,
"tagVersionMustMatchReleaseVersion": true,
"whenRequirementsNotMet": "skip",
"requireCleanWorkingTree": false,
"ensureTagOnRemote": true,
"remoteName": "origin"
},
{
"name": "NpmPack",
"stageLabel": "release",
"enabled": true,
"workspaceRoot": "..\\..\\..\\src",
"publishOrder": [
"@maks-it.com/webui-contracts",
"@maks-it.com/webui-core",
"@maks-it.com/webui-components"
]
},
{
"name": "GitHub",
"stageLabel": "release",
"enabled": true,
"githubSecret": "GitHub",
"repository": "https://github.com/MAKS-IT-COM/maksit-webui",
"releaseNotesFile": "..\\..\\..\\CHANGELOG.md",
"releaseTitlePattern": "Release {version}"
},
{
"name": "NpmPublish",
"stageLabel": "release",
"enabled": true,
"npmSecret": "Npm",
"registry": "https://registry.npmjs.org",
"access": "public",
"workspaceRoot": "..\\..\\..\\src",
"publishOrder": [
"@maks-it.com/webui-contracts",
"@maks-it.com/webui-core",
"@maks-it.com/webui-components"
]
}
]
}

View File

@ -1,17 +1,17 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"title": "Run Tests Script Settings",
"description": "maksit-webui: plugin-driven Jest tests and coverage badges.",
"description": "maksit-webui: Jest tests and coverage badges for README assets.",
"paths": {
"badgesDir": "..\\..\\..\\..\\assets\\badges"
"badgesDir": "..\\..\\..\\assets\\badges"
},
"plugins": [
{
"name": "NpmJestTest",
"stageLabel": "test",
"enabled": true,
"workspaceRoot": "..\\..\\..\\..\\src",
"testScript": "test",
"workspaceRoot": "..\\..\\..\\src",
"testScript": "test:coverage",
"coverageDirectory": "coverage"
},
{
@ -25,7 +25,7 @@
"name": "CoverageBadges",
"stageLabel": "report",
"enabled": true,
"badgesDir": "..\\..\\..\\..\\assets\\badges",
"badgesDir": "..\\..\\..\\assets\\badges",
"badges": [
{
"name": "coverage-lines.svg",
@ -52,12 +52,5 @@
"red": 0
}
}
],
"_comments": {
"plugins": {
"NpmJestTest": "Runs npm test with Jest coverage in workspaceRoot. Publishes coverage metrics on shared context.",
"QualityGate": "Reads shared context metrics; set coverageThreshold > 0 to enforce minimum line coverage.",
"CoverageBadges": "Writes SVG badges from shared context metrics into badgesDir."
}
}
]
}

View File

@ -0,0 +1,68 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Plugin-driven Helm uninstall engine entry script.
.PARAMETER Mode
Optional override when inheriting HelmSelfDeploy settings (single or ha).
When omitted, HelmSelfDeploy.deployMode from engines/release/scriptSettings.json is used.
#>
[CmdletBinding()]
param(
[ValidateSet('single', 'ha')]
[string]$Mode
)
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
$srcDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
Import-EngineModules -Engine Test
$settings = Get-ScriptSettings -ScriptDir $PSScriptRoot
$releaseSettings = Get-ScriptSettings -ScriptDir (Join-Path $PSScriptRoot '..\release')
$resolveModeParams = @{ Settings = $releaseSettings }
if ($PSBoundParameters.ContainsKey('Mode')) {
$resolveModeParams['ModeOverride'] = $Mode
}
$deployMode = Get-DeployModeFromSettings @resolveModeParams
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
Write-Log -Level 'STEP' -Message '=================================================='
Write-Log -Level 'STEP' -Message "HELM UNINSTALL ENGINE (deploy mode: $deployMode)"
Write-Log -Level 'STEP' -Message '=================================================='
$engineContext = New-EngineContext -ScriptDir $PSScriptRoot -SrcDir $srcDir -Settings $settings -DeployMode $deployMode
if ($configuredPlugins.Count -eq 0) {
Write-Log -Level 'WARN' -Message 'No plugins configured in scriptSettings.json.'
exit 0
}
$uninstallHadPluginFailures = $false
foreach ($plugin in $configuredPlugins) {
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -EngineDirectory $PSScriptRoot -ContinueOnError:$false
if (-not $pluginSucceeded) {
$uninstallHadPluginFailures = $true
break
}
}
Write-Log -Level 'OK' -Message '=================================================='
if ($uninstallHadPluginFailures) {
Write-Log -Level 'ERROR' -Message 'UNINSTALL FAILED'
}
else {
Write-Log -Level 'OK' -Message 'UNINSTALL COMPLETE'
}
Write-Log -Level 'OK' -Message '=================================================='
if ($uninstallHadPluginFailures) {
exit 1
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"title": "Helm Uninstall Script Settings",
"description": "HelmUninstall inherits namespace, charts, and bgpServices from HelmSelfDeploy in engines/release/scriptSettings.json unless inheritFromRelease is false.",
"plugins": [
{
"name": "HelmUninstall",
"stageLabel": "uninstall",
"enabled": false
}
]
}

View File

@ -0,0 +1,189 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Shared helpers for DockerCompose and PodmanCompose release plugins.
#>
$containerEngineSupportModulePath = Join-Path $PSScriptRoot 'ContainerEngineSupport.psm1'
if (Test-Path $containerEngineSupportModulePath -PathType Leaf) {
Import-Module $containerEngineSupportModulePath -Force -Global
}
function Get-ComposeInvocation {
param(
[Parameter(Mandatory = $false)]
[psobject]$SharedSettings = $null
)
if ($null -ne $SharedSettings -and $SharedSettings.PSObject.Properties.Name -contains 'containerEngineProbeCompleted' -and $SharedSettings.containerEngineProbeCompleted) {
return Get-SharedComposeInvocation -SharedSettings $SharedSettings
}
if (Test-ComposeSubcommand 'docker') {
return @{ Executable = 'docker'; BaseArgs = @('compose'); Engine = 'docker' }
}
if (Test-ComposeSubcommand 'podman') {
return @{ Executable = 'podman'; BaseArgs = @('compose'); Engine = 'podman' }
}
if (Get-Command docker-compose -ErrorAction SilentlyContinue) {
return @{ Executable = 'docker-compose'; BaseArgs = @(); Engine = 'docker-compose' }
}
throw 'Compose is not available. Run ContainerEngineProbe first, or install docker compose / podman compose.'
}
function Resolve-ComposeProjectDirectory {
param(
[Parameter(Mandatory = $true)]
[string]$ScriptDir,
[Parameter(Mandatory = $true)]
[string]$ComposePath
)
$projectDir = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $ComposePath))
if (-not (Test-Path $projectDir -PathType Container)) {
throw "Compose project directory not found: $projectDir"
}
return $projectDir
}
function Get-ComposeFileArguments {
param(
[Parameter(Mandatory = $true)]
[string[]]$ComposeFiles
)
if ($ComposeFiles.Count -eq 0) {
throw 'At least one compose file is required.'
}
$args = New-Object System.Collections.Generic.List[string]
foreach ($fileName in $ComposeFiles) {
if ([string]::IsNullOrWhiteSpace($fileName)) {
continue
}
$args.Add('-f')
$args.Add([string]$fileName)
}
if ($args.Count -eq 0) {
throw 'At least one non-empty compose file name is required.'
}
return @($args)
}
function Invoke-ComposeConfigValidation {
param(
[Parameter(Mandatory = $true)]
[string]$ProjectDir,
[Parameter(Mandatory = $true)]
[string[]]$ComposeFiles,
[Parameter(Mandatory = $false)]
[psobject]$SharedSettings = $null
)
$compose = Get-ComposeInvocation -SharedSettings $SharedSettings
$fileArgs = Get-ComposeFileArguments -ComposeFiles $ComposeFiles
foreach ($fileName in $ComposeFiles) {
$composeFilePath = Join-Path $ProjectDir $fileName
if (-not (Test-Path $composeFilePath -PathType Leaf)) {
throw "Compose file not found: $composeFilePath"
}
}
$commandArgs = @($compose.BaseArgs) + $fileArgs + @('config', '--quiet')
Write-Log -Level 'INFO' -Message "Validating Compose project at $ProjectDir ($($ComposeFiles -join ', ')) via $($compose.Engine) ..."
Push-Location $ProjectDir
try {
$output = & $compose.Executable @commandArgs 2>&1
if ($LASTEXITCODE -ne 0) {
throw "$($compose.Engine) compose config failed: $($output | Out-String)"
}
}
finally {
Pop-Location
}
Write-Log -Level 'OK' -Message ' Compose configuration is valid.'
}
function Invoke-ComposeStackPlugin {
param(
[Parameter(Mandatory = $true)]
$Settings,
[Parameter(Mandatory = $true)]
[ValidateSet('docker', 'podman')]
[string]$RequiredEngine,
[Parameter(Mandatory = $true)]
[string]$PluginName
)
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'ContainerEngineSupport' -RequiredCommand 'Test-ShouldRunContainerEnginePlugin'
$pluginSettings = $Settings
$shared = $Settings.context
$scriptDir = [string]$shared.scriptDir
if (-not (Test-ShouldRunContainerEnginePlugin -RequiredEngine $RequiredEngine -SharedSettings $shared -PluginName $PluginName)) {
return
}
if ([string]::IsNullOrWhiteSpace($pluginSettings.composePath)) {
throw "$PluginName requires 'composePath' (directory with docker-compose.yml, relative to engines/release)."
}
$composeFiles = @('docker-compose.yml', 'docker-compose.override.yml')
if ($pluginSettings.PSObject.Properties.Name -contains 'composeFiles' -and $null -ne $pluginSettings.composeFiles) {
$composeFiles = @($pluginSettings.composeFiles | ForEach-Object { [string]$_ })
}
$projectDir = Resolve-ComposeProjectDirectory -ScriptDir $scriptDir -ComposePath ([string]$pluginSettings.composePath)
Invoke-ComposeConfigValidation -ProjectDir $projectDir -ComposeFiles $composeFiles -SharedSettings $shared
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
if ($dryRun) {
Write-Log -Level 'INFO' -Message 'Dry run: compose up/build skipped.'
return
}
$compose = Get-ComposeInvocation -SharedSettings $shared
$fileArgs = Get-ComposeFileArguments -ComposeFiles $composeFiles
$upArgs = @($compose.BaseArgs) + $fileArgs + @('up', '--build', '-d')
Write-Log -Level 'STEP' -Message "Starting Compose stack at $projectDir via $($compose.Engine) ..."
Push-Location $projectDir
try {
$output = & $compose.Executable @upArgs 2>&1
if ($LASTEXITCODE -ne 0) {
throw "$($compose.Engine) compose up failed: $($output | Out-String)"
}
}
finally {
Pop-Location
}
Write-Log -Level 'OK' -Message " Compose stack started via $($compose.Engine)."
$shared | Add-Member -NotePropertyName composeDeployCompleted -NotePropertyValue $true -Force
}
Export-ModuleMember -Function `
Get-ComposeInvocation, `
Resolve-ComposeProjectDirectory, `
Get-ComposeFileArguments, `
Invoke-ComposeConfigValidation, `
Invoke-ComposeStackPlugin

View File

@ -0,0 +1,320 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Container engine probe, shared pipeline state, and CLI helpers.
.DESCRIPTION
ContainerEngineProbe sets mandatory shared context fields. Engine-specific plugins call
Test-ShouldRunContainerEnginePlugin before running (safe skip when another engine is active).
#>
function Get-ContainerRemoteConnection {
$containerHost = [Environment]::GetEnvironmentVariable('CONTAINER_HOST')
$dockerHost = [Environment]::GetEnvironmentVariable('DOCKER_HOST')
return @{
ContainerHost = if ([string]::IsNullOrWhiteSpace($containerHost)) { $null } else { $containerHost.Trim() }
DockerHost = if ([string]::IsNullOrWhiteSpace($dockerHost)) { $null } else { $dockerHost.Trim() }
}
}
function Test-ContainerEngineCli {
param(
[Parameter(Mandatory = $true)]
[string]$Executable
)
if (-not (Get-Command $Executable -ErrorAction SilentlyContinue)) {
return $false
}
$null = & $Executable version 2>$null
return $LASTEXITCODE -eq 0
}
function Test-ComposeSubcommand {
param(
[Parameter(Mandatory = $true)]
[string]$Executable
)
if (-not (Get-Command $Executable -ErrorAction SilentlyContinue)) {
return $false
}
$null = & $Executable compose version 2>$null
return $LASTEXITCODE -eq 0
}
function Resolve-ActiveContainerEngineState {
$remote = Get-ContainerRemoteConnection
$forced = [Environment]::GetEnvironmentVariable('MAKSIT_CONTAINER_ENGINE')
if (-not [string]::IsNullOrWhiteSpace($forced)) {
$forced = $forced.Trim().ToLowerInvariant()
}
$dockerEngineAvailable = Test-ContainerEngineCli 'docker'
$podmanEngineAvailable = Test-ContainerEngineCli 'podman'
$invocation = $null
if ($forced -eq 'docker') {
if (-not $dockerEngineAvailable) {
throw 'MAKSIT_CONTAINER_ENGINE=docker but the docker CLI is not available.'
}
$invocation = New-ContainerEngineInvocation -Executable 'docker' -Engine 'docker' -Remote $remote
}
elseif ($forced -eq 'podman') {
if (-not $podmanEngineAvailable) {
throw 'MAKSIT_CONTAINER_ENGINE=podman but the podman CLI is not available.'
}
$invocation = New-ContainerEngineInvocation -Executable 'podman' -Engine 'podman' -Remote $remote
}
elseif ($remote.ContainerHost) {
if ($podmanEngineAvailable) {
$invocation = New-ContainerEngineInvocation -Executable 'podman' -Engine 'podman' -Remote $remote -UsesRemote $true
}
elseif ($dockerEngineAvailable) {
$invocation = New-ContainerEngineInvocation -Executable 'docker' -Engine 'docker' -Remote $remote -UsesRemote $true
}
else {
throw 'CONTAINER_HOST is set but neither podman nor docker CLI is on PATH.'
}
}
elseif ($dockerEngineAvailable) {
$invocation = New-ContainerEngineInvocation -Executable 'docker' -Engine 'docker' -Remote $remote
}
elseif ($podmanEngineAvailable) {
$invocation = New-ContainerEngineInvocation -Executable 'podman' -Engine 'podman' -Remote $remote
}
elseif ($remote.DockerHost -and $dockerEngineAvailable) {
$invocation = New-ContainerEngineInvocation -Executable 'docker' -Engine 'docker' -Remote $remote -UsesRemote $true
}
if ($null -eq $invocation) {
throw 'No container engine found. Install Docker Desktop or Podman locally, or set CONTAINER_HOST to a remote Podman API.'
}
$composeInvocation = Resolve-ComposeInvocationForEngine -Engine $invocation.Engine -DockerEngineAvailable $dockerEngineAvailable -PodmanEngineAvailable $podmanEngineAvailable
return @{
ActiveContainerEngine = [string]$invocation.Engine
DockerEngineAvailable = [bool]$dockerEngineAvailable
PodmanEngineAvailable = [bool]$podmanEngineAvailable
ContainerExecutable = [string]$invocation.Executable
ContainerRemoteLabel = $invocation.RemoteLabel
ComposeExecutable = [string]$composeInvocation.Executable
ComposeBaseArgs = @($composeInvocation.BaseArgs)
ComposeEngine = [string]$composeInvocation.Engine
}
}
function Resolve-ComposeInvocationForEngine {
param(
[Parameter(Mandatory = $true)]
[string]$Engine,
[Parameter(Mandatory = $true)]
[bool]$DockerEngineAvailable,
[Parameter(Mandatory = $true)]
[bool]$PodmanEngineAvailable
)
if ($Engine -eq 'docker' -and $DockerEngineAvailable -and (Test-ComposeSubcommand 'docker')) {
return @{ Executable = 'docker'; BaseArgs = @('compose'); Engine = 'docker' }
}
if ($Engine -eq 'podman' -and $PodmanEngineAvailable -and (Test-ComposeSubcommand 'podman')) {
return @{ Executable = 'podman'; BaseArgs = @('compose'); Engine = 'podman' }
}
if ($Engine -eq 'docker' -and (Get-Command docker-compose -ErrorAction SilentlyContinue)) {
return @{ Executable = 'docker-compose'; BaseArgs = @(); Engine = 'docker-compose' }
}
throw "Compose is not available for active engine '$Engine'. Install docker compose or podman compose."
}
function New-ContainerEngineInvocation {
param(
[Parameter(Mandatory = $true)]
[string]$Executable,
[Parameter(Mandatory = $true)]
[string]$Engine,
[Parameter(Mandatory = $true)]
[hashtable]$Remote,
[bool]$UsesRemote = $false
)
$remoteLabel = $null
if ($Remote.ContainerHost) {
$remoteLabel = "CONTAINER_HOST=$($Remote.ContainerHost)"
}
elseif ($Remote.DockerHost) {
$remoteLabel = "DOCKER_HOST=$($Remote.DockerHost)"
}
return @{
Executable = $Executable
Engine = $Engine
UsesRemote = $UsesRemote -or [bool]$Remote.ContainerHost -or [bool]$Remote.DockerHost
RemoteLabel = $remoteLabel
}
}
function Set-ContainerEngineSharedState {
param(
[Parameter(Mandatory = $true)]
[psobject]$SharedSettings,
[Parameter(Mandatory = $true)]
[hashtable]$State
)
$SharedSettings | Add-Member -NotePropertyName containerEngineProbeCompleted -NotePropertyValue $true -Force
$SharedSettings | Add-Member -NotePropertyName activeContainerEngine -NotePropertyValue $State.ActiveContainerEngine -Force
$SharedSettings | Add-Member -NotePropertyName dockerEngineAvailable -NotePropertyValue $State.DockerEngineAvailable -Force
$SharedSettings | Add-Member -NotePropertyName podmanEngineAvailable -NotePropertyValue $State.PodmanEngineAvailable -Force
$SharedSettings | Add-Member -NotePropertyName containerExecutable -NotePropertyValue $State.ContainerExecutable -Force
if ($State.ContainerRemoteLabel) {
$SharedSettings | Add-Member -NotePropertyName containerRemoteLabel -NotePropertyValue $State.ContainerRemoteLabel -Force
}
$SharedSettings | Add-Member -NotePropertyName composeExecutable -NotePropertyValue $State.ComposeExecutable -Force
$SharedSettings | Add-Member -NotePropertyName composeBaseArgs -NotePropertyValue $State.ComposeBaseArgs -Force
$SharedSettings | Add-Member -NotePropertyName composeEngine -NotePropertyValue $State.ComposeEngine -Force
}
function Assert-ContainerEngineProbeCompleted {
param(
[Parameter(Mandatory = $true)]
[psobject]$SharedSettings
)
if (-not ($SharedSettings.PSObject.Properties.Name -contains 'containerEngineProbeCompleted') -or -not $SharedSettings.containerEngineProbeCompleted) {
throw 'ContainerEngineProbe must run before container engine plugins. Add ContainerEngineProbe as the first enabled plugin in scriptSettings.json.'
}
}
function Test-ShouldRunContainerEnginePlugin {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('docker', 'podman')]
[string]$RequiredEngine,
[Parameter(Mandatory = $true)]
[psobject]$SharedSettings,
[Parameter(Mandatory = $true)]
[string]$PluginName
)
Assert-ContainerEngineProbeCompleted -SharedSettings $SharedSettings
if ([string]$SharedSettings.activeContainerEngine -eq $RequiredEngine) {
return $true
}
$engineAvailable = if ($RequiredEngine -eq 'docker') {
[bool]$SharedSettings.dockerEngineAvailable
}
else {
[bool]$SharedSettings.podmanEngineAvailable
}
Write-Log -Level 'INFO' -Message "Skipping $PluginName (requires $RequiredEngine; active engine: $($SharedSettings.activeContainerEngine); ${RequiredEngine} CLI available: $engineAvailable)."
return $false
}
function Get-SharedContainerEngineInvocation {
param(
[Parameter(Mandatory = $true)]
[psobject]$SharedSettings
)
Assert-ContainerEngineProbeCompleted -SharedSettings $SharedSettings
return @{
Executable = [string]$SharedSettings.containerExecutable
Engine = [string]$SharedSettings.activeContainerEngine
RemoteLabel = if ($SharedSettings.PSObject.Properties.Name -contains 'containerRemoteLabel') { [string]$SharedSettings.containerRemoteLabel } else { $null }
}
}
function Get-SharedComposeInvocation {
param(
[Parameter(Mandatory = $true)]
[psobject]$SharedSettings
)
Assert-ContainerEngineProbeCompleted -SharedSettings $SharedSettings
return @{
Executable = [string]$SharedSettings.composeExecutable
BaseArgs = @($SharedSettings.composeBaseArgs)
Engine = [string]$SharedSettings.composeEngine
}
}
function Get-ContainerEngineDisplayName {
param(
[Parameter(Mandatory = $true)]
[hashtable]$Engine
)
if ($Engine.RemoteLabel) {
return "$($Engine.Engine) ($($Engine.RemoteLabel))"
}
return [string]$Engine.Engine
}
function Invoke-ContainerEngine {
param(
[Parameter(Mandatory = $true)]
[hashtable]$Engine,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$ArgumentList
)
& $Engine.Executable @ArgumentList
return $LASTEXITCODE
}
function Invoke-ContainerEngineWithInput {
param(
[Parameter(Mandatory = $true)]
[hashtable]$Engine,
[Parameter(Mandatory = $true)]
[string]$InputObject,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$ArgumentList
)
$InputObject | & $Engine.Executable @ArgumentList 2>&1
}
Export-ModuleMember -Function `
Get-ContainerRemoteConnection, `
Resolve-ActiveContainerEngineState, `
Set-ContainerEngineSharedState, `
Assert-ContainerEngineProbeCompleted, `
Test-ShouldRunContainerEnginePlugin, `
Get-SharedContainerEngineInvocation, `
Get-SharedComposeInvocation, `
Get-ContainerEngineDisplayName, `
Invoke-ContainerEngine, `
Invoke-ContainerEngineWithInput, `
Test-ContainerEngineCli, `
Test-ComposeSubcommand

View File

@ -3,52 +3,15 @@
<#
.SYNOPSIS
.NET Docker publish plugin — build and push container images for .NET apps.
.DESCRIPTION
Logs in with credentials from a Base64-encoded username:password environment variable,
builds each configured image once, then tags and pushes: bare semver from DotNetReleaseVersion
(e.g. 3.3.4), v-prefixed alias (v3.3.4) when different, optional exact shared.tag if it differs,
and optional latest.
Release image tags align with shared.version (same bare semver as Helm chart/OCI when used together); not from Chart.yaml.
Shared container image build/push logic for DockerImagePush and PodmanImagePush.
#>
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
}
$containerEngineSupportModulePath = Join-Path $PSScriptRoot 'ContainerEngineSupport.psm1'
if (Test-Path $containerEngineSupportModulePath -PathType Leaf) {
Import-Module $containerEngineSupportModulePath -Force -Global
}
function Get-RegistryCredentialsFromEnv {
param(
[Parameter(Mandatory = $true)]
[string]$EnvVarName
)
$raw = [Environment]::GetEnvironmentVariable($EnvVarName)
if ([string]::IsNullOrWhiteSpace($raw)) {
throw "Environment variable '$EnvVarName' is not set."
}
try {
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($raw))
}
catch {
throw "Failed to decode '$EnvVarName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
}
$parts = $decoded -split ':', 2
if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) {
throw "Decoded '$EnvVarName' must be in the form 'username:password'."
}
return @{ User = $parts[0]; Password = $parts[1] }
}
function Set-EnvVersionValue {
function Set-ContainerImageEnvVersionValue {
param(
[Parameter(Mandatory = $true)]
[string]$FilePath,
@ -69,48 +32,52 @@ function Set-EnvVersionValue {
Set-Content -LiteralPath $FilePath -Value $content -NoNewline
}
function Invoke-Plugin {
function Invoke-ContainerImagePush {
param(
[Parameter(Mandatory = $true)]
$Settings
$Settings,
[Parameter(Mandatory = $true)]
[string]$PluginDisplayName
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'ContainerEngineSupport' -RequiredCommand 'Get-SharedContainerEngineInvocation'
$pluginSettings = $Settings
$shared = $Settings.context
Assert-Command docker
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
$engine = Get-SharedContainerEngineInvocation -SharedSettings $shared
$engineName = Get-ContainerEngineDisplayName -Engine $engine
if ([string]::IsNullOrWhiteSpace($pluginSettings.registryUrl)) {
throw "DotNetDockerPush plugin requires 'registryUrl' (registry hostname, no scheme)."
throw "$PluginDisplayName requires 'registryUrl' (registry hostname, no scheme)."
}
if ([string]::IsNullOrWhiteSpace($pluginSettings.credentialsEnvVar)) {
throw "DotNetDockerPush plugin requires 'credentialsEnvVar' (name of env var holding base64 username:password)."
$containerRegistrySecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'containerRegistrySecret'
if ([string]::IsNullOrWhiteSpace($containerRegistrySecret)) {
throw "$PluginDisplayName requires 'containerRegistrySecret' (logical secret name, e.g. ContainerRegistry)."
}
if ([string]::IsNullOrWhiteSpace($pluginSettings.projectName)) {
throw "DotNetDockerPush plugin requires 'projectName' (image path segment after registry)."
throw "$PluginDisplayName requires 'projectName' (image path segment after registry)."
}
if ([string]::IsNullOrWhiteSpace($pluginSettings.contextPath)) {
throw "DotNetDockerPush plugin requires 'contextPath' (Docker build context, relative to engines/release folder)."
throw "$PluginDisplayName requires 'contextPath' (build context, relative to engines/release folder)."
}
if (-not $pluginSettings.images -or @($pluginSettings.images).Count -eq 0) {
throw "DotNetDockerPush plugin requires a non-empty 'images' array with 'service' and 'dockerfile' per entry."
throw "$PluginDisplayName requires a non-empty 'images' array with 'service' and 'dockerfile' per entry."
}
$scriptDir = $shared.scriptDir
$contextPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$pluginSettings.contextPath)))
if (-not (Test-Path $contextPath -PathType Container)) {
throw "Docker context directory not found: $contextPath"
throw "Build context directory not found: $contextPath"
}
$registryUrl = [string]$pluginSettings.registryUrl.TrimEnd('/')
$creds = Get-RegistryCredentialsFromEnv -EnvVarName ([string]$pluginSettings.credentialsEnvVar)
$bareVersion = $null
if ($shared.PSObject.Properties.Name -contains 'version' -and -not [string]::IsNullOrWhiteSpace([string]$shared.version)) {
@ -120,9 +87,37 @@ function Invoke-Plugin {
$bareVersion = ([string]$shared.tag).Trim() -replace '^[vV]', ''
}
if ([string]::IsNullOrWhiteSpace($bareVersion)) {
throw "DotNetDockerPush: could not derive version tag (need shared.version from DotNetReleaseVersion or shared.tag)."
throw "${PluginDisplayName}: could not derive version tag (need shared.version from a version plugin or shared.tag)."
}
if ($dryRun) {
foreach ($img in @($pluginSettings.images)) {
if ($null -eq $img.service -or $null -eq $img.dockerfile) {
throw "Each images[] entry must define 'service' and 'dockerfile'."
}
$service = [string]$img.service
$dockerfileRel = [string]$img.dockerfile
$imgContextPath = $contextPath
if ($img.PSObject.Properties.Name -contains 'contextPath' -and -not [string]::IsNullOrWhiteSpace([string]$img.contextPath)) {
$imgContextPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$img.contextPath)))
}
$dockerfilePath = [System.IO.Path]::GetFullPath((Join-Path $imgContextPath $dockerfileRel))
if (-not (Test-Path $dockerfilePath -PathType Leaf)) {
throw "Dockerfile not found: $dockerfilePath"
}
$plannedRef = "$registryUrl/$($pluginSettings.projectName)/${service}:$bareVersion"
Write-Log -Level 'INFO' -Message "Dry run: would build and push $plannedRef via $engineName (dockerfile: $dockerfilePath)"
}
Write-Log -Level 'OK' -Message " $PluginDisplayName dry run completed."
return
}
$creds = Get-RegistryCredentialsFromRuntime -SecretName $containerRegistrySecret -SharedSettings $shared
$imageTags = New-Object System.Collections.Generic.List[string]
function Add-ImageTag([System.Collections.Generic.List[string]]$List, [string]$Tag) {
if ([string]::IsNullOrWhiteSpace($Tag)) { return }
@ -138,10 +133,10 @@ function Invoke-Plugin {
Add-ImageTag $imageTags 'latest'
}
Write-Log -Level "STEP" -Message "Docker login to $registryUrl..."
$loginResult = $creds.Password | docker login $registryUrl -u $creds.User --password-stdin 2>&1
Write-Log -Level 'STEP' -Message "Container login to $registryUrl via $engineName..."
$loginResult = Invoke-ContainerEngineWithInput -Engine $engine -InputObject $creds.Password login $registryUrl -u $creds.User --password-stdin
if ($LASTEXITCODE -ne 0 -or ($loginResult -notmatch 'Login Succeeded')) {
throw "Docker login failed for ${registryUrl}: $loginResult"
throw "Container login failed for ${registryUrl}: $loginResult"
}
try {
@ -157,7 +152,7 @@ function Invoke-Plugin {
if ($img.PSObject.Properties.Name -contains 'contextPath' -and -not [string]::IsNullOrWhiteSpace([string]$img.contextPath)) {
$imgContextPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$img.contextPath)))
if (-not (Test-Path $imgContextPath -PathType Container)) {
throw "Docker context directory not found for image '$service': $imgContextPath"
throw "Build context directory not found for image '$service': $imgContextPath"
}
}
@ -182,7 +177,7 @@ function Invoke-Plugin {
$backupPath = "$envFilePath.repoutils.bak"
Copy-Item -LiteralPath $envFilePath -Destination $backupPath -Force
$versionEnvFiles += [pscustomobject]@{
FilePath = $envFilePath
FilePath = $envFilePath
BackupPath = $backupPath
}
}
@ -190,33 +185,33 @@ function Invoke-Plugin {
try {
foreach ($envFile in $versionEnvFiles) {
Write-Log -Level "INFO" -Message "Temporarily setting VITE_APP_VERSION=$bareVersion in $($envFile.FilePath)"
Set-EnvVersionValue -FilePath $envFile.FilePath -Version $bareVersion
Write-Log -Level 'INFO' -Message "Temporarily setting VITE_APP_VERSION=$bareVersion in $($envFile.FilePath)"
Set-ContainerImageEnvVersionValue -FilePath $envFile.FilePath -Version $bareVersion
}
$primaryRef = "${baseName}:$($imageTags[0])"
Write-Log -Level "STEP" -Message "Building $primaryRef ..."
docker build -t $primaryRef -f $dockerfilePath $imgContextPath
if ($LASTEXITCODE -ne 0) {
throw "Docker build failed for $primaryRef"
Write-Log -Level 'STEP' -Message "Building $primaryRef via $engineName ..."
$buildExitCode = Invoke-ContainerEngine -Engine $engine build -t $primaryRef -f $dockerfilePath $imgContextPath
if ($buildExitCode -ne 0) {
throw "Container build failed for $primaryRef"
}
Write-Log -Level "STEP" -Message "Pushing $primaryRef ..."
docker push $primaryRef
if ($LASTEXITCODE -ne 0) {
throw "Docker push failed for $primaryRef"
Write-Log -Level 'STEP' -Message "Pushing $primaryRef via $engineName ..."
$pushExitCode = Invoke-ContainerEngine -Engine $engine push $primaryRef
if ($pushExitCode -ne 0) {
throw "Container push failed for $primaryRef"
}
for ($ti = 1; $ti -lt $imageTags.Count; $ti++) {
$aliasRef = "${baseName}:$($imageTags[$ti])"
Write-Log -Level "STEP" -Message "Tagging and pushing $aliasRef ..."
docker tag $primaryRef $aliasRef
if ($LASTEXITCODE -ne 0) {
throw "Docker tag failed: $primaryRef -> $aliasRef"
Write-Log -Level 'STEP' -Message "Tagging and pushing $aliasRef via $engineName ..."
$tagExitCode = Invoke-ContainerEngine -Engine $engine tag $primaryRef $aliasRef
if ($tagExitCode -ne 0) {
throw "Container tag failed: $primaryRef -> $aliasRef"
}
docker push $aliasRef
if ($LASTEXITCODE -ne 0) {
throw "Docker push failed for $aliasRef"
$aliasPushExitCode = Invoke-ContainerEngine -Engine $engine push $aliasRef
if ($aliasPushExitCode -ne 0) {
throw "Container push failed for $aliasRef"
}
}
}
@ -235,11 +230,11 @@ function Invoke-Plugin {
}
}
finally {
docker logout $registryUrl 2>&1 | Out-Null
$null = Invoke-ContainerEngine -Engine $engine logout $registryUrl
}
Write-Log -Level "OK" -Message " Docker push completed."
Write-Log -Level 'OK' -Message " $PluginDisplayName completed."
$shared | Add-Member -NotePropertyName publishCompleted -NotePropertyValue $true -Force
}
Export-ModuleMember -Function Invoke-Plugin
Export-ModuleMember -Function Invoke-ContainerImagePush

View File

@ -9,6 +9,7 @@
Used by New-EngineContext and version plugins:
- DotNetReleaseVersion plugin -> projectFiles (.csproj <Version>)
- NpmReleaseVersion plugin -> packageJsonPath (package.json version)
- FileReleaseVersion plugin -> versionFilePath (repo-root VERSION file)
#>
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
@ -190,6 +191,72 @@ function Resolve-NpmReleaseVersion {
}
}
function Get-VersionFileSemver {
param(
[Parameter(Mandatory = $true)]
[string]$VersionFilePath
)
if (-not (Test-Path $VersionFilePath -PathType Leaf)) {
Write-Error "FileReleaseVersion: VERSION file not found at: $VersionFilePath"
exit 1
}
$version = (Get-Content -Path $VersionFilePath -Raw -Encoding UTF8).Trim()
if ([string]::IsNullOrWhiteSpace($version)) {
Write-Error "FileReleaseVersion: VERSION file is empty at: $VersionFilePath"
exit 1
}
$version = $version -replace '^[vV]', ''
if ($version -notmatch '^\d+\.\d+\.\d+') {
Write-Error "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver."
exit 1
}
return $version
}
function Resolve-FileReleaseVersion {
param(
[Parameter(Mandatory = $true)]
[object[]]$Plugins,
[Parameter(Mandatory = $true)]
[string]$ScriptDir
)
$releaseVersionPlugin = @($Plugins | Where-Object { $_.name -eq 'FileReleaseVersion' } | Select-Object -First 1)
if ($releaseVersionPlugin.Count -eq 0 -or $null -eq $releaseVersionPlugin[0]) {
Write-Error "Configure a FileReleaseVersion plugin in scriptSettings.json with versionFilePath."
exit 1
}
$releaseVersionSettings = $releaseVersionPlugin[0]
$versionFileSetting = if ($releaseVersionSettings.versionFilePath) {
$releaseVersionSettings.versionFilePath
}
else {
'..\\..\\..\\VERSION'
}
$versionFilePaths = @(Resolve-RelativePaths -Value $versionFileSetting -BasePath $ScriptDir)
if ($versionFilePaths.Count -eq 0) {
Write-Error "Configure release version via FileReleaseVersion.versionFilePath (repo-root VERSION file)."
exit 1
}
$versionFilePath = $versionFilePaths[0]
Write-Log -Level "INFO" -Message "Reading version from VERSION file (versionFilePath)..."
$version = Get-VersionFileSemver -VersionFilePath $versionFilePath
Write-Log -Level "OK" -Message " $([System.IO.Path]::GetFileName($versionFilePath)): $version"
return [pscustomobject]@{
version = $version
source = 'FileReleaseVersion'
}
}
function Resolve-ReleaseVersion {
param(
[Parameter(Mandatory = $true)]
@ -201,9 +268,15 @@ function Resolve-ReleaseVersion {
$dotnetPlugin = @($Plugins | Where-Object { $_.name -eq 'DotNetReleaseVersion' -and $_.enabled -ne $false })
$npmPlugin = @($Plugins | Where-Object { $_.name -eq 'NpmReleaseVersion' -and $_.enabled -ne $false })
$filePlugin = @($Plugins | Where-Object { $_.name -eq 'FileReleaseVersion' -and $_.enabled -ne $false })
if ($dotnetPlugin.Count -gt 0 -and $npmPlugin.Count -gt 0) {
Write-Error "Configure only one release version plugin: DotNetReleaseVersion or NpmReleaseVersion, not both."
$enabledVersionPlugins = @()
if ($dotnetPlugin.Count -gt 0) { $enabledVersionPlugins += 'DotNetReleaseVersion' }
if ($npmPlugin.Count -gt 0) { $enabledVersionPlugins += 'NpmReleaseVersion' }
if ($filePlugin.Count -gt 0) { $enabledVersionPlugins += 'FileReleaseVersion' }
if ($enabledVersionPlugins.Count -gt 1) {
Write-Error "Configure only one release version plugin: $($enabledVersionPlugins -join ', ')."
exit 1
}
@ -215,11 +288,15 @@ function Resolve-ReleaseVersion {
return Resolve-NpmReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
}
Write-Error "Configure a DotNetReleaseVersion plugin (projectFiles) or NpmReleaseVersion plugin (packageJsonPath) in scriptSettings.json."
if ($filePlugin.Count -gt 0) {
return Resolve-FileReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
}
Write-Error "Configure a DotNetReleaseVersion (projectFiles), NpmReleaseVersion (packageJsonPath), or FileReleaseVersion (versionFilePath) plugin in scriptSettings.json."
exit 1
}
Export-ModuleMember -Function Get-CsprojPropertyValue, Get-CsprojVersions, Resolve-RelativePaths, Resolve-DotNetReleaseVersion, Resolve-NpmReleaseVersion, Resolve-ReleaseVersion
Export-ModuleMember -Function Get-CsprojPropertyValue, Get-CsprojVersions, Get-VersionFileSemver, Resolve-RelativePaths, Resolve-DotNetReleaseVersion, Resolve-NpmReleaseVersion, Resolve-FileReleaseVersion, Resolve-ReleaseVersion

View File

@ -117,17 +117,265 @@ function Test-PluginAllowedOnBranch {
return $allowedBranches -contains $CurrentBranch
}
function Test-IsPublishPlugin {
function Get-MaksitOrchestrator {
<#
.SYNOPSIS
Returns Compose/Kubernetes when MAKSIT_ORCHESTRATOR is set; otherwise $null (local dev — all plugins run).
#>
$raw = [Environment]::GetEnvironmentVariable('MAKSIT_ORCHESTRATOR')
if ([string]::IsNullOrWhiteSpace($raw)) {
return $null
}
switch ($raw.Trim().ToLowerInvariant()) {
'compose' { return 'Compose' }
'kubernetes' { return 'Kubernetes' }
'k8s' { return 'Kubernetes' }
default { return $raw.Trim() }
}
}
function Get-PluginMetadataObject {
param(
[Parameter(Mandatory = $true)]
$Plugin
$Plugin,
[Parameter(Mandatory = $true)]
[string]$EngineDirectory
)
$modulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
if (-not (Test-Path $modulePath -PathType Leaf)) {
return $null
}
try {
$moduleInfo = Import-Module $modulePath -Force -PassThru -ErrorAction Stop
$metadataCommand = Get-Command -Name 'Get-PluginMetadata' -Module $moduleInfo.Name -ErrorAction SilentlyContinue
if (-not $metadataCommand) {
return $null
}
return & $metadataCommand
}
catch {
return $null
}
}
function Test-PluginSupportsOrchestrator {
<#
.SYNOPSIS
When MAKSIT_ORCHESTRATOR is unset (local dev), returns $true for every plugin.
When set (CI/ContainerBuilder), skips plugins whose orchestrators list excludes the active profile.
#>
param(
[Parameter(Mandatory = $true)]
$Plugin,
[Parameter(Mandatory = $true)]
[string]$EngineDirectory,
[Parameter(Mandatory = $false)]
[bool]$WriteLogs = $true
)
$orchestrator = Get-MaksitOrchestrator
if ([string]::IsNullOrWhiteSpace($orchestrator)) {
return $true
}
$allowed = @()
if ($Plugin.PSObject.Properties.Name -contains 'orchestrators' -and $Plugin.orchestrators) {
$allowed = @($Plugin.orchestrators | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}
else {
$metadata = Get-PluginMetadataObject -Plugin $Plugin -EngineDirectory $EngineDirectory
if ($null -ne $metadata -and $metadata.PSObject.Properties.Name -contains 'orchestrators' -and $metadata.orchestrators) {
$allowed = @($metadata.orchestrators | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}
}
if ($allowed.Count -eq 0) {
return $true
}
if ($orchestrator -in $allowed) {
return $true
}
if ($WriteLogs) {
Write-Log -Level 'INFO' -Message "Skipping plugin '$($Plugin.name)' (requires orchestrator in: $($allowed -join ', '); active: $orchestrator)."
}
return $false
}
function Test-PluginMutatesRemote {
param(
[Parameter(Mandatory = $true)]
$Plugin,
[Parameter(Mandatory = $false)]
[string]$EngineDirectory
)
if ($null -eq $Plugin -or [string]::IsNullOrWhiteSpace([string]$Plugin.name)) {
return $false
}
return @('GitHub', 'DotNetNuGet', 'DotNetDockerPush', 'DotNetHelmPush', 'NpmPublish') -contains ([string]$Plugin.name)
if ([string]::IsNullOrWhiteSpace($EngineDirectory)) {
if ($Plugin.PSObject.Properties.Name -contains 'context' -and $null -ne $Plugin.context -and $Plugin.context.scriptDir) {
$EngineDirectory = [string]$Plugin.context.scriptDir
}
elseif ($Plugin.PSObject.Properties.Name -contains 'scriptDir' -and -not [string]::IsNullOrWhiteSpace([string]$Plugin.scriptDir)) {
$EngineDirectory = [string]$Plugin.scriptDir
}
}
if ([string]::IsNullOrWhiteSpace($EngineDirectory)) {
return $false
}
$modulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
if (-not (Test-Path $modulePath -PathType Leaf)) {
return $false
}
try {
$moduleInfo = Import-Module $modulePath -Force -PassThru -ErrorAction Stop
$metadataCommand = Get-Command -Name 'Get-PluginMetadata' -Module $moduleInfo.Name -ErrorAction SilentlyContinue
if (-not $metadataCommand) {
return $false
}
$metadata = & $metadataCommand
if ($null -eq $metadata) {
return $false
}
if ($metadata.PSObject.Properties.Name -contains 'mutatesRemote') {
return [bool]$metadata.mutatesRemote
}
}
catch {
return $false
}
return $false
}
function Get-SecretEnvironmentValue {
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
return [Environment]::GetEnvironmentVariable($Name)
}
function Resolve-PluginSecretName {
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)) {
return $value.Trim()
}
}
return $null
}
function Get-RegistryCredentialsFromRuntime {
param(
[Parameter(Mandatory = $true)]
[string]$SecretName,
[Parameter(Mandatory = $false)]
[psobject]$SharedSettings
)
$raw = Get-SecretEnvironmentValue -Name $SecretName
if ([string]::IsNullOrWhiteSpace($raw)) {
throw "Environment variable '$SecretName' is not set."
}
try {
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($raw))
}
catch {
throw "Failed to decode '$SecretName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
}
$parts = $decoded -split ':', 2
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'."
}
return @{ User = $parts[0]; Password = $parts[1] }
}
function Get-EngineDryRun {
param(
[Parameter(Mandatory = $false)]
[psobject]$Settings
)
$envVal = [Environment]::GetEnvironmentVariable('MAKSIT_REPOUTILS_DRY_RUN')
if (-not [string]::IsNullOrWhiteSpace($envVal)) {
return @('1', 'true', 'yes') -contains $envVal.Trim().ToLowerInvariant()
}
if ($Settings -and $Settings.PSObject.Properties['dryRun'] -and $null -ne $Settings.dryRun) {
return [bool]$Settings.dryRun
}
return $false
}
function Test-EngineDryRun {
param(
[Parameter(Mandatory = $true)]
[psobject]$SharedSettings
)
if ($SharedSettings.PSObject.Properties.Name -contains 'dryRun') {
return [bool]$SharedSettings.dryRun
}
return $false
}
function Test-PluginSkipsRemoteMutation {
param(
[Parameter(Mandatory = $true)]
$Plugin,
[Parameter(Mandatory = $true)]
[psobject]$SharedSettings
)
$engineDirectory = [string]$SharedSettings.scriptDir
return (Test-EngineDryRun -SharedSettings $SharedSettings) -and (Test-PluginMutatesRemote -Plugin $Plugin -EngineDirectory $engineDirectory)
}
function Test-IsPublishPlugin {
param(
[Parameter(Mandatory = $true)]
$Plugin,
[Parameter(Mandatory = $false)]
[string]$EngineDirectory
)
return Test-PluginMutatesRemote -Plugin $Plugin -EngineDirectory $EngineDirectory
}
function Get-PluginSettingValue {
@ -266,6 +514,9 @@ function Resolve-PluginModulePath {
$candidatePaths = @(
(Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName),
(Join-Path (Join-Path $pluginsRoot "Platform") $pluginFileName),
(Join-Path (Join-Path $pluginsRoot "Docker") $pluginFileName),
(Join-Path (Join-Path $pluginsRoot "Podman") $pluginFileName),
(Join-Path (Join-Path $pluginsRoot "Helm") $pluginFileName),
(Join-Path (Join-Path $pluginsRoot "DotNet") $pluginFileName),
(Join-Path (Join-Path $pluginsRoot "Npm") $pluginFileName)
)
@ -365,6 +616,10 @@ function Invoke-ConfiguredPlugin {
return $true
}
if (-not (Test-PluginSupportsOrchestrator -Plugin $Plugin -EngineDirectory $EngineDirectory -WriteLogs:$true)) {
return $true
}
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.name)'..."
@ -383,4 +638,4 @@ function Invoke-ConfiguredPlugin {
}
}
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStageLabel, Get-PluginBranches, 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-MaksitOrchestrator, Get-PluginMetadataObject, Test-PluginSupportsOrchestrator, Test-PluginMutatesRemote, Resolve-PluginSecretName, Get-SecretEnvironmentValue, Get-RegistryCredentialsFromRuntime, Get-EngineDryRun, Test-EngineDryRun, Test-PluginSkipsRemoteMutation, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin

View File

@ -78,7 +78,12 @@ function New-EngineContext {
[string]$SrcDir,
[Parameter(Mandatory = $false)]
[psobject]$Settings
[psobject]$Settings,
[switch]$DryRun,
[ValidateSet('single', 'ha')]
[string]$DeployMode = 'ha'
)
$resolvedVersion = Resolve-ReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
@ -119,6 +124,23 @@ function New-EngineContext {
$tag = "v$version"
Write-Log -Level "INFO" -Message " Release tag default from ${versionSource}: $tag (ReleasePublishGuard may replace from git when publish is allowed)."
$dryRun = $false
if ($DryRun) {
$dryRun = $true
}
else {
$dryRun = Get-EngineDryRun -Settings $Settings
}
Write-Log -Level "INFO" -Message " Dry run (remote mutations only): $dryRun"
$orchestrator = Get-MaksitOrchestrator
if ($orchestrator) {
Write-Log -Level "INFO" -Message " Orchestrator: $orchestrator (plugin profile filtering active)"
}
else {
Write-Log -Level "INFO" -Message " Orchestrator: not set (dev mode — all plugins eligible; engine probe still selects docker vs podman)"
}
return [pscustomobject]@{
scriptDir = $ScriptDir
srcDir = $SrcDir
@ -132,6 +154,9 @@ function New-EngineContext {
releaseBranches = $releaseBranches
publishCompleted = $false
skipPublishPlugins = $false
dryRun = $dryRun
deployMode = $DeployMode
orchestrator = $orchestrator
}
}

View File

@ -19,7 +19,10 @@ function New-EngineContext {
[string]$SrcDir,
[Parameter(Mandatory = $false)]
[psobject]$Settings
[psobject]$Settings,
[ValidateSet('single', 'ha')]
[string]$DeployMode = 'ha'
)
$badgesDir = $null
@ -32,6 +35,7 @@ function New-EngineContext {
srcDir = $SrcDir
utilsDir = $SrcDir
badgesDir = $badgesDir
deployMode = $DeployMode
}
}

View File

@ -0,0 +1,631 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Shared helpers for HelmSelfDeploy (OCI chart upgrade/install against a live cluster).
#>
if (-not (Get-Command Get-RegistryCredentialsFromRuntime -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path $PSScriptRoot -Parent
$pluginSupportModulePath = Join-Path $srcDir "Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
$scriptConfigModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'ScriptConfig.psm1'
if (Test-Path -LiteralPath $scriptConfigModulePath -PathType Leaf) {
Import-Module $scriptConfigModulePath -Force -Global -ErrorAction Stop
}
function Get-ContainerRegistryCredentialsFromSecret {
param(
[Parameter(Mandatory = $true)]
[string]$SecretName,
[Parameter(Mandatory = $false)]
[psobject]$SharedSettings
)
return Get-RegistryCredentialsFromRuntime -SecretName $SecretName -SharedSettings $SharedSettings
}
function Test-HelmAvailable {
return [bool](Get-Command helm -ErrorAction SilentlyContinue)
}
function Test-KubectlAvailable {
return [bool](Get-Command kubectl -ErrorAction SilentlyContinue)
}
function Confirm-HelmAndKubectlAvailable {
if (-not (Test-HelmAvailable)) {
throw "Helm is not installed or not in PATH."
}
if (-not (Test-KubectlAvailable)) {
throw "kubectl is not installed or not in PATH."
}
}
function Test-KubectlClusterReachable {
kubectl cluster-info 2>$null | Out-Null
return $LASTEXITCODE -eq 0
}
function Get-HelmUpgradeDryRunArgs {
if (Test-KubectlClusterReachable) {
return @('--dry-run=server')
}
Write-Host "Cluster not reachable via kubectl; using client-side helm --dry-run."
return @('--dry-run')
}
function Test-SecretEnvironmentValueAvailable {
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
if ([string]::IsNullOrWhiteSpace($Name)) {
return $false
}
$value = [Environment]::GetEnvironmentVariable($Name)
return -not [string]::IsNullOrWhiteSpace($value)
}
function Set-K8sNamespace {
param(
[Parameter(Mandatory = $true)]
[string]$Namespace
)
$Namespace = $Namespace.Trim()
if ([string]::IsNullOrWhiteSpace($Namespace)) {
throw "Namespace name is missing or empty."
}
$ns = kubectl get namespace $Namespace --ignore-not-found -o name 2>$null
if ($LASTEXITCODE -ne 0) {
throw "kubectl get namespace failed for '$Namespace' (exit code $LASTEXITCODE)."
}
if (-not $ns) {
Write-Host "Creating namespace $Namespace"
kubectl create namespace $Namespace | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Failed to create namespace $Namespace"
}
}
}
function Wait-NamespacePodsReady {
param(
[Parameter(Mandatory = $true)]
[string]$Namespace,
[int]$TimeoutSeconds = 300,
[int]$PollIntervalSeconds = 5
)
$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
while ([DateTime]::UtcNow -lt $deadline) {
$json = kubectl get pods -n $Namespace -o json 2>$null
if ($LASTEXITCODE -ne 0) {
Start-Sleep -Seconds $PollIntervalSeconds
continue
}
$obj = $json | ConvertFrom-Json
$pods = @($obj.items)
if ($pods.Count -eq 0) {
Start-Sleep -Seconds $PollIntervalSeconds
continue
}
$ready = 0
foreach ($p in $pods) {
$phase = if ($p.status) { $p.status.phase } else { "Unknown" }
$conditions = if ($p.status.conditions) { @($p.status.conditions) } else { @() }
$readyCond = $conditions | Where-Object { $_.type -eq "Ready" } | Select-Object -First 1
if ($phase -eq "Running" -and $null -ne $readyCond -and $readyCond.status -eq "True") {
$ready++
}
}
if ($ready -eq $pods.Count) {
Write-Host "All $($pods.Count) pod(s) in namespace $Namespace are ready."
return $true
}
Write-Host "Pods in $Namespace : $ready / $($pods.Count) ready"
Start-Sleep -Seconds $PollIntervalSeconds
}
Write-Warning "Timeout after ${TimeoutSeconds}s: not all pods in namespace $Namespace are ready."
return $false
}
function Get-DerivedOciChartRefFromChartEntry {
param(
[Parameter(Mandatory = $true)]
[object]$Chart,
[Parameter(Mandatory = $true)]
[string]$ChartVersion
)
if ($Chart.PSObject.Properties.Name -notcontains "containerRegistry" -or -not $Chart.containerRegistry) {
return $null
}
$cr = $Chart.containerRegistry
$regHost = ""
if ($cr.PSObject.Properties.Name -contains "registryUrl" -and -not [string]::IsNullOrWhiteSpace([string]$cr.registryUrl)) {
$regHost = [string]$cr.registryUrl.Trim().TrimEnd('/')
$regHost = $regHost -replace '^(https?|oci)://', ''
}
$ociChartsProject = if ($cr.PSObject.Properties.Name -contains "ociChartsProject" -and -not [string]::IsNullOrWhiteSpace([string]$cr.ociChartsProject)) {
[string]$cr.ociChartsProject.Trim().Trim('/')
}
else {
""
}
$chartProject = if ($cr.PSObject.Properties.Name -contains "chartProject" -and -not [string]::IsNullOrWhiteSpace([string]$cr.chartProject)) {
[string]$cr.chartProject.Trim().Trim('/')
}
else {
""
}
if ([string]::IsNullOrWhiteSpace($regHost) -or [string]::IsNullOrWhiteSpace($chartProject)) {
return $null
}
$path = if ([string]::IsNullOrWhiteSpace($ociChartsProject)) { $chartProject } else { "$ociChartsProject/$chartProject" }
return "oci://${regHost}/${path}:${ChartVersion}"
}
function Invoke-HelmOciChartRegistryLoginFromEnv {
param(
[Parameter(Mandatory = $true)]
[string]$RegistryHost,
[Parameter(Mandatory = $true)]
[string]$CredentialsSecret,
[psobject]$SharedSettings = $null
)
$RegistryHost = $RegistryHost.Trim().TrimEnd('/')
$helmCreds = Get-ContainerRegistryCredentialsFromSecret -SecretName $CredentialsSecret -SharedSettings $SharedSettings
Write-Host "Logging Helm into OCI chart registry $RegistryHost"
$prevEap = $ErrorActionPreference
try {
$ErrorActionPreference = "Continue"
$helmCreds.Password | & helm registry login $RegistryHost --username $helmCreds.User --password-stdin 2>&1 | Out-Null
}
finally {
$ErrorActionPreference = $prevEap
}
if ($LASTEXITCODE -ne 0) {
throw "helm registry login failed for $RegistryHost (exit code $LASTEXITCODE)."
}
}
function Set-KubernetesImagePullSecretFromEnv {
param(
[Parameter(Mandatory = $true)]
[string]$Namespace,
[Parameter(Mandatory = $true)]
[string]$RegistryHost,
[Parameter(Mandatory = $true)]
[string]$SecretName,
[Parameter(Mandatory = $true)]
[string]$CredentialsSecret,
[psobject]$SharedSettings = $null
)
$RegistryHost = $RegistryHost.Trim().TrimEnd('/')
$creds = Get-ContainerRegistryCredentialsFromSecret -SecretName $CredentialsSecret -SharedSettings $SharedSettings
Write-Host "Creating/updating image pull secret '$SecretName' in namespace $Namespace"
$createArgs = @(
"create", "secret", "docker-registry", $SecretName,
"--docker-server=$RegistryHost",
"--docker-username=$($creds.User)",
"--docker-password", $creds.Password,
"-n", $Namespace,
"--dry-run=client", "-o", "yaml"
)
$secretYaml = & kubectl @createArgs 2>&1
if ($LASTEXITCODE -ne 0) {
throw "kubectl create secret (dry-run) failed for '$SecretName': $secretYaml"
}
$secretYaml | kubectl apply -f -
if ($LASTEXITCODE -ne 0) {
throw "kubectl apply failed for docker-registry secret '$SecretName' (exit code $LASTEXITCODE)."
}
}
function Initialize-ContainerRegistryAuthForChart {
param(
[Parameter(Mandatory = $true)]
[string]$Namespace,
[Parameter(Mandatory = $true)]
[object]$Chart,
[string]$DefaultHelmCredentialsSecret = "",
[psobject]$SharedSettings = $null,
[switch]$DryRun
)
$reg = if ($Chart.PSObject.Properties.Name -contains "containerRegistry") { $Chart.containerRegistry } else { $null }
if ($null -eq $reg) {
return
}
$rel = if ($Chart.releaseName) { [string]$Chart.releaseName } else { "<unknown>" }
$baseUrl = if ($reg.PSObject.Properties.Name -contains "registryUrl" -and -not [string]::IsNullOrWhiteSpace([string]$reg.registryUrl)) {
[string]$reg.registryUrl.Trim().TrimEnd('/')
}
else {
""
}
$helmUrl = if ($reg.PSObject.Properties.Name -contains "helmRegistryUrl" -and -not [string]::IsNullOrWhiteSpace([string]$reg.helmRegistryUrl)) {
[string]$reg.helmRegistryUrl.Trim().TrimEnd('/')
}
else {
$baseUrl
}
$imgUrl = if ($reg.PSObject.Properties.Name -contains "imagesRegistryUrl" -and -not [string]::IsNullOrWhiteSpace([string]$reg.imagesRegistryUrl)) {
[string]$reg.imagesRegistryUrl.Trim().TrimEnd('/')
}
else {
$baseUrl
}
$helmCredsSecret = if ($reg.PSObject.Properties.Name -contains 'helmOciCredentialsSecret' -and -not [string]::IsNullOrWhiteSpace([string]$reg.helmOciCredentialsSecret)) {
[string]$reg.helmOciCredentialsSecret
}
elseif (-not [string]::IsNullOrWhiteSpace($DefaultHelmCredentialsSecret)) {
$DefaultHelmCredentialsSecret
}
else {
""
}
$imgCredsSecret = if ($reg.PSObject.Properties.Name -contains 'imagesCredentialsSecret' -and -not [string]::IsNullOrWhiteSpace([string]$reg.imagesCredentialsSecret)) {
[string]$reg.imagesCredentialsSecret
}
else {
""
}
$imgSecretName = if ($reg.PSObject.Properties.Name -contains "imagePullSecretName" -and -not [string]::IsNullOrWhiteSpace([string]$reg.imagePullSecretName)) {
[string]$reg.imagePullSecretName
}
else {
"cr-maks-it"
}
if (-not [string]::IsNullOrWhiteSpace($helmCredsSecret)) {
if ([string]::IsNullOrWhiteSpace($helmUrl)) {
throw "Chart '$rel': containerRegistry.registryUrl is required for Helm OCI login."
}
if ($DryRun -and -not (Test-SecretEnvironmentValueAvailable -Name $helmCredsSecret)) {
Write-Host "Dry run: skipping Helm OCI login; set env var '$helmCredsSecret' to validate remote OCI charts."
}
else {
Invoke-HelmOciChartRegistryLoginFromEnv -RegistryHost $helmUrl -CredentialsSecret $helmCredsSecret -SharedSettings $SharedSettings
}
}
if (-not $DryRun -and -not [string]::IsNullOrWhiteSpace($imgCredsSecret)) {
if ([string]::IsNullOrWhiteSpace($imgUrl)) {
throw "Chart '$rel': containerRegistry.registryUrl is required for image pull secret."
}
Set-KubernetesImagePullSecretFromEnv -Namespace $Namespace -RegistryHost $imgUrl -SecretName $imgSecretName -CredentialsSecret $imgCredsSecret -SharedSettings $SharedSettings
}
}
function Test-HelmReleaseExists {
param(
[Parameter(Mandatory = $true)]
[string]$Namespace,
[Parameter(Mandatory = $true)]
[string]$ReleaseName
)
$releaseExists = helm list -n $Namespace -q 2>$null | Select-String -Pattern "^${ReleaseName}$" -Quiet
return [bool]$releaseExists
}
function Invoke-PostgresClusterWait {
param(
[Parameter(Mandatory = $true)]
[object]$PostgresWait
)
if ($null -eq $PostgresWait -or $PostgresWait.enabled -ne $true) {
return
}
$pgNs = if ($PostgresWait.namespace) { [string]$PostgresWait.namespace } else { "postgresql" }
$cluster = if ($PostgresWait.clusterName) { [string]$PostgresWait.clusterName } else { "postgresql" }
$timeout = if ($null -ne $PostgresWait.timeoutSeconds) { [int]$PostgresWait.timeoutSeconds } else { 600 }
Write-Host "postgresWait: waiting for CNPG cluster '$cluster' in namespace '$pgNs' (timeout ${timeout}s)..."
kubectl wait "cluster.postgresql.cnpg.io/$cluster" -n $pgNs --for=condition=Ready --timeout="${timeout}s"
if ($LASTEXITCODE -ne 0) {
throw "postgresWait failed: cluster '$cluster' in '$pgNs' did not become Ready."
}
Write-Host "postgresWait: cluster is Ready."
}
function Get-HelmSetArgsFromChartEntry {
param(
[Parameter(Mandatory = $true)]
[object]$Chart,
[Parameter(Mandatory = $true)]
[string]$ChartVersion
)
$setArg = @()
if ($Chart.PSObject.Properties.Name -notcontains 'helmSet' -or -not $Chart.helmSet) {
return $setArg
}
foreach ($prop in $Chart.helmSet.PSObject.Properties) {
$val = ([string]$prop.Value) -replace '\{chartVersion\}', $ChartVersion
$setArg += '--set'
$setArg += "$($prop.Name)=$val"
}
return $setArg
}
function Invoke-HelmRemoteChartUpgrade {
param(
[Parameter(Mandatory = $true)]
[string]$Namespace,
[Parameter(Mandatory = $true)]
[object]$Chart,
[Parameter(Mandatory = $true)]
[string]$ChartVersion,
[Parameter(Mandatory = $true)]
[string]$DeploySettingsDir,
[string]$DefaultHelmCredentialsSecret = "",
[psobject]$SharedSettings = $null,
[switch]$DryRun
)
$releaseName = [string]$Chart.releaseName
if ([string]::IsNullOrWhiteSpace($releaseName)) {
throw "Chart entry is missing releaseName."
}
Initialize-ContainerRegistryAuthForChart -Namespace $Namespace -Chart $Chart -DefaultHelmCredentialsSecret $DefaultHelmCredentialsSecret -SharedSettings $SharedSettings -DryRun:$DryRun
$ociRef = Get-DerivedOciChartRefFromChartEntry -Chart $Chart -ChartVersion $ChartVersion
if ([string]::IsNullOrWhiteSpace($ociRef)) {
throw "Could not derive OCI chart reference for release '$releaseName'. Set chart.containerRegistry.registryUrl and chartProject."
}
$valuesArg = @()
$valuesPath = $null
if ($Chart.PSObject.Properties.Name -contains 'valuesFile' -and -not [string]::IsNullOrWhiteSpace([string]$Chart.valuesFile)) {
$deployMode = $null
if ($null -ne $SharedSettings -and $SharedSettings.PSObject.Properties.Name -contains 'deployMode') {
$deployMode = [string]$SharedSettings.deployMode
}
if ($deployMode -in @('single', 'ha')) {
$valuesPath = Resolve-DeployValuesFilePath -DeploySettingsDir $DeploySettingsDir -ValuesFile ([string]$Chart.valuesFile) -DeployMode $deployMode
}
else {
throw "HelmSelfDeploy requires deployMode 'single' or 'ha' when valuesFile is set."
}
$valuesArg = @('-f', $valuesPath)
}
$releaseExists = Test-HelmReleaseExists -Namespace $Namespace -ReleaseName $releaseName
$reuseArg = @()
if ($valuesArg.Count -gt 0) {
if ($releaseExists) {
Write-Host "Upgrading existing release '$releaseName' in namespace '$Namespace' with values file $valuesPath (chart version $ChartVersion)"
}
else {
Write-Host "Installing new release '$releaseName' in namespace '$Namespace' with values file $valuesPath (chart version $ChartVersion)"
}
}
elseif ($releaseExists) {
Write-Host "Upgrading existing release '$releaseName' in namespace '$Namespace' to chart version $ChartVersion (reuse-values)"
$reuseArg = @("--reuse-values")
}
else {
throw "Release '$releaseName' is not installed in namespace '$Namespace'. Add values.single.yaml and values.ha.yaml beside scriptSettings.json and run utils/Invoke-ReleasePackage-HA.bat or Invoke-ReleasePackage-Single.bat for first install."
}
$setArg = Get-HelmSetArgsFromChartEntry -Chart $Chart -ChartVersion $ChartVersion
$dryRunArg = if ($DryRun) { Get-HelmUpgradeDryRunArgs } else { @() }
if ($DryRun) {
if ($valuesArg.Count -gt 0) {
Write-Host "Dry run: helm upgrade --install '$releaseName' in namespace '$Namespace' with values file $valuesPath (chart version $ChartVersion)"
}
else {
Write-Host "Dry run: helm upgrade --install '$releaseName' in namespace '$Namespace' to chart version $ChartVersion"
}
}
helm upgrade --install $releaseName $ociRef -n $Namespace --create-namespace --version $ChartVersion @reuseArg @valuesArg @setArg @dryRunArg
if ($LASTEXITCODE -ne 0) {
$action = if ($DryRun) { 'Helm dry-run upgrade/install' } else { 'Helm upgrade/install' }
throw "$action failed for release '$releaseName' (exit code $LASTEXITCODE)."
}
}
function Remove-BgpServices {
param(
[Parameter(Mandatory = $true)]
[string]$Namespace,
[Parameter(Mandatory = $true)]
[string]$ScriptDir,
[Parameter(Mandatory = $true)]
[string]$ReleaseName,
[Parameter(Mandatory = $true)]
[AllowEmptyCollection()]
[object[]]$BgpServices,
[Parameter(Mandatory = $true)]
[int]$DefaultPort
)
foreach ($svc in $BgpServices) {
$template = $svc.template
$ip = $svc.ip
$path = Join-Path $ScriptDir $template
if (-not (Test-Path $path)) {
Write-Host "BGP template not found (skip delete): $path"
continue
}
Write-Host "Removing BGP service $template (IP: $ip)"
$content = Get-Content $path -Raw
$portStr = if ($null -ne $svc.port) { [string]$svc.port } else { [string]$DefaultPort }
$content = $content -replace "__RELEASE_NAME__", $ReleaseName -replace "__NAMESPACE__", $Namespace -replace "__BGP_IP__", $ip -replace "__PORT__", $portStr
$content | kubectl delete -f - --ignore-not-found 2>$null
}
}
function Invoke-HelmReleaseUninstall {
param(
[Parameter(Mandatory = $true)]
[string]$Namespace,
[Parameter(Mandatory = $true)]
[object]$Chart
)
$releaseName = [string]$Chart.releaseName
if ([string]::IsNullOrWhiteSpace($releaseName)) {
throw "Chart entry is missing releaseName."
}
if (-not (Test-HelmReleaseExists -Namespace $Namespace -ReleaseName $releaseName)) {
Write-Host "Release '$releaseName' not found in namespace '$Namespace', nothing to uninstall."
return
}
Write-Host "Uninstalling Helm release '$releaseName' from namespace '$Namespace' ..."
$prevEap = $ErrorActionPreference
try {
$ErrorActionPreference = "Continue"
helm uninstall $releaseName -n $Namespace 2>&1 | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "helm uninstall failed for release '$releaseName' (exit code $LASTEXITCODE)."
}
}
finally {
$ErrorActionPreference = $prevEap
}
}
function Wait-TcpConnectivity {
param(
[Parameter(Mandatory = $true)]
[string]$ComputerName,
[Parameter(Mandatory = $true)]
[int]$Port,
[int]$TimeoutSeconds = 300,
[int]$PollIntervalSeconds = 3
)
$deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
while ([DateTime]::UtcNow -lt $deadline) {
$result = Test-NetConnection -ComputerName $ComputerName -Port $Port -WarningAction SilentlyContinue
if ($result.TcpTestSucceeded) {
Write-Host "Connectivity OK (${ComputerName}:${Port})"
return $true
}
Start-Sleep -Seconds $PollIntervalSeconds
}
Write-Warning "Timeout after ${TimeoutSeconds}s: no connectivity to ${ComputerName}:${Port}"
return $false
}
function Set-BgpServices {
param(
[Parameter(Mandatory = $true)]
[string]$Namespace,
[Parameter(Mandatory = $true)]
[string]$ScriptDir,
[Parameter(Mandatory = $true)]
[string]$ReleaseName,
[Parameter(Mandatory = $true)]
[AllowEmptyCollection()]
[object[]]$BgpServices,
[Parameter(Mandatory = $true)]
[int]$DefaultPort
)
foreach ($svc in $BgpServices) {
$template = $svc.template
$ip = $svc.ip
$path = Join-Path $ScriptDir $template
if (-not (Test-Path $path)) {
Write-Warning "BGP template not found: $path"
continue
}
Write-Host "Applying BGP service $template (IP: $ip)"
$content = Get-Content $path -Raw
$portStr = if ($null -ne $svc.port) { [string]$svc.port } else { [string]$DefaultPort }
$content = $content -replace "__RELEASE_NAME__", $ReleaseName -replace "__NAMESPACE__", $Namespace -replace "__BGP_IP__", $ip -replace "__PORT__", $portStr
$content | kubectl apply -f -
if ($LASTEXITCODE -ne 0) {
throw "kubectl apply failed for BGP template $template"
}
}
}
function Test-BgpConnectivity {
param(
[Parameter(Mandatory = $true)]
[AllowEmptyCollection()]
[object[]]$BgpServices,
[int]$TimeoutSeconds = 300,
[int]$PollIntervalSeconds = 3
)
$toTest = @($BgpServices | Where-Object { $null -ne $_.port })
if ($toTest.Count -eq 0) {
Write-Warning "No BGP services have port set; skipping connectivity test."
return
}
foreach ($svc in $toTest) {
$h = $svc.ip
$p = [int]$svc.port
Write-Host "Waiting for connectivity to ${h}:${p}..."
Wait-TcpConnectivity -ComputerName $h -Port $p -TimeoutSeconds $TimeoutSeconds -PollIntervalSeconds $PollIntervalSeconds | Out-Null
}
}
Export-ModuleMember -Function `
Confirm-HelmAndKubectlAvailable, `
Set-K8sNamespace, `
Wait-NamespacePodsReady, `
Invoke-PostgresClusterWait, `
Invoke-HelmRemoteChartUpgrade, `
Invoke-HelmReleaseUninstall, `
Set-BgpServices, `
Remove-BgpServices, `
Test-BgpConnectivity

View File

@ -0,0 +1,135 @@
#requires -Version 7.0
#requires -PSEdition Core
function Get-ScriptSettings {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ScriptDir,
[ValidateSet('single', 'ha')]
[string]$Mode,
[Parameter(Mandatory = $false)]
[string]$SettingsFileName = 'scriptSettings.json'
)
$settingsPath = if ($PSBoundParameters.ContainsKey('Mode')) {
$modePath = Join-Path $ScriptDir "scriptSettings.$Mode.json"
if (Test-Path -LiteralPath $modePath -PathType Leaf) {
$modePath
}
else {
Join-Path $ScriptDir $SettingsFileName
}
}
else {
Join-Path $ScriptDir $SettingsFileName
}
if (-not (Test-Path -LiteralPath $settingsPath -PathType Leaf)) {
throw "Settings file not found: $settingsPath"
}
return Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json
}
function Get-HelmSelfDeployPluginFromSettings {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[psobject]$Settings
)
if (-not ($Settings.PSObject.Properties.Name -contains 'plugins') -or -not $Settings.plugins) {
return $null
}
foreach ($plugin in @($Settings.plugins)) {
if ($plugin.name -eq 'HelmSelfDeploy') {
return $plugin
}
}
return $null
}
function Get-DeployModeFromSettings {
<#
.SYNOPSIS
Resolves cluster deploy profile from HelmSelfDeploy plugin settings or an explicit CLI override.
.DESCRIPTION
CI/CD pipelines omit -Mode and read deployMode on the HelmSelfDeploy plugin.
Manual installs use Invoke-ReleasePackage-Single.bat / Invoke-ReleasePackage-HA.bat.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[psobject]$Settings,
[ValidateSet('single', 'ha')]
[string]$ModeOverride
)
if ($PSBoundParameters.ContainsKey('ModeOverride')) {
return $ModeOverride
}
$helmSelfDeploy = Get-HelmSelfDeployPluginFromSettings -Settings $Settings
if ($null -ne $helmSelfDeploy -and $helmSelfDeploy.PSObject.Properties.Name -contains 'deployMode') {
$mode = [string]$helmSelfDeploy.deployMode
if (-not [string]::IsNullOrWhiteSpace($mode)) {
$mode = $mode.Trim().ToLowerInvariant()
if ($mode -in @('single', 'ha')) {
return $mode
}
throw "HelmSelfDeploy.deployMode must be 'single' or 'ha' (got '$mode')."
}
}
return 'ha'
}
function Resolve-DeployValuesFilePath {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$DeploySettingsDir,
[Parameter(Mandatory = $true)]
[string]$ValuesFile,
[Parameter(Mandatory = $true)]
[ValidateSet('single', 'ha')]
[string]$DeployMode
)
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($ValuesFile)
$extension = [System.IO.Path]::GetExtension($ValuesFile)
if ([string]::IsNullOrEmpty($extension)) {
$extension = '.yaml'
}
$valuesPath = Join-Path $DeploySettingsDir "$baseName.$DeployMode$extension"
if (Test-Path -LiteralPath $valuesPath -PathType Leaf) {
return $valuesPath
}
throw "Deploy values file not found: '$valuesPath'. HelmSelfDeploy expects values.single.yaml or values.ha.yaml beside scriptSettings.json."
}
function Assert-Command {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Command
)
if (-not (Get-Command $Command -ErrorAction SilentlyContinue)) {
throw "Required command '$Command' is missing. Aborting."
}
}
Export-ModuleMember -Function Get-ScriptSettings, Get-DeployModeFromSettings, Resolve-DeployValuesFilePath, Assert-Command

View File

@ -0,0 +1,35 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Docker Compose plugin — validate or bring up the stack via docker compose.
.DESCRIPTION
Requires ContainerEngineProbe first. Skips safely when activeContainerEngine is podman.
Orchestrator filtering (Compose-only) applies only when MAKSIT_ORCHESTRATOR is set (CI).
#>
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 'ComposeDeploySupport' -RequiredCommand 'Invoke-ComposeStackPlugin'
Invoke-ComposeStackPlugin -Settings $Settings -RequiredEngine 'docker' -PluginName 'DockerCompose'
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true; orchestrators = @('Compose') }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -0,0 +1,43 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Docker image publish plugin — build and push images via docker (Docker Desktop / local docker CLI).
.DESCRIPTION
Requires ContainerEngineProbe first. Skips safely when activeContainerEngine is podman.
Builds any Dockerfile listed in images[]; not limited to .NET projects.
#>
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 'Test-ShouldRunContainerEnginePlugin'
Import-PluginDependency -ModuleName 'ContainerImagePushSupport' -RequiredCommand 'Invoke-ContainerImagePush'
$shared = $Settings.context
if (-not (Test-ShouldRunContainerEnginePlugin -RequiredEngine 'docker' -SharedSettings $shared -PluginName 'DockerImagePush')) {
return
}
Invoke-ContainerImagePush -Settings $Settings -PluginDisplayName 'DockerImagePush'
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -29,22 +29,36 @@ function Invoke-Plugin {
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$nugetApiKeyEnvVar = $pluginSettings.nugetApiKey
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret'
$packageFile = $sharedSettings.packageFile
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
Assert-Command dotnet
if (-not $packageFile) {
throw "DotNetNuGet plugin requires a NuGet package artifact. Ensure DotNetPack produced a .nupkg before running DotNetNuGet."
}
if ([string]::IsNullOrWhiteSpace($nugetApiKeyEnvVar)) {
throw "DotNetNuGet plugin requires 'nugetApiKey' in scriptSettings.json."
if ($dryRun) {
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
"https://api.nuget.org/v3/index.json"
}
else {
$pluginSettings.source
}
Write-Log -Level "INFO" -Message "Dry run: would push $($packageFile.FullName) to $nugetSource"
return
}
$nugetApiKey = [System.Environment]::GetEnvironmentVariable($nugetApiKeyEnvVar)
if ([string]::IsNullOrWhiteSpace($nugetApiKey)) {
throw "NuGet API key is not set. Set '$nugetApiKeyEnvVar' and rerun."
if ([string]::IsNullOrWhiteSpace($nugetSecret)) {
throw "DotNetNuGet plugin requires 'nugetSecret' in scriptSettings.json (logical secret name, e.g. NuGet)."
}
$nugetKey = Get-SecretEnvironmentValue -Name $nugetSecret
if ([string]::IsNullOrWhiteSpace($nugetKey)) {
throw "NuGet API key is not set. Set environment variable '$nugetSecret'."
}
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
@ -55,7 +69,7 @@ function Invoke-Plugin {
}
Write-Log -Level "STEP" -Message "Pushing package to NuGet feed..."
dotnet nuget push $packageFile.FullName -k $nugetApiKey -s $nugetSource --skip-duplicate
dotnet nuget push $packageFile.FullName -k $nugetKey -s $nugetSource --skip-duplicate
if ($LASTEXITCODE -ne 0) {
throw "Failed to push the package to NuGet feed."
@ -65,7 +79,11 @@ function Invoke-Plugin {
$sharedSettings | Add-Member -NotePropertyName publishCompleted -NotePropertyValue $true -Force
}
Export-ModuleMember -Function Invoke-Plugin
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -3,13 +3,13 @@
<#
.SYNOPSIS
.NET Helm publish plugin — package and push charts versioned from DotNetReleaseVersion.
Helm publish plugin — package and push charts versioned from shared release context.
.DESCRIPTION
The chart in the repo should keep placeholder version and appVersion (e.g. 0.0.0); this plugin
overwrites them with the bare semver from shared context (DotNetReleaseVersion / shared.version,
e.g. 3.3.4 — no leading v), falling back to stripping v/V from shared.tag if version is missing,
then runs helm package and helm push, then restores Chart.yaml.
overwrites them with the bare semver from shared context (shared.version from a release-version
plugin, e.g. DotNetReleaseVersion or NpmReleaseVersion — no leading v), falling back to stripping
v/V from shared.tag if version is missing, then runs helm package and helm push, then restores Chart.yaml.
Optional pushLatest (default false when omitted): when true, after the versioned push, copies the chart
to a :latest tag in the same OCI repository using the oras CLI (https://oras.land). Requires oras on PATH.
@ -17,38 +17,12 @@
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Get-RegistryCredentialsFromEnv {
param(
[Parameter(Mandatory = $true)]
[string]$EnvVarName
)
$raw = [Environment]::GetEnvironmentVariable($EnvVarName)
if ([string]::IsNullOrWhiteSpace($raw)) {
throw "Environment variable '$EnvVarName' is not set."
}
try {
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($raw))
}
catch {
throw "Failed to decode '$EnvVarName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
}
$parts = $decoded -split ':', 2
if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) {
throw "Decoded '$EnvVarName' must be in the form 'username:password'."
}
return @{ User = $parts[0]; Password = $parts[1] }
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
@ -61,20 +35,25 @@ function Invoke-Plugin {
$pluginSettings = $Settings
$shared = $Settings.context
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
Assert-Command helm
$pushLatest = if ($null -ne $pluginSettings.pushLatest) { [bool]$pluginSettings.pushLatest } else { $false }
if ([string]::IsNullOrWhiteSpace($pluginSettings.chartPath)) {
throw "DotNetHelmPush plugin requires 'chartPath' (chart directory, relative to engines/release folder)."
throw "HelmPush plugin requires 'chartPath' (chart directory, relative to engines/release folder)."
}
if ([string]::IsNullOrWhiteSpace($pluginSettings.ociRepository)) {
throw "DotNetHelmPush plugin requires 'ociRepository' (e.g. oci://cr.maks-it.com/charts)."
throw "HelmPush plugin requires 'ociRepository' (e.g. oci://cr.maks-it.com/charts)."
}
if ([string]::IsNullOrWhiteSpace($pluginSettings.credentialsEnvVar)) {
throw "DotNetHelmPush plugin requires 'credentialsEnvVar' (name of env var holding base64 username:password)."
if (-not $dryRun) {
$containerRegistrySecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'containerRegistrySecret'
if ([string]::IsNullOrWhiteSpace($containerRegistrySecret)) {
throw "HelmPush plugin requires 'containerRegistrySecret' (logical secret name, e.g. ContainerRegistry)."
}
}
$scriptDir = $shared.ScriptDir
@ -93,10 +72,9 @@ function Invoke-Plugin {
$chartVersion = ([string]$shared.tag).Trim() -replace '^[vV]', ''
}
if ([string]::IsNullOrWhiteSpace($chartVersion)) {
throw "Could not derive chart version: need shared.version (DotNetReleaseVersion) or shared.tag (e.g. v3.3.4)."
throw "Could not derive chart version: need shared.version (release version plugin) or shared.tag (e.g. v3.3.4)."
}
$creds = Get-RegistryCredentialsFromEnv -EnvVarName ([string]$pluginSettings.credentialsEnvVar)
$ociRepository = [string]$pluginSettings.ociRepository.TrimEnd('/')
$chartNameLine = Select-String -LiteralPath $chartYaml -Pattern '^\s*name:\s*(.+)\s*$' | Select-Object -First 1
@ -133,6 +111,16 @@ function Invoke-Plugin {
throw "Expected chart package not found: $chartPackage (helm output: $packageOutput)"
}
if ($dryRun) {
Write-Log -Level "INFO" -Message "Dry run: would push $chartPackage to $ociRepository"
Remove-Item -LiteralPath $chartPackage -Force -ErrorAction SilentlyContinue
Write-Log -Level "OK" -Message " Helm chart dry run completed."
return
}
$creds = Get-RegistryCredentialsFromRuntime -SecretName $containerRegistrySecret -SharedSettings $shared
$ociRepository = [string]$pluginSettings.ociRepository.TrimEnd('/')
Write-Log -Level "STEP" -Message "Pushing $chartPackage to $ociRepository ..."
helm push $chartPackage $ociRepository --username $creds.User --password $creds.Password
if ($LASTEXITCODE -ne 0) {
@ -178,4 +166,8 @@ function Invoke-Plugin {
$shared | Add-Member -NotePropertyName publishCompleted -NotePropertyValue $true -Force
}
Export-ModuleMember -Function Invoke-Plugin
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -0,0 +1,174 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Helm self-deploy plugin — upgrade/install the release chart on the target cluster after HelmPush.
.DESCRIPTION
Deployment settings (namespace, postgresWait, charts) are configured on the HelmSelfDeploy plugin
entry in scriptSettings.json. Chart version comes from shared.version (DotNetReleaseVersion).
Existing releases with valuesFile use -f on every upgrade. Without valuesFile, upgrades use --reuse-values. helmSet supports {chartVersion} for --set overrides (e.g. image tags).
#>
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 "HelmDeploySupport" -RequiredCommand "Invoke-HelmRemoteChartUpgrade"
$pluginSettings = $Settings
$shared = $Settings.context
$scriptDir = [string]$shared.ScriptDir
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
if (-not $pluginSettings.namespace) {
throw "HelmSelfDeploy plugin requires 'namespace'."
}
if (-not $pluginSettings.charts -or @($pluginSettings.charts).Count -eq 0) {
throw "HelmSelfDeploy plugin requires at least one chart in 'charts'."
}
$containerRegistrySecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'containerRegistrySecret'
if (-not $dryRun -and [string]::IsNullOrWhiteSpace($containerRegistrySecret)) {
throw "HelmSelfDeploy plugin requires 'containerRegistrySecret' (logical secret name, e.g. ContainerRegistry)."
}
$chartVersion = $null
if ($shared.PSObject.Properties.Name -contains 'version' -and -not [string]::IsNullOrWhiteSpace([string]$shared.version)) {
$chartVersion = ([string]$shared.version).Trim() -replace '^[vV]', ''
}
if ([string]::IsNullOrWhiteSpace($chartVersion) -and $shared.PSObject.Properties.Name -contains 'tag') {
$chartVersion = ([string]$shared.tag).Trim() -replace '^[vV]', ''
}
if ([string]::IsNullOrWhiteSpace($chartVersion)) {
throw "HelmSelfDeploy could not derive chart version from shared.version or shared.tag."
}
Confirm-HelmAndKubectlAvailable
if ($dryRun) {
Write-Log -Level "INFO" -Message "HelmSelfDeploy dry run: validating helm upgrade/install for namespace '$($pluginSettings.namespace)'."
}
$namespace = [string]$pluginSettings.namespace
if (-not $dryRun) {
Set-K8sNamespace -Namespace $namespace
}
$deployMode = 'ha'
if ($shared.PSObject.Properties.Name -contains 'deployMode' -and -not [string]::IsNullOrWhiteSpace([string]$shared.deployMode)) {
$deployMode = [string]$shared.deployMode
}
Write-Log -Level 'INFO' -Message "HelmSelfDeploy deploy mode: $deployMode (values.{mode}.yaml when present)."
$skipPostgresWait = $false
if ($pluginSettings.PSObject.Properties.Name -contains 'skipPostgresWait') {
$skipPostgresWait = [bool]$pluginSettings.skipPostgresWait
}
if (-not $dryRun -and -not $skipPostgresWait -and $pluginSettings.PSObject.Properties.Name -contains 'postgresWait') {
Invoke-PostgresClusterWait -PostgresWait $pluginSettings.postgresWait
}
foreach ($chart in @($pluginSettings.charts)) {
if ($chart.source -and [string]$chart.source -ne "remote") {
Write-Log -Level "INFO" -Message "Skipping non-remote chart entry (source=$($chart.source))."
continue
}
$stepMessage = if ($dryRun) {
"Dry run: validating Helm release '$($chart.releaseName)' version $chartVersion ..."
}
else {
"Self-deploying Helm release '$($chart.releaseName)' version $chartVersion ..."
}
Write-Log -Level "STEP" -Message $stepMessage
Invoke-HelmRemoteChartUpgrade `
-Namespace $namespace `
-Chart $chart `
-ChartVersion $chartVersion `
-DeploySettingsDir $scriptDir `
-DefaultHelmCredentialsSecret $containerRegistrySecret `
-SharedSettings $shared `
-DryRun:$dryRun
}
if ($dryRun) {
Write-Log -Level "INFO" -Message "HelmSelfDeploy dry run completed for namespace '$namespace'."
return
}
$waitForPods = $true
if ($pluginSettings.PSObject.Properties.Name -contains 'waitForPodsReady') {
$waitForPods = [bool]$pluginSettings.waitForPodsReady
}
if ($waitForPods) {
$timeout = 600
if ($pluginSettings.PSObject.Properties.Name -contains 'podsReadyTimeoutSeconds' -and $null -ne $pluginSettings.podsReadyTimeoutSeconds) {
$timeout = [int]$pluginSettings.podsReadyTimeoutSeconds
}
if (-not (Wait-NamespacePodsReady -Namespace $namespace -TimeoutSeconds $timeout)) {
throw "Not all pods became ready in namespace '$namespace' within ${timeout}s."
}
}
$bgpServices = @()
if ($pluginSettings.PSObject.Properties.Name -contains 'bgpServices' -and $pluginSettings.bgpServices) {
$bgpServices = @($pluginSettings.bgpServices)
}
if ($bgpServices.Count -gt 0) {
Import-PluginDependency -ModuleName "HelmDeploySupport" -RequiredCommand "Set-BgpServices"
$releaseName = [string]$pluginSettings.charts[0].releaseName
$bgpDefaultPort = 80
if ($pluginSettings.PSObject.Properties.Name -contains 'bgpDefaultPort' -and $null -ne $pluginSettings.bgpDefaultPort) {
$bgpDefaultPort = [int]$pluginSettings.bgpDefaultPort
}
Write-Log -Level "STEP" -Message "Applying $($bgpServices.Count) BGP service(s) for release '$releaseName' ..."
Set-BgpServices -Namespace $namespace -ScriptDir $scriptDir -ReleaseName $releaseName -BgpServices $bgpServices -DefaultPort $bgpDefaultPort
$testBgp = $false
if ($pluginSettings.PSObject.Properties.Name -contains 'testBgpConnectivity') {
$testBgp = [bool]$pluginSettings.testBgpConnectivity
}
if ($testBgp) {
Import-PluginDependency -ModuleName "HelmDeploySupport" -RequiredCommand "Test-BgpConnectivity"
$bgpTimeout = 300
if ($pluginSettings.PSObject.Properties.Name -contains 'bgpConnectivityTimeoutSeconds' -and $null -ne $pluginSettings.bgpConnectivityTimeoutSeconds) {
$bgpTimeout = [int]$pluginSettings.bgpConnectivityTimeoutSeconds
}
Test-BgpConnectivity -BgpServices $bgpServices -TimeoutSeconds $bgpTimeout
}
}
Write-Log -Level "INFO" -Message "HelmSelfDeploy completed for namespace '$namespace'."
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -0,0 +1,166 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Helm uninstall plugin — remove Helm release(s) and BGP LoadBalancer services from the target cluster.
.DESCRIPTION
By default inherits namespace, charts, and bgpServices from the enabled HelmSelfDeploy entry in
engines/release/scriptSettings.json (releaseSettingsFile, default ..\\release\\scriptSettings.json).
Optional overrides on this plugin: deleteNamespace, releaseSettingsFile.
kubectl/helm use KUBECONFIG, default kubeconfig, or in-cluster service account credentials.
#>
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 Resolve-HelmUninstallDeploySettings {
param(
[Parameter(Mandatory = $true)]
$PluginSettings,
[Parameter(Mandatory = $true)]
[string]$EngineDir
)
$inherit = $true
if ($PluginSettings.PSObject.Properties.Name -contains 'inheritFromRelease') {
$inherit = [bool]$PluginSettings.inheritFromRelease
}
elseif ($PluginSettings.PSObject.Properties.Name -contains 'namespace' -and -not [string]::IsNullOrWhiteSpace([string]$PluginSettings.namespace)) {
$inherit = $false
}
if (-not $inherit) {
return $PluginSettings
}
$releaseSettingsFile = '..\release\scriptSettings.json'
if ($PluginSettings.PSObject.Properties.Name -contains 'releaseSettingsFile' -and -not [string]::IsNullOrWhiteSpace([string]$PluginSettings.releaseSettingsFile)) {
$releaseSettingsFile = [string]$PluginSettings.releaseSettingsFile
}
$releaseSettingsPath = [System.IO.Path]::GetFullPath((Join-Path $EngineDir $releaseSettingsFile))
if (-not (Test-Path $releaseSettingsPath -PathType Leaf)) {
throw "HelmUninstall could not find release settings at '$releaseSettingsPath' (releaseSettingsFile)."
}
$releaseSettings = Get-Content $releaseSettingsPath -Raw | ConvertFrom-Json
$deployPlugin = $null
if ($releaseSettings.PSObject.Properties.Name -contains 'plugins' -and $releaseSettings.plugins) {
foreach ($candidate in @($releaseSettings.plugins)) {
if ($candidate.name -eq 'HelmSelfDeploy' -and $candidate.enabled) {
$deployPlugin = $candidate
break
}
}
}
if ($null -eq $deployPlugin) {
throw "HelmUninstall inheritFromRelease: no enabled HelmSelfDeploy plugin in '$releaseSettingsPath'."
}
$merged = @{}
foreach ($property in $deployPlugin.PSObject.Properties) {
$merged[$property.Name] = $property.Value
}
foreach ($property in $PluginSettings.PSObject.Properties) {
if ($property.Name -in @('name', 'stageLabel', 'enabled', 'inheritFromRelease', 'releaseSettingsFile')) {
continue
}
$merged[$property.Name] = $property.Value
}
if (-not $merged.ContainsKey('settingsDir') -or [string]::IsNullOrWhiteSpace([string]$merged['settingsDir'])) {
$merged['settingsDir'] = '..\release'
}
return [pscustomobject]$merged
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "HelmDeploySupport" -RequiredCommand "Invoke-HelmReleaseUninstall"
$shared = $Settings.context
$engineDir = [string]$shared.ScriptDir
$pluginSettings = Resolve-HelmUninstallDeploySettings -PluginSettings $Settings -EngineDir $engineDir
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
if ($dryRun) {
Write-Log -Level "INFO" -Message "Dry run: would uninstall Helm release(s) in namespace '$($pluginSettings.namespace)'."
return
}
if (-not $pluginSettings.namespace) {
throw "HelmUninstall requires 'namespace' (set explicitly or inherit from HelmSelfDeploy)."
}
if (-not $pluginSettings.charts -or @($pluginSettings.charts).Count -eq 0) {
throw "HelmUninstall requires at least one chart in 'charts' (inherit from HelmSelfDeploy or set explicitly)."
}
$settingsDir = $engineDir
if ($pluginSettings.PSObject.Properties.Name -contains 'settingsDir' -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.settingsDir)) {
$settingsDir = [System.IO.Path]::GetFullPath((Join-Path $engineDir ([string]$pluginSettings.settingsDir)))
}
Confirm-HelmAndKubectlAvailable
$namespace = [string]$pluginSettings.namespace
foreach ($chart in @($pluginSettings.charts)) {
Write-Log -Level "STEP" -Message "Removing Helm release '$($chart.releaseName)' ..."
Invoke-HelmReleaseUninstall -Namespace $namespace -Chart $chart
}
$bgpServices = @()
if ($pluginSettings.PSObject.Properties.Name -contains 'bgpServices' -and $pluginSettings.bgpServices) {
$bgpServices = @($pluginSettings.bgpServices)
}
if ($bgpServices.Count -gt 0) {
Import-PluginDependency -ModuleName "HelmDeploySupport" -RequiredCommand "Remove-BgpServices"
$releaseName = [string]$pluginSettings.charts[0].releaseName
$bgpDefaultPort = 80
if ($pluginSettings.PSObject.Properties.Name -contains 'bgpDefaultPort' -and $null -ne $pluginSettings.bgpDefaultPort) {
$bgpDefaultPort = [int]$pluginSettings.bgpDefaultPort
}
Write-Log -Level "STEP" -Message "Removing $($bgpServices.Count) BGP service(s) for release '$releaseName' ..."
Remove-BgpServices -Namespace $namespace -ScriptDir $settingsDir -ReleaseName $releaseName -BgpServices $bgpServices -DefaultPort $bgpDefaultPort
}
$deleteNamespace = $false
if ($Settings.PSObject.Properties.Name -contains 'deleteNamespace') {
$deleteNamespace = [bool]$Settings.deleteNamespace
}
if ($deleteNamespace) {
Write-Log -Level "STEP" -Message "Deleting namespace '$namespace' ..."
kubectl delete namespace $namespace --ignore-not-found 2>&1 | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "kubectl delete namespace failed for '$namespace' (exit code $LASTEXITCODE)."
}
}
Write-Log -Level "INFO" -Message "HelmUninstall completed for namespace '$namespace'."
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -0,0 +1,119 @@
#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 "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 "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)))
}
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)."
}
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'..."
$tarballName = (npm pack -w $packageName --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"
}
Write-Log -Level "OK" -Message " Package ready: $tarballPath"
$releaseAssetPaths += $tarballPath
}
}
finally {
Pop-Location
}
$shared | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
$shared | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $releaseAssetPaths -Force
if ($releaseAssetPaths.Count -gt 0) {
$shared | Add-Member -NotePropertyName packageFile -NotePropertyValue (Get-Item -LiteralPath $releaseAssetPaths[0]) -Force
}
Write-Log -Level "OK" -Message " npm pack completed ($($releaseAssetPaths.Count) tarball(s))."
}
Export-ModuleMember -Function Invoke-Plugin

View File

@ -6,9 +6,9 @@
Publishes npm workspace packages to the npm registry.
.DESCRIPTION
Publishes packages in configured order using an API key from an environment
variable (for example NPMJS_MAKS_IT). Uses a temporary .npmrc in the
workspace root for auth and supports --skip-duplicate semantics via npm.
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)) {
@ -32,18 +32,10 @@ function Invoke-Plugin {
$pluginSettings = $Settings
$shared = $Settings.context
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
Assert-Command npm
$npmApiKeyEnvVar = $pluginSettings.npmApiKey
if ([string]::IsNullOrWhiteSpace($npmApiKeyEnvVar)) {
throw "NpmPublish plugin requires 'npmApiKey' in scriptSettings.json (environment variable name)."
}
$npmApiKey = [System.Environment]::GetEnvironmentVariable($npmApiKeyEnvVar)
if ([string]::IsNullOrWhiteSpace($npmApiKey)) {
throw "npm API key is not set. Set '$npmApiKeyEnvVar' and rerun."
}
$workspaceRoot = $null
if ($pluginSettings.workspaceRoot) {
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $shared.scriptDir)
@ -84,11 +76,28 @@ function Invoke-Plugin {
throw "NpmPublish plugin requires non-empty 'publishOrder' (workspace package names)."
}
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=$npmApiKey
//$registryHost/:_authToken=$npmToken
"@
Push-Location $workspaceRoot
@ -115,4 +124,8 @@ registry=$registry
}
}
Export-ModuleMember -Function Invoke-Plugin
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -0,0 +1,47 @@
#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)"
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

View File

@ -0,0 +1,41 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Loads release version from a repo-root VERSION file into shared context.
.DESCRIPTION
Reads a single-line semver from the configured versionFilePath (default repo-root VERSION).
Used by container-only repos without .csproj or package.json.
#>
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 "EngineContext" -RequiredCommand "Resolve-FileReleaseVersion"
$shared = $Settings.context
$resolved = Resolve-FileReleaseVersion -Plugins @($Settings) -ScriptDir $shared.scriptDir
$versionFilePaths = @(Resolve-RelativePaths -Value $Settings.versionFilePath -BasePath $shared.scriptDir)
$shared | Add-Member -NotePropertyName version -NotePropertyValue $resolved.version -Force
if ($versionFilePaths.Count -gt 0) {
$shared | Add-Member -NotePropertyName versionFilePath -NotePropertyValue $versionFilePaths[0] -Force
}
Write-Log -Level "OK" -Message " Release version loaded by FileReleaseVersion plugin: $($shared.version)"
}
Export-ModuleMember -Function Invoke-Plugin

View File

@ -98,7 +98,7 @@ function Invoke-Plugin {
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$githubTokenEnvVar = $pluginSettings.githubToken
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret'
$configuredRepository = $pluginSettings.repository
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
@ -108,15 +108,37 @@ function Invoke-Plugin {
$releaseDir = $sharedSettings.releaseDir
$releaseAssetPaths = @()
Assert-Command gh
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
if ([string]::IsNullOrWhiteSpace($githubTokenEnvVar)) {
throw "GitHub plugin requires 'githubToken' in scriptSettings.json."
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
throw "GitHub plugin requires 'releaseNotesFile' in scriptSettings.json."
}
$githubToken = [System.Environment]::GetEnvironmentVariable($githubTokenEnvVar)
if ([string]::IsNullOrWhiteSpace($githubToken)) {
throw "GitHub token is not set. Set '$githubTokenEnvVar' and rerun."
$releaseNotesFile = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $releaseNotesFileSetting))
$releaseNotes = Get-ReleaseNotesInternal -ReleaseNotesFile $releaseNotesFile -Version $version
if ($dryRun) {
$repo = Get-GitHubRepositoryInternal -ConfiguredRepository $configuredRepository
$releaseTitlePattern = if ([string]::IsNullOrWhiteSpace($releaseTitlePatternSetting)) {
"Release {version}"
}
else {
$releaseTitlePatternSetting
}
$releaseName = $releaseTitlePattern -replace '\{version\}', $version
Write-Log -Level "INFO" -Message "Dry run: would create GitHub release '$releaseName' ($tag) on $repo"
return
}
Assert-Command gh
if ([string]::IsNullOrWhiteSpace($githubSecret)) {
throw "GitHub plugin requires 'githubSecret' in scriptSettings.json (logical secret name, e.g. GitHub)."
}
$ghToken = Get-SecretEnvironmentValue -Name $githubSecret
if ([string]::IsNullOrWhiteSpace($ghToken)) {
throw "GitHub token is not set. Set environment variable '$githubSecret'."
}
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
@ -163,7 +185,7 @@ function Invoke-Plugin {
Write-Log -Level "INFO" -Message " GitHub title: $releaseName"
$previousGhToken = $env:GH_TOKEN
$env:GH_TOKEN = $githubToken
$env:GH_TOKEN = $ghToken
try {
$ghVersion = & gh --version 2>&1
@ -171,7 +193,7 @@ function Invoke-Plugin {
Write-Log -Level "INFO" -Message " gh version: $($ghVersion[0])"
}
Write-Log -Level "INFO" -Message " Auth env var: $githubTokenEnvVar (set)"
Write-Log -Level "INFO" -Message " Auth secret: $githubSecret"
$authArgs = @("api", "repos/$repo", "--jq", ".full_name")
$authOutput = & gh @authArgs 2>&1
@ -188,7 +210,7 @@ function Invoke-Plugin {
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
}
throw "GitHub CLI authentication failed for repository '$repo'. Ensure '$githubTokenEnvVar' is valid and has access to this repository."
throw "GitHub CLI authentication failed for repository '$repo'. Ensure secret '$githubSecret' is valid and has access to this repository."
}
Write-Log -Level "OK" -Message " GitHub token validated for repository: $($authOutput | Select-Object -First 1)"
@ -241,4 +263,8 @@ function Invoke-Plugin {
}
}
Export-ModuleMember -Function Invoke-Plugin
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -3,7 +3,7 @@
<#
.SYNOPSIS
Central gate for publish-stage plugins (DotNetDockerPush, DotNetHelmPush, GitHub, DotNetNuGet, NpmPublish).
Central gate before remote-mutation plugins (see each plugin's Get-PluginMetadata).
.DESCRIPTION
Place this plugin immediately before any publish plugins in scriptSettings.json. It sets
@ -86,6 +86,11 @@ function Invoke-Plugin {
Write-Log -Level "STEP" -Message "Release publish guard..."
if ($shared.PSObject.Properties.Name -contains 'dryRun' -and [bool]$shared.dryRun) {
Write-Log -Level "INFO" -Message " Dry run: publish guard relaxed; publish plugins will validate only."
return
}
$allowed = @(Get-PluginBranches -Plugin $pluginSettings)
if ($allowed.Count -gt 0 -and $allowed -notcontains '*' -and $allowed -notcontains $shared.currentBranch) {
Invoke-NotMetInternal -Shared $shared -When $when -Reason "branch '$($shared.currentBranch)' is not in the guard branches list."

View File

@ -0,0 +1,35 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Podman Compose plugin — validate or bring up the stack via podman compose.
.DESCRIPTION
Requires ContainerEngineProbe first. Skips safely when activeContainerEngine is docker.
Orchestrator filtering (Compose-only) applies only when MAKSIT_ORCHESTRATOR is set (CI).
#>
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 'ComposeDeploySupport' -RequiredCommand 'Invoke-ComposeStackPlugin'
Invoke-ComposeStackPlugin -Settings $Settings -RequiredEngine 'podman' -PluginName 'PodmanCompose'
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true; orchestrators = @('Compose') }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -0,0 +1,44 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Podman image publish plugin — build and push images via podman (local podman CLI).
.DESCRIPTION
Requires ContainerEngineProbe first. Skips safely when activeContainerEngine is docker.
In CI, runs inside the spawned podman builder container (local nested podman, not CONTAINER_HOST).
Builds any Dockerfile listed in images[]; not limited to .NET projects.
#>
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 'Test-ShouldRunContainerEnginePlugin'
Import-PluginDependency -ModuleName 'ContainerImagePushSupport' -RequiredCommand 'Invoke-ContainerImagePush'
$shared = $Settings.context
if (-not (Test-ShouldRunContainerEnginePlugin -RequiredEngine 'podman' -SharedSettings $shared -PluginName 'PodmanImagePush')) {
return
}
Invoke-ContainerImagePush -Settings $Settings -PluginDisplayName 'PodmanImagePush'
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -1,3 +0,0 @@
@echo off
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0engines\release\Invoke-ReleasePackage.ps1" %*
pause

View File

@ -1,3 +0,0 @@
@echo off
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0engines\test\Invoke-TestEngine.ps1" %*
pause

View File

@ -1 +0,0 @@

View File

@ -1,59 +0,0 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"title": "Release Package Script Settings",
"description": "maksit-webui: npm workspace publish to npmjs. ReleasePublishGuard (before NpmPublish) controls allowed branches and tag rules. Set NPMJS_MAKS_IT env var to the npm automation token (same pattern as NUGET_MAKS_IT).",
"plugins": [
{
"name": "NpmReleaseVersion",
"stageLabel": "build",
"enabled": true,
"packageJsonPath": "..\\..\\..\\..\\src\\package.json",
"syncWorkspaceVersions": true
},
{
"name": "NpmBuild",
"stageLabel": "build",
"enabled": true,
"workspaceRoot": "..\\..\\..\\..\\src",
"useCi": true,
"buildScript": "build"
},
{
"name": "ReleasePublishGuard",
"stageLabel": "release",
"enabled": true,
"branches": [
"main"
],
"requireExactTagOnHead": true,
"tagVersionMustMatchReleaseVersion": true,
"whenRequirementsNotMet": "skip",
"requireCleanWorkingTree": false,
"ensureTagOnRemote": true,
"remoteName": "origin"
},
{
"name": "NpmPublish",
"stageLabel": "release",
"enabled": true,
"npmApiKey": "NPMJS_MAKS_IT",
"registry": "https://registry.npmjs.org",
"access": "public",
"workspaceRoot": "..\\..\\..\\..\\src",
"publishOrder": [
"@maks-it.com/webui-contracts",
"@maks-it.com/webui-core",
"@maks-it.com/webui-components"
]
}
],
"_comments": {
"plugins": {
"NpmReleaseVersion": "Reads version from src/package.json (workspace root). syncWorkspaceVersions aligns packages/*/package.json before build/publish.",
"NpmBuild": "Runs npm ci + npm run build in workspaceRoot.",
"ReleasePublishGuard": "Place before NpmPublish. tagVersionMustMatchReleaseVersion compares git tag on HEAD to NpmReleaseVersion.",
"NpmPublish": "npmApiKey is the environment variable name holding the npm automation token (NPMJS_MAKS_IT). publishOrder must follow dependency order.",
"maksit-repoutils": "Engine docs: https://github.com/MAKS-IT-COM/maksit-repoutils (src/engines/release/README.md)."
}
}
}

View File

@ -1,35 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
function Get-ScriptSettings {
param(
[Parameter(Mandatory = $true)]
[string]$ScriptDir,
[Parameter(Mandatory = $false)]
[string]$SettingsFileName = "scriptSettings.json"
)
$settingsPath = Join-Path $ScriptDir $SettingsFileName
if (-not (Test-Path $settingsPath -PathType Leaf)) {
Write-Error "Settings file not found: $settingsPath"
exit 1
}
return Get-Content $settingsPath -Raw | ConvertFrom-Json
}
function Assert-Command {
param(
[Parameter(Mandatory = $true)]
[string]$Command
)
if (-not (Get-Command $Command -ErrorAction SilentlyContinue)) {
Write-Error "Required command '$Command' is missing. Aborting."
exit 1
}
}
Export-ModuleMember -Function Get-ScriptSettings, Assert-Command

View File

@ -248,6 +248,11 @@ try {
}
if (-not $dryRun) {
$backupDirectory = Split-Path -Parent $backupPath
if (-not (Test-Path -Path $backupDirectory -PathType Container)) {
New-Item -ItemType Directory -Path $backupDirectory -Force | Out-Null
}
Copy-Item -Path $file.FullName -Destination $backupPath -Force
}
}