mirror of
https://github.com/MAKS-IT-COM/maksit-webui.git
synced 2026-11-15 08:16:11 +01:00
(bugfix): HOSTNAMES_ARRAY_POLICY and deepDelta fixes
Some checks are pending
Storybook tests / storybook-tests (push) Waiting to run
Some checks are pending
Storybook tests / storybook-tests (push) Waiting to run
This commit is contained in:
parent
0f2638d439
commit
b74e9250a7
22
CHANGELOG.md
22
CHANGELOG.md
@ -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).
|
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
|
## [0.3.3] - 2026-05-31
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
14
README.md
14
README.md
@ -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-core` | Utilities (`deepDelta`, enum helpers, ACL parsers) and `useFormState` |
|
||||||
| `@maks-it.com/webui-components` | React components, layout, editors, DataTable, auth shell |
|
| `@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
|
## 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.
|
**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
|
## 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).
|
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 |
|
| Plugin | Role |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| `NpmReleaseVersion` | Read semver from `src/package.json`; sync `packages/*/package.json` |
|
| `NpmReleaseVersion` | Read semver from `src/package.json`; sync `packages/*/package.json` |
|
||||||
| `NpmBuild` | `npm ci` + `npm run build` |
|
| `NpmBuild` | `npm ci` + `npm run build` |
|
||||||
| `ReleasePublishGuard` | Branch/tag checks before publish |
|
| `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 |
|
| `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
|
## Consume in product repos
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 72%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 78%">
|
||||||
<title>Branch Coverage: 72%</title>
|
<title>Branch Coverage: 78%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" 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">
|
<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 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 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 aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">78%</text>
|
||||||
<text x="127.5" y="14" fill="#fff">72%</text>
|
<text x="127.5" y="14" fill="#fff">78%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 75.2%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 82.2%">
|
||||||
<title>Line Coverage: 75.2%</title>
|
<title>Line Coverage: 82.2%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
@ -9,13 +9,13 @@
|
|||||||
</clipPath>
|
</clipPath>
|
||||||
<g clip-path="url(#r)">
|
<g clip-path="url(#r)">
|
||||||
<rect width="94.5" height="20" fill="#555"/>
|
<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)"/>
|
<rect width="137" height="20" fill="url(#s)"/>
|
||||||
</g>
|
</g>
|
||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<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 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 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 aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">82.2%</text>
|
||||||
<text x="115.75" y="14" fill="#fff">75.2%</text>
|
<text x="115.75" y="14" fill="#fff">82.2%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 66.7%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 62.4%">
|
||||||
<title>Method Coverage: 66.7%</title>
|
<title>Method Coverage: 62.4%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" 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">
|
<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 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 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 aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">62.4%</text>
|
||||||
<text x="128.75" y="14" fill="#fff">66.7%</text>
|
<text x="128.75" y="14" fill="#fff">62.4%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -17,7 +17,7 @@ Published packages:
|
|||||||
3. Create an **Automation** token (recommended) or Granular Access token with **Publish** on `@maks-it.com/*`:
|
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
|
- https://www.npmjs.com/settings/maks-it.com/tokens
|
||||||
4. Store the token for release tooling:
|
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:
|
- **Local one-off publish:** `npm login` or a user-level `~/.npmrc` entry:
|
||||||
```
|
```
|
||||||
//registry.npmjs.org/:_authToken=YOUR_TOKEN
|
//registry.npmjs.org/:_authToken=YOUR_TOKEN
|
||||||
@ -50,13 +50,13 @@ npm view @maks-it.com/webui-components version
|
|||||||
|
|
||||||
## Release pipeline (recommended)
|
## 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`).
|
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`.
|
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
|
## After publish — Certs UI / Vault
|
||||||
|
|
||||||
|
|||||||
@ -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\\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\\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\\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\\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\\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\\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\\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\\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\\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\\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\\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\\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\\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\\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\\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\\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}}
|
,"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
62
src/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "maksit-webui",
|
"name": "maksit-webui",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "maksit-webui",
|
"name": "maksit-webui",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@ -2326,6 +2326,29 @@
|
|||||||
"node": ">=14.0.0"
|
"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": {
|
"node_modules/@oxc-resolver/binding-win32-arm64-msvc": {
|
||||||
"version": "11.20.0",
|
"version": "11.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.20.0.tgz",
|
"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": ">=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": {
|
"node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
|
||||||
"version": "1.12.2",
|
"version": "1.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz",
|
"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": {
|
"packages/components": {
|
||||||
"name": "@maks-it.com/webui-components",
|
"name": "@maks-it.com/webui-components",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@maks-it.com/webui-contracts": "^0.3.3",
|
"@maks-it.com/webui-contracts": "^0.3.4",
|
||||||
"@maks-it.com/webui-core": "^0.3.3"
|
"@maks-it.com/webui-core": "^0.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
@ -10529,7 +10575,7 @@
|
|||||||
},
|
},
|
||||||
"packages/contracts": {
|
"packages/contracts": {
|
||||||
"name": "@maks-it.com/webui-contracts",
|
"name": "@maks-it.com/webui-contracts",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.5.1",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@ -10541,9 +10587,9 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@maks-it.com/webui-core",
|
"name": "@maks-it.com/webui-core",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@maks-it.com/webui-contracts": "^0.3.3",
|
"@maks-it.com/webui-contracts": "^0.3.4",
|
||||||
"date-fns": "^4.3.0"
|
"date-fns": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "maksit-webui",
|
"name": "maksit-webui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"description": "Shared React UI library for MaksIT Certs UI and Vault WebUI",
|
"description": "Shared React UI library for MaksIT Certs UI and Vault WebUI",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|||||||
@ -25,6 +25,9 @@ npm install react react-dom react-router-dom lucide-react @tanstack/react-table
|
|||||||
| `EntityScopesSummary` | Entity scope permissions summary |
|
| `EntityScopesSummary` | Entity scope permissions summary |
|
||||||
| `Toast`, `addToast` | Toast notifications |
|
| `Toast`, `addToast` | Toast notifications |
|
||||||
| `FieldContainer` | Label + validation wrapper for fields |
|
| `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
|
## Example — DataTable
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@maks-it.com/webui-components",
|
"name": "@maks-it.com/webui-components",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"description": "Shared React components for MaksIT WebUI apps",
|
"description": "Shared React components for MaksIT WebUI apps",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.cjs",
|
"main": "./dist/index.cjs",
|
||||||
@ -33,8 +33,8 @@
|
|||||||
"directory": "src/packages/components"
|
"directory": "src/packages/components"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@maks-it.com/webui-contracts": "^0.3.3",
|
"@maks-it.com/webui-contracts": "^0.3.4",
|
||||||
"@maks-it.com/webui-core": "^0.3.3"
|
"@maks-it.com/webui-core": "^0.3.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tanstack/react-table": "^8.0.0",
|
"@tanstack/react-table": "^8.0.0",
|
||||||
|
|||||||
@ -30,3 +30,4 @@ export { Toast as ToastContainer } from './components/Toast'
|
|||||||
export { EntityScopesSummary } from './components/Scopes'
|
export { EntityScopesSummary } from './components/Scopes'
|
||||||
export type { EntityScopesSummaryProps } from './components/Scopes'
|
export type { EntityScopesSummaryProps } from './components/Scopes'
|
||||||
export { Layout } from './components/Layout'
|
export { Layout } from './components/Layout'
|
||||||
|
export * from './oauth'
|
||||||
|
|||||||
@ -19,6 +19,7 @@ npm install @maks-it.com/webui-contracts zod
|
|||||||
| API errors | `ProblemDetails` |
|
| API errors | `ProblemDetails` |
|
||||||
| Search | `SearchResponseBase`, `SearchEntityScopeEntry` |
|
| Search | `SearchResponseBase`, `SearchEntityScopeEntry` |
|
||||||
| Identity | `LoginRequest` / `LoginRequestSchema`, `LoginResponse`, `RefreshTokenRequest`, `LogoutRequest`, `Claims` |
|
| Identity | `LoginRequest` / `LoginRequestSchema`, `LoginResponse`, `RefreshTokenRequest`, `LogoutRequest`, `Claims` |
|
||||||
|
| OAuth / IdP | `LoginProviderExternal`, `EmailProtocol`, `ServerAuthenticationMethod`, `IdpRedeemLoginResponse`, `MailboxOAuthPending`, mailbox OAuth constants |
|
||||||
| Misc | `TrngResponse`, `RequestModelBase`, `ResponseModelBase` |
|
| Misc | `TrngResponse`, `RequestModelBase`, `ResponseModelBase` |
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@maks-it.com/webui-contracts",
|
"name": "@maks-it.com/webui-contracts",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"description": "Shared TypeScript contracts for MaksIT WebUI apps",
|
"description": "Shared TypeScript contracts for MaksIT WebUI apps",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.cjs",
|
"main": "./dist/index.cjs",
|
||||||
|
|||||||
@ -32,3 +32,5 @@ export { RefreshTokenRequestSchema } from './identity/login/RefreshTokenRequest'
|
|||||||
export type { LogoutRequest } from './identity/logout/LogoutRequest'
|
export type { LogoutRequest } from './identity/logout/LogoutRequest'
|
||||||
export { LogoutRequestSchema } from './identity/logout/LogoutRequest'
|
export { LogoutRequestSchema } from './identity/logout/LogoutRequest'
|
||||||
export type { LogoutResponse } from './identity/logout/LogoutResponse'
|
export type { LogoutResponse } from './identity/logout/LogoutResponse'
|
||||||
|
|
||||||
|
export * from './oauth'
|
||||||
|
|||||||
@ -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 |
|
| HTTP | `createWebUiHttpClient`, auth interceptors, Problem Details helpers |
|
||||||
| SignalR | `useWebUiHub` |
|
| SignalR | `useWebUiHub` |
|
||||||
| Enum / flags | `enumToArr`, `flagsToString`, `hasFlag`, `toggleFlag` |
|
| Enum / flags | `enumToArr`, `flagsToString`, `hasFlag`, `toggleFlag` |
|
||||||
|
| PATCH collections | `ENTITY_SCOPES_ARRAY_POLICY`, `HOSTNAMES_ARRAY_POLICY`, `VERSIONS_ARRAY_POLICY` |
|
||||||
| Identity storage | `readIdentity`, `writeIdentity`, `removeIdentity` |
|
| Identity storage | `readIdentity`, `writeIdentity`, `removeIdentity` |
|
||||||
|
| OAuth / IdP | `startIdpRedirect`, `useIdpLoginCallback`, `collectMailboxOAuthProtocols`, `startMailboxOAuthRedirect`, `useMailboxOAuthReturn` |
|
||||||
|
|
||||||
## Example — form state
|
## Example — form state
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@maks-it.com/webui-core",
|
"name": "@maks-it.com/webui-core",
|
||||||
"version": "0.3.3",
|
"version": "0.3.4",
|
||||||
"description": "Shared utilities and hooks for MaksIT WebUI apps",
|
"description": "Shared utilities and hooks for MaksIT WebUI apps",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.cjs",
|
"main": "./dist/index.cjs",
|
||||||
@ -34,7 +34,7 @@
|
|||||||
"directory": "src/packages/core"
|
"directory": "src/packages/core"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@maks-it.com/webui-contracts": "^0.3.3",
|
"@maks-it.com/webui-contracts": "^0.3.4",
|
||||||
"date-fns": "^4.3.0"
|
"date-fns": "^4.3.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@ -437,7 +437,7 @@ export const deepDelta = <T extends Record<string, unknown>>(
|
|||||||
const roleBecameNull = backupRole !== null && formRole === null
|
const roleBecameNull = backupRole !== null && formRole === null
|
||||||
if (roleBecameNull) {
|
if (roleBecameNull) {
|
||||||
const removeItem = {} as DeltaArrayItem<U>
|
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 }
|
removeItem.operations = { [COLLECTION_ITEM_OPERATION]: PatchOperation.RemoveFromCollection }
|
||||||
arrayDelta.push(removeItem)
|
arrayDelta.push(removeItem)
|
||||||
continue
|
continue
|
||||||
@ -446,7 +446,8 @@ export const deepDelta = <T extends Record<string, unknown>>(
|
|||||||
|
|
||||||
// 1.e) Field-level diff
|
// 1.e) Field-level diff
|
||||||
const itemDeltaBase = {} as (PlainObject & OperationBag & { id?: U['id'] })
|
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(
|
calculateDelta(
|
||||||
formItem as PlainObject,
|
formItem as PlainObject,
|
||||||
@ -454,7 +455,9 @@ export const deepDelta = <T extends Record<string, unknown>>(
|
|||||||
itemDeltaBase
|
itemDeltaBase
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasMeaningfulChanges = Object.keys(itemDeltaBase).some(k => k !== idFieldKey)
|
const hasMeaningfulChanges = Object.keys(itemDeltaBase).some(
|
||||||
|
k => k !== identityField && k !== 'operations'
|
||||||
|
)
|
||||||
if (hasMeaningfulChanges) {
|
if (hasMeaningfulChanges) {
|
||||||
arrayDelta.push(itemDeltaBase as DeltaArrayItem<U>)
|
arrayDelta.push(itemDeltaBase as DeltaArrayItem<U>)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
} from './deepDelta'
|
} from './deepDelta'
|
||||||
import {
|
import {
|
||||||
ENTITY_SCOPES_ARRAY_POLICY,
|
ENTITY_SCOPES_ARRAY_POLICY,
|
||||||
|
HOSTNAMES_ARRAY_POLICY,
|
||||||
VERSIONS_ARRAY_POLICY,
|
VERSIONS_ARRAY_POLICY,
|
||||||
} from './patchCollectionPolicies'
|
} from './patchCollectionPolicies'
|
||||||
import { deepEqualArrays, deepEqual } from './deepEqual'
|
import { deepEqualArrays, deepEqual } from './deepEqual'
|
||||||
@ -17,6 +18,7 @@ export {
|
|||||||
deepDelta,
|
deepDelta,
|
||||||
deltaHasOperations,
|
deltaHasOperations,
|
||||||
ENTITY_SCOPES_ARRAY_POLICY,
|
ENTITY_SCOPES_ARRAY_POLICY,
|
||||||
|
HOSTNAMES_ARRAY_POLICY,
|
||||||
VERSIONS_ARRAY_POLICY,
|
VERSIONS_ARRAY_POLICY,
|
||||||
deepEqualArrays,
|
deepEqualArrays,
|
||||||
deepEqual,
|
deepEqual,
|
||||||
|
|||||||
@ -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
|
* Uses `_deltaId` in the delta payload when items lack a server `id`, avoiding
|
||||||
* invalid Guid values in the `id` field expected by the API.
|
* invalid Guid values in the `id` field expected by the API.
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
deepDelta,
|
deepDelta,
|
||||||
deltaHasOperations,
|
deltaHasOperations,
|
||||||
ENTITY_SCOPES_ARRAY_POLICY,
|
ENTITY_SCOPES_ARRAY_POLICY,
|
||||||
|
HOSTNAMES_ARRAY_POLICY,
|
||||||
VERSIONS_ARRAY_POLICY,
|
VERSIONS_ARRAY_POLICY,
|
||||||
deepEqual,
|
deepEqual,
|
||||||
deepEqualArrays,
|
deepEqualArrays,
|
||||||
@ -63,6 +64,7 @@ export {
|
|||||||
deepDelta,
|
deepDelta,
|
||||||
deltaHasOperations,
|
deltaHasOperations,
|
||||||
ENTITY_SCOPES_ARRAY_POLICY,
|
ENTITY_SCOPES_ARRAY_POLICY,
|
||||||
|
HOSTNAMES_ARRAY_POLICY,
|
||||||
VERSIONS_ARRAY_POLICY,
|
VERSIONS_ARRAY_POLICY,
|
||||||
deepEqual,
|
deepEqual,
|
||||||
deepEqualArrays,
|
deepEqualArrays,
|
||||||
|
|||||||
@ -4,3 +4,4 @@ export * from './http'
|
|||||||
export * from './signalr'
|
export * from './signalr'
|
||||||
export { readIdentity, writeIdentity, removeIdentity } from './localStorage/identity'
|
export { readIdentity, writeIdentity, removeIdentity } from './localStorage/identity'
|
||||||
export { useFormState } from './hooks/useFormState'
|
export { useFormState } from './hooks/useFormState'
|
||||||
|
export * from './oauth'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
4
utils/Invoke-ReleasePackage-HA.bat
Normal file
4
utils/Invoke-ReleasePackage-HA.bat
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
pwsh -NoProfile -File "%~dp0engines\release\Invoke-ReleasePackage.ps1" -Mode ha %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
4
utils/Invoke-ReleasePackage-Single.bat
Normal file
4
utils/Invoke-ReleasePackage-Single.bat
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
pwsh -NoProfile -File "%~dp0engines\release\Invoke-ReleasePackage.ps1" -Mode single %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
4
utils/Invoke-TestEngine.bat
Normal file
4
utils/Invoke-TestEngine.bat
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
pwsh -NoProfile -File "%~dp0engines\test\Invoke-TestEngine.ps1" %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
4
utils/Uninstall-Helm-HA.bat
Normal file
4
utils/Uninstall-Helm-HA.bat
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
pwsh -NoProfile -File "%~dp0engines\uninstall\Invoke-UninstallEngine.ps1" -Mode ha %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
4
utils/Uninstall-Helm-Single.bat
Normal file
4
utils/Uninstall-Helm-Single.bat
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
pwsh -NoProfile -File "%~dp0engines\uninstall\Invoke-UninstallEngine.ps1" -Mode single %*
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
@ -4,23 +4,46 @@
|
|||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Plugin-driven release engine entry script.
|
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
|
[CmdletBinding()]
|
||||||
$srcDir = (Resolve-Path (Join-Path $scriptDir '..\..')).Path
|
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')
|
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
|
||||||
Import-EngineModules -Engine Release
|
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
|
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||||
|
|
||||||
Write-Log -Level 'STEP' -Message '=================================================='
|
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 '=================================================='
|
Write-Log -Level 'STEP' -Message '=================================================='
|
||||||
|
|
||||||
$plugins = $configuredPlugins
|
$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!'
|
Write-Log -Level 'OK' -Message 'All pre-flight checks passed!'
|
||||||
$sharedPluginSettings = $engineContext
|
$sharedPluginSettings = $engineContext
|
||||||
|
|
||||||
@ -34,15 +57,15 @@ else {
|
|||||||
for ($pluginIndex = 0; $pluginIndex -lt $plugins.Count; $pluginIndex++) {
|
for ($pluginIndex = 0; $pluginIndex -lt $plugins.Count; $pluginIndex++) {
|
||||||
$plugin = $plugins[$pluginIndex]
|
$plugin = $plugins[$pluginIndex]
|
||||||
|
|
||||||
if ((Test-IsPublishPlugin -Plugin $plugin) -and -not $releaseStageInitialized) {
|
if ((Test-IsPublishPlugin -Plugin $plugin -EngineDirectory $PSScriptRoot) -and -not $releaseStageInitialized) {
|
||||||
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $scriptDir -WriteLogs:$false) {
|
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -WriteLogs:$false) {
|
||||||
$remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)])
|
$remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)])
|
||||||
Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory -Version $engineContext.version
|
Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory -Version $engineContext.version
|
||||||
$releaseStageInitialized = $true
|
$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) {
|
if (-not $pluginSucceeded) {
|
||||||
$releaseHadPluginFailures = $true
|
$releaseHadPluginFailures = $true
|
||||||
break
|
break
|
||||||
@ -52,7 +75,7 @@ else {
|
|||||||
|
|
||||||
if (-not $releaseStageInitialized) {
|
if (-not $releaseStageInitialized) {
|
||||||
$noReleasePluginsLogLevel = if ($engineContext.isNonReleaseBranch) { 'INFO' } else { 'WARN' }
|
$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 '=================================================='
|
Write-Log -Level 'OK' -Message '=================================================='
|
||||||
@ -65,6 +88,9 @@ elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -
|
|||||||
elseif ($engineContext.isNonReleaseBranch) {
|
elseif ($engineContext.isNonReleaseBranch) {
|
||||||
Write-Log -Level 'OK' -Message 'NON-RELEASE RUN COMPLETE'
|
Write-Log -Level 'OK' -Message 'NON-RELEASE RUN COMPLETE'
|
||||||
}
|
}
|
||||||
|
elseif ($engineContext.dryRun) {
|
||||||
|
Write-Log -Level 'OK' -Message 'DRY RUN COMPLETE'
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
Write-Log -Level 'OK' -Message 'RELEASE COMPLETE'
|
Write-Log -Level 'OK' -Message 'RELEASE COMPLETE'
|
||||||
}
|
}
|
||||||
70
utils/engines/release/scriptSettings.json
Normal file
70
utils/engines/release/scriptSettings.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft-07/schema",
|
"$schema": "https://json-schema.org/draft-07/schema",
|
||||||
"title": "Run Tests Script Settings",
|
"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": {
|
"paths": {
|
||||||
"badgesDir": "..\\..\\..\\..\\assets\\badges"
|
"badgesDir": "..\\..\\..\\assets\\badges"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "NpmJestTest",
|
"name": "NpmJestTest",
|
||||||
"stageLabel": "test",
|
"stageLabel": "test",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"workspaceRoot": "..\\..\\..\\..\\src",
|
"workspaceRoot": "..\\..\\..\\src",
|
||||||
"testScript": "test",
|
"testScript": "test:coverage",
|
||||||
"coverageDirectory": "coverage"
|
"coverageDirectory": "coverage"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -25,7 +25,7 @@
|
|||||||
"name": "CoverageBadges",
|
"name": "CoverageBadges",
|
||||||
"stageLabel": "report",
|
"stageLabel": "report",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"badgesDir": "..\\..\\..\\..\\assets\\badges",
|
"badgesDir": "..\\..\\..\\assets\\badges",
|
||||||
"badges": [
|
"badges": [
|
||||||
{
|
{
|
||||||
"name": "coverage-lines.svg",
|
"name": "coverage-lines.svg",
|
||||||
@ -52,12 +52,5 @@
|
|||||||
"red": 0
|
"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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
68
utils/engines/uninstall/Invoke-UninstallEngine.ps1
Normal file
68
utils/engines/uninstall/Invoke-UninstallEngine.ps1
Normal 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
|
||||||
|
}
|
||||||
13
utils/engines/uninstall/scriptSettings.json
Normal file
13
utils/engines/uninstall/scriptSettings.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
189
utils/modules/ComposeDeploySupport.psm1
Normal file
189
utils/modules/ComposeDeploySupport.psm1
Normal 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
|
||||||
320
utils/modules/ContainerEngineSupport.psm1
Normal file
320
utils/modules/ContainerEngineSupport.psm1
Normal 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
|
||||||
@ -3,52 +3,15 @@
|
|||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
.NET Docker publish plugin — build and push container images for .NET apps.
|
Shared container image build/push logic for DockerImagePush and PodmanImagePush.
|
||||||
|
|
||||||
.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.
|
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
$containerEngineSupportModulePath = Join-Path $PSScriptRoot 'ContainerEngineSupport.psm1'
|
||||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
if (Test-Path $containerEngineSupportModulePath -PathType Leaf) {
|
||||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
Import-Module $containerEngineSupportModulePath -Force -Global
|
||||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
|
||||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-RegistryCredentialsFromEnv {
|
function Set-ContainerImageEnvVersionValue {
|
||||||
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 {
|
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$FilePath,
|
[string]$FilePath,
|
||||||
@ -69,48 +32,52 @@ function Set-EnvVersionValue {
|
|||||||
Set-Content -LiteralPath $FilePath -Value $content -NoNewline
|
Set-Content -LiteralPath $FilePath -Value $content -NoNewline
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-Plugin {
|
function Invoke-ContainerImagePush {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
$Settings
|
$Settings,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$PluginDisplayName
|
||||||
)
|
)
|
||||||
|
|
||||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
Import-PluginDependency -ModuleName 'ContainerEngineSupport' -RequiredCommand 'Get-SharedContainerEngineInvocation'
|
||||||
|
|
||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$shared = $Settings.context
|
$shared = $Settings.context
|
||||||
|
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
|
||||||
Assert-Command docker
|
$engine = Get-SharedContainerEngineInvocation -SharedSettings $shared
|
||||||
|
$engineName = Get-ContainerEngineDisplayName -Engine $engine
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.registryUrl)) {
|
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)) {
|
$containerRegistrySecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'containerRegistrySecret'
|
||||||
throw "DotNetDockerPush plugin requires 'credentialsEnvVar' (name of env var holding base64 username:password)."
|
if ([string]::IsNullOrWhiteSpace($containerRegistrySecret)) {
|
||||||
|
throw "$PluginDisplayName requires 'containerRegistrySecret' (logical secret name, e.g. ContainerRegistry)."
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.projectName)) {
|
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)) {
|
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) {
|
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
|
$scriptDir = $shared.scriptDir
|
||||||
$contextPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$pluginSettings.contextPath)))
|
$contextPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$pluginSettings.contextPath)))
|
||||||
if (-not (Test-Path $contextPath -PathType Container)) {
|
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('/')
|
$registryUrl = [string]$pluginSettings.registryUrl.TrimEnd('/')
|
||||||
$creds = Get-RegistryCredentialsFromEnv -EnvVarName ([string]$pluginSettings.credentialsEnvVar)
|
|
||||||
|
|
||||||
$bareVersion = $null
|
$bareVersion = $null
|
||||||
if ($shared.PSObject.Properties.Name -contains 'version' -and -not [string]::IsNullOrWhiteSpace([string]$shared.version)) {
|
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]', ''
|
$bareVersion = ([string]$shared.tag).Trim() -replace '^[vV]', ''
|
||||||
}
|
}
|
||||||
if ([string]::IsNullOrWhiteSpace($bareVersion)) {
|
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]
|
$imageTags = New-Object System.Collections.Generic.List[string]
|
||||||
function Add-ImageTag([System.Collections.Generic.List[string]]$List, [string]$Tag) {
|
function Add-ImageTag([System.Collections.Generic.List[string]]$List, [string]$Tag) {
|
||||||
if ([string]::IsNullOrWhiteSpace($Tag)) { return }
|
if ([string]::IsNullOrWhiteSpace($Tag)) { return }
|
||||||
@ -138,10 +133,10 @@ function Invoke-Plugin {
|
|||||||
Add-ImageTag $imageTags 'latest'
|
Add-ImageTag $imageTags 'latest'
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Docker login to $registryUrl..."
|
Write-Log -Level 'STEP' -Message "Container login to $registryUrl via $engineName..."
|
||||||
$loginResult = $creds.Password | docker login $registryUrl -u $creds.User --password-stdin 2>&1
|
$loginResult = Invoke-ContainerEngineWithInput -Engine $engine -InputObject $creds.Password login $registryUrl -u $creds.User --password-stdin
|
||||||
if ($LASTEXITCODE -ne 0 -or ($loginResult -notmatch 'Login Succeeded')) {
|
if ($LASTEXITCODE -ne 0 -or ($loginResult -notmatch 'Login Succeeded')) {
|
||||||
throw "Docker login failed for ${registryUrl}: $loginResult"
|
throw "Container login failed for ${registryUrl}: $loginResult"
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -157,7 +152,7 @@ function Invoke-Plugin {
|
|||||||
if ($img.PSObject.Properties.Name -contains 'contextPath' -and -not [string]::IsNullOrWhiteSpace([string]$img.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)))
|
$imgContextPath = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$img.contextPath)))
|
||||||
if (-not (Test-Path $imgContextPath -PathType Container)) {
|
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"
|
$backupPath = "$envFilePath.repoutils.bak"
|
||||||
Copy-Item -LiteralPath $envFilePath -Destination $backupPath -Force
|
Copy-Item -LiteralPath $envFilePath -Destination $backupPath -Force
|
||||||
$versionEnvFiles += [pscustomobject]@{
|
$versionEnvFiles += [pscustomobject]@{
|
||||||
FilePath = $envFilePath
|
FilePath = $envFilePath
|
||||||
BackupPath = $backupPath
|
BackupPath = $backupPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,33 +185,33 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
foreach ($envFile in $versionEnvFiles) {
|
foreach ($envFile in $versionEnvFiles) {
|
||||||
Write-Log -Level "INFO" -Message "Temporarily setting VITE_APP_VERSION=$bareVersion in $($envFile.FilePath)"
|
Write-Log -Level 'INFO' -Message "Temporarily setting VITE_APP_VERSION=$bareVersion in $($envFile.FilePath)"
|
||||||
Set-EnvVersionValue -FilePath $envFile.FilePath -Version $bareVersion
|
Set-ContainerImageEnvVersionValue -FilePath $envFile.FilePath -Version $bareVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
$primaryRef = "${baseName}:$($imageTags[0])"
|
$primaryRef = "${baseName}:$($imageTags[0])"
|
||||||
Write-Log -Level "STEP" -Message "Building $primaryRef ..."
|
Write-Log -Level 'STEP' -Message "Building $primaryRef via $engineName ..."
|
||||||
docker build -t $primaryRef -f $dockerfilePath $imgContextPath
|
$buildExitCode = Invoke-ContainerEngine -Engine $engine build -t $primaryRef -f $dockerfilePath $imgContextPath
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($buildExitCode -ne 0) {
|
||||||
throw "Docker build failed for $primaryRef"
|
throw "Container build failed for $primaryRef"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Pushing $primaryRef ..."
|
Write-Log -Level 'STEP' -Message "Pushing $primaryRef via $engineName ..."
|
||||||
docker push $primaryRef
|
$pushExitCode = Invoke-ContainerEngine -Engine $engine push $primaryRef
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($pushExitCode -ne 0) {
|
||||||
throw "Docker push failed for $primaryRef"
|
throw "Container push failed for $primaryRef"
|
||||||
}
|
}
|
||||||
|
|
||||||
for ($ti = 1; $ti -lt $imageTags.Count; $ti++) {
|
for ($ti = 1; $ti -lt $imageTags.Count; $ti++) {
|
||||||
$aliasRef = "${baseName}:$($imageTags[$ti])"
|
$aliasRef = "${baseName}:$($imageTags[$ti])"
|
||||||
Write-Log -Level "STEP" -Message "Tagging and pushing $aliasRef ..."
|
Write-Log -Level 'STEP' -Message "Tagging and pushing $aliasRef via $engineName ..."
|
||||||
docker tag $primaryRef $aliasRef
|
$tagExitCode = Invoke-ContainerEngine -Engine $engine tag $primaryRef $aliasRef
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($tagExitCode -ne 0) {
|
||||||
throw "Docker tag failed: $primaryRef -> $aliasRef"
|
throw "Container tag failed: $primaryRef -> $aliasRef"
|
||||||
}
|
}
|
||||||
docker push $aliasRef
|
$aliasPushExitCode = Invoke-ContainerEngine -Engine $engine push $aliasRef
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($aliasPushExitCode -ne 0) {
|
||||||
throw "Docker push failed for $aliasRef"
|
throw "Container push failed for $aliasRef"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -235,11 +230,11 @@ function Invoke-Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
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
|
$shared | Add-Member -NotePropertyName publishCompleted -NotePropertyValue $true -Force
|
||||||
}
|
}
|
||||||
|
|
||||||
Export-ModuleMember -Function Invoke-Plugin
|
Export-ModuleMember -Function Invoke-ContainerImagePush
|
||||||
@ -9,6 +9,7 @@
|
|||||||
Used by New-EngineContext and version plugins:
|
Used by New-EngineContext and version plugins:
|
||||||
- DotNetReleaseVersion plugin -> projectFiles (.csproj <Version>)
|
- DotNetReleaseVersion plugin -> projectFiles (.csproj <Version>)
|
||||||
- NpmReleaseVersion plugin -> packageJsonPath (package.json version)
|
- NpmReleaseVersion plugin -> packageJsonPath (package.json version)
|
||||||
|
- FileReleaseVersion plugin -> versionFilePath (repo-root VERSION file)
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
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 {
|
function Resolve-ReleaseVersion {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -201,9 +268,15 @@ function Resolve-ReleaseVersion {
|
|||||||
|
|
||||||
$dotnetPlugin = @($Plugins | Where-Object { $_.name -eq 'DotNetReleaseVersion' -and $_.enabled -ne $false })
|
$dotnetPlugin = @($Plugins | Where-Object { $_.name -eq 'DotNetReleaseVersion' -and $_.enabled -ne $false })
|
||||||
$npmPlugin = @($Plugins | Where-Object { $_.name -eq 'NpmReleaseVersion' -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) {
|
$enabledVersionPlugins = @()
|
||||||
Write-Error "Configure only one release version plugin: DotNetReleaseVersion or NpmReleaseVersion, not both."
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,11 +288,15 @@ function Resolve-ReleaseVersion {
|
|||||||
return Resolve-NpmReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
|
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
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -117,17 +117,265 @@ function Test-PluginAllowedOnBranch {
|
|||||||
return $allowedBranches -contains $CurrentBranch
|
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(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[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)) {
|
if ($null -eq $Plugin -or [string]::IsNullOrWhiteSpace([string]$Plugin.name)) {
|
||||||
return $false
|
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 {
|
function Get-PluginSettingValue {
|
||||||
@ -266,6 +514,9 @@ function Resolve-PluginModulePath {
|
|||||||
$candidatePaths = @(
|
$candidatePaths = @(
|
||||||
(Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName),
|
(Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName),
|
||||||
(Join-Path (Join-Path $pluginsRoot "Platform") $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 "DotNet") $pluginFileName),
|
||||||
(Join-Path (Join-Path $pluginsRoot "Npm") $pluginFileName)
|
(Join-Path (Join-Path $pluginsRoot "Npm") $pluginFileName)
|
||||||
)
|
)
|
||||||
@ -365,6 +616,10 @@ function Invoke-ConfiguredPlugin {
|
|||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (-not (Test-PluginSupportsOrchestrator -Plugin $Plugin -EngineDirectory $EngineDirectory -WriteLogs:$true)) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
|
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
|
||||||
Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.name)'..."
|
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
|
||||||
@ -78,7 +78,12 @@ function New-EngineContext {
|
|||||||
[string]$SrcDir,
|
[string]$SrcDir,
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[psobject]$Settings
|
[psobject]$Settings,
|
||||||
|
|
||||||
|
[switch]$DryRun,
|
||||||
|
|
||||||
|
[ValidateSet('single', 'ha')]
|
||||||
|
[string]$DeployMode = 'ha'
|
||||||
)
|
)
|
||||||
|
|
||||||
$resolvedVersion = Resolve-ReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
|
$resolvedVersion = Resolve-ReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
|
||||||
@ -119,6 +124,23 @@ function New-EngineContext {
|
|||||||
$tag = "v$version"
|
$tag = "v$version"
|
||||||
Write-Log -Level "INFO" -Message " Release tag default from ${versionSource}: $tag (ReleasePublishGuard may replace from git when publish is allowed)."
|
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]@{
|
return [pscustomobject]@{
|
||||||
scriptDir = $ScriptDir
|
scriptDir = $ScriptDir
|
||||||
srcDir = $SrcDir
|
srcDir = $SrcDir
|
||||||
@ -132,6 +154,9 @@ function New-EngineContext {
|
|||||||
releaseBranches = $releaseBranches
|
releaseBranches = $releaseBranches
|
||||||
publishCompleted = $false
|
publishCompleted = $false
|
||||||
skipPublishPlugins = $false
|
skipPublishPlugins = $false
|
||||||
|
dryRun = $dryRun
|
||||||
|
deployMode = $DeployMode
|
||||||
|
orchestrator = $orchestrator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,7 +19,10 @@ function New-EngineContext {
|
|||||||
[string]$SrcDir,
|
[string]$SrcDir,
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[psobject]$Settings
|
[psobject]$Settings,
|
||||||
|
|
||||||
|
[ValidateSet('single', 'ha')]
|
||||||
|
[string]$DeployMode = 'ha'
|
||||||
)
|
)
|
||||||
|
|
||||||
$badgesDir = $null
|
$badgesDir = $null
|
||||||
@ -32,6 +35,7 @@ function New-EngineContext {
|
|||||||
srcDir = $SrcDir
|
srcDir = $SrcDir
|
||||||
utilsDir = $SrcDir
|
utilsDir = $SrcDir
|
||||||
badgesDir = $badgesDir
|
badgesDir = $badgesDir
|
||||||
|
deployMode = $DeployMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
631
utils/modules/HelmDeploySupport.psm1
Normal file
631
utils/modules/HelmDeploySupport.psm1
Normal 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
|
||||||
135
utils/modules/ScriptConfig.psm1
Normal file
135
utils/modules/ScriptConfig.psm1
Normal 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
|
||||||
35
utils/plugins/Docker/DockerCompose.psm1
Normal file
35
utils/plugins/Docker/DockerCompose.psm1
Normal 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
|
||||||
43
utils/plugins/Docker/DockerImagePush.psm1
Normal file
43
utils/plugins/Docker/DockerImagePush.psm1
Normal 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
|
||||||
@ -29,22 +29,36 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$sharedSettings = $Settings.context
|
$sharedSettings = $Settings.context
|
||||||
$nugetApiKeyEnvVar = $pluginSettings.nugetApiKey
|
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret'
|
||||||
$packageFile = $sharedSettings.packageFile
|
$packageFile = $sharedSettings.packageFile
|
||||||
|
|
||||||
|
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
||||||
|
|
||||||
Assert-Command dotnet
|
Assert-Command dotnet
|
||||||
|
|
||||||
if (-not $packageFile) {
|
if (-not $packageFile) {
|
||||||
throw "DotNetNuGet plugin requires a NuGet package artifact. Ensure DotNetPack produced a .nupkg before running DotNetNuGet."
|
throw "DotNetNuGet plugin requires a NuGet package artifact. Ensure DotNetPack produced a .nupkg before running DotNetNuGet."
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($nugetApiKeyEnvVar)) {
|
if ($dryRun) {
|
||||||
throw "DotNetNuGet plugin requires 'nugetApiKey' in scriptSettings.json."
|
$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($nugetSecret)) {
|
||||||
if ([string]::IsNullOrWhiteSpace($nugetApiKey)) {
|
throw "DotNetNuGet plugin requires 'nugetSecret' in scriptSettings.json (logical secret name, e.g. NuGet)."
|
||||||
throw "NuGet API key is not set. Set '$nugetApiKeyEnvVar' and rerun."
|
}
|
||||||
|
|
||||||
|
$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)) {
|
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
|
||||||
@ -55,7 +69,7 @@ function Invoke-Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-Log -Level "STEP" -Message "Pushing package to NuGet feed..."
|
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) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "Failed to push the package to NuGet feed."
|
throw "Failed to push the package to NuGet feed."
|
||||||
@ -65,7 +79,11 @@ function Invoke-Plugin {
|
|||||||
$sharedSettings | Add-Member -NotePropertyName publishCompleted -NotePropertyValue $true -Force
|
$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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.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
|
.DESCRIPTION
|
||||||
The chart in the repo should keep placeholder version and appVersion (e.g. 0.0.0); this plugin
|
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,
|
overwrites them with the bare semver from shared context (shared.version from a release-version
|
||||||
e.g. 3.3.4 — no leading v), falling back to stripping v/V from shared.tag if version is missing,
|
plugin, e.g. DotNetReleaseVersion or NpmReleaseVersion — no leading v), falling back to stripping
|
||||||
then runs helm package and helm push, then restores Chart.yaml.
|
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
|
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.
|
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)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
$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) {
|
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
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 {
|
function Invoke-Plugin {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
@ -61,20 +35,25 @@ function Invoke-Plugin {
|
|||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$shared = $Settings.context
|
$shared = $Settings.context
|
||||||
|
|
||||||
|
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
|
||||||
|
|
||||||
Assert-Command helm
|
Assert-Command helm
|
||||||
|
|
||||||
$pushLatest = if ($null -ne $pluginSettings.pushLatest) { [bool]$pluginSettings.pushLatest } else { $false }
|
$pushLatest = if ($null -ne $pluginSettings.pushLatest) { [bool]$pluginSettings.pushLatest } else { $false }
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.chartPath)) {
|
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)) {
|
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)) {
|
if (-not $dryRun) {
|
||||||
throw "DotNetHelmPush plugin requires 'credentialsEnvVar' (name of env var holding base64 username:password)."
|
$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
|
$scriptDir = $shared.ScriptDir
|
||||||
@ -93,10 +72,9 @@ function Invoke-Plugin {
|
|||||||
$chartVersion = ([string]$shared.tag).Trim() -replace '^[vV]', ''
|
$chartVersion = ([string]$shared.tag).Trim() -replace '^[vV]', ''
|
||||||
}
|
}
|
||||||
if ([string]::IsNullOrWhiteSpace($chartVersion)) {
|
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('/')
|
$ociRepository = [string]$pluginSettings.ociRepository.TrimEnd('/')
|
||||||
|
|
||||||
$chartNameLine = Select-String -LiteralPath $chartYaml -Pattern '^\s*name:\s*(.+)\s*$' | Select-Object -First 1
|
$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)"
|
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 ..."
|
Write-Log -Level "STEP" -Message "Pushing $chartPackage to $ociRepository ..."
|
||||||
helm push $chartPackage $ociRepository --username $creds.User --password $creds.Password
|
helm push $chartPackage $ociRepository --username $creds.User --password $creds.Password
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
@ -178,4 +166,8 @@ function Invoke-Plugin {
|
|||||||
$shared | Add-Member -NotePropertyName publishCompleted -NotePropertyValue $true -Force
|
$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
|
||||||
174
utils/plugins/Helm/HelmSelfDeploy.psm1
Normal file
174
utils/plugins/Helm/HelmSelfDeploy.psm1
Normal 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
|
||||||
166
utils/plugins/Helm/HelmUninstall.psm1
Normal file
166
utils/plugins/Helm/HelmUninstall.psm1
Normal 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
|
||||||
119
utils/plugins/Npm/NpmPack.psm1
Normal file
119
utils/plugins/Npm/NpmPack.psm1
Normal 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
|
||||||
@ -6,9 +6,9 @@
|
|||||||
Publishes npm workspace packages to the npm registry.
|
Publishes npm workspace packages to the npm registry.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Publishes packages in configured order using an API key from an environment
|
Publishes packages in configured order using npmSecret (logical secret name).
|
||||||
variable (for example NPMJS_MAKS_IT). Uses a temporary .npmrc in the
|
Pass the token via an environment variable named like the configured npm secret (e.g. Npm).
|
||||||
workspace root for auth and supports --skip-duplicate semantics via npm.
|
Uses a temporary .npmrc in the workspace root.
|
||||||
#>
|
#>
|
||||||
|
|
||||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||||
@ -32,18 +32,10 @@ function Invoke-Plugin {
|
|||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$shared = $Settings.context
|
$shared = $Settings.context
|
||||||
|
|
||||||
|
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $shared
|
||||||
|
|
||||||
Assert-Command npm
|
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
|
$workspaceRoot = $null
|
||||||
if ($pluginSettings.workspaceRoot) {
|
if ($pluginSettings.workspaceRoot) {
|
||||||
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $shared.scriptDir)
|
$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)."
|
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
|
$registryHost = ([uri]$registry).Host
|
||||||
$tempNpmRcPath = Join-Path $workspaceRoot ".npmrc.release-temp"
|
$tempNpmRcPath = Join-Path $workspaceRoot ".npmrc.release-temp"
|
||||||
$npmRcContent = @"
|
$npmRcContent = @"
|
||||||
registry=$registry
|
registry=$registry
|
||||||
//$registryHost/:_authToken=$npmApiKey
|
//$registryHost/:_authToken=$npmToken
|
||||||
"@
|
"@
|
||||||
|
|
||||||
Push-Location $workspaceRoot
|
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
|
||||||
47
utils/plugins/Platform/ContainerEngineProbe.psm1
Normal file
47
utils/plugins/Platform/ContainerEngineProbe.psm1
Normal 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
|
||||||
41
utils/plugins/Platform/FileReleaseVersion.psm1
Normal file
41
utils/plugins/Platform/FileReleaseVersion.psm1
Normal 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
|
||||||
@ -98,7 +98,7 @@ function Invoke-Plugin {
|
|||||||
|
|
||||||
$pluginSettings = $Settings
|
$pluginSettings = $Settings
|
||||||
$sharedSettings = $Settings.context
|
$sharedSettings = $Settings.context
|
||||||
$githubTokenEnvVar = $pluginSettings.githubToken
|
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret'
|
||||||
$configuredRepository = $pluginSettings.repository
|
$configuredRepository = $pluginSettings.repository
|
||||||
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
|
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
|
||||||
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
|
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
|
||||||
@ -108,15 +108,37 @@ function Invoke-Plugin {
|
|||||||
$releaseDir = $sharedSettings.releaseDir
|
$releaseDir = $sharedSettings.releaseDir
|
||||||
$releaseAssetPaths = @()
|
$releaseAssetPaths = @()
|
||||||
|
|
||||||
Assert-Command gh
|
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($githubTokenEnvVar)) {
|
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||||
throw "GitHub plugin requires 'githubToken' in scriptSettings.json."
|
throw "GitHub plugin requires 'releaseNotesFile' in scriptSettings.json."
|
||||||
}
|
}
|
||||||
|
|
||||||
$githubToken = [System.Environment]::GetEnvironmentVariable($githubTokenEnvVar)
|
$releaseNotesFile = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $releaseNotesFileSetting))
|
||||||
if ([string]::IsNullOrWhiteSpace($githubToken)) {
|
$releaseNotes = Get-ReleaseNotesInternal -ReleaseNotesFile $releaseNotesFile -Version $version
|
||||||
throw "GitHub token is not set. Set '$githubTokenEnvVar' and rerun."
|
|
||||||
|
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)) {
|
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||||
@ -163,7 +185,7 @@ function Invoke-Plugin {
|
|||||||
Write-Log -Level "INFO" -Message " GitHub title: $releaseName"
|
Write-Log -Level "INFO" -Message " GitHub title: $releaseName"
|
||||||
|
|
||||||
$previousGhToken = $env:GH_TOKEN
|
$previousGhToken = $env:GH_TOKEN
|
||||||
$env:GH_TOKEN = $githubToken
|
$env:GH_TOKEN = $ghToken
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$ghVersion = & gh --version 2>&1
|
$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 " 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")
|
$authArgs = @("api", "repos/$repo", "--jq", ".full_name")
|
||||||
$authOutput = & gh @authArgs 2>&1
|
$authOutput = & gh @authArgs 2>&1
|
||||||
@ -188,7 +210,7 @@ function Invoke-Plugin {
|
|||||||
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
||||||
}
|
}
|
||||||
|
|
||||||
throw "GitHub CLI authentication failed for repository '$repo'. Ensure '$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)"
|
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
|
||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.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
|
.DESCRIPTION
|
||||||
Place this plugin immediately before any publish plugins in scriptSettings.json. It sets
|
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..."
|
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)
|
$allowed = @(Get-PluginBranches -Plugin $pluginSettings)
|
||||||
if ($allowed.Count -gt 0 -and $allowed -notcontains '*' -and $allowed -notcontains $shared.currentBranch) {
|
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."
|
Invoke-NotMetInternal -Shared $shared -When $when -Reason "branch '$($shared.currentBranch)' is not in the guard branches list."
|
||||||
35
utils/plugins/Podman/PodmanCompose.psm1
Normal file
35
utils/plugins/Podman/PodmanCompose.psm1
Normal 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
|
||||||
44
utils/plugins/Podman/PodmanImagePush.psm1
Normal file
44
utils/plugins/Podman/PodmanImagePush.psm1
Normal 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
|
||||||
@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0engines\release\Invoke-ReleasePackage.ps1" %*
|
|
||||||
pause
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
@echo off
|
|
||||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0engines\test\Invoke-TestEngine.ps1" %*
|
|
||||||
pause
|
|
||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -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)."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -248,6 +248,11 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (-not $dryRun) {
|
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
|
Copy-Item -Path $file.FullName -Destination $backupPath -Force
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user