mirror of
https://github.com/MAKS-IT-COM/maksit-webui.git
synced 2026-06-30 20:06:43 +02:00
(feature): add useWebUiHub
Some checks failed
Storybook tests / storybook-tests (push) Has been cancelled
Some checks failed
Storybook tests / storybook-tests (push) Has been cancelled
This commit is contained in:
parent
b0b8fe8614
commit
003018df9f
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,5 +6,5 @@ src/storybook-static/
|
||||
*.tsbuildinfo
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
utils/Release-Package/.npmrc.release-temp
|
||||
utils/src/engines/release/.npmrc.release-temp
|
||||
src/.npmrc.release-temp
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.3] - 2026-05-31
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated `utils/` to maksit-repoutils **1.0.14** layout under `utils/src/` (`Invoke-ReleasePackage`, `Invoke-TestEngine`, `Update-RepoUtils`). Product `scriptSettings.json` paths updated for the deeper engine folders.
|
||||
|
||||
### Added
|
||||
|
||||
- `@maks-it.com/webui-core`: `useWebUiHub` React hook for JWT-authenticated SignalR hubs — connection state (`idle` / `connecting` / `connected` / `reconnecting` / `disconnected`), optional automatic reconnect, and typed hub event handlers.
|
||||
- `resolveHubUrl` helper (absolute URLs unchanged; relative paths resolve against `window.location.origin`).
|
||||
- `@microsoft/signalr` peer dependency on `@maks-it.com/webui-core`.
|
||||
|
||||
### Changed
|
||||
|
||||
- All `@maks-it.com/webui-*` packages published at `0.3.3` with aligned workspace dependency ranges.
|
||||
- Jest tests for core and contracts moved from co-located `src/**/*.test.ts` to `packages/*/test/`; root `jest.config.cjs` roots updated accordingly.
|
||||
- `@maks-it.com/webui-core` README documents SignalR install and `useWebUiHub` usage.
|
||||
|
||||
## [0.3.2] - 2026-05-30
|
||||
|
||||
### Fixed
|
||||
|
||||
10
README.md
10
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-components` | React components, layout, editors, DataTable, auth shell |
|
||||
|
||||
Source lives under `src/` (npm workspaces). Release automation lives under `utils/` (from [maksit-repoutils](https://github.com/MAKS-IT-COM/maksit-repoutils)).
|
||||
Source lives under `src/` (npm workspaces). Release automation lives under `utils/src/` (from [maksit-repoutils](https://github.com/MAKS-IT-COM/maksit-repoutils)).
|
||||
|
||||
## Local development
|
||||
|
||||
@ -26,15 +26,15 @@ npm run storybook
|
||||
|
||||
**Storybook** (`npm run storybook`) runs a local catalog of `@maks-it.com/webui-components` with Tailwind, React Router, autodocs, a11y checks, and **Vitest component tests** (testing widget + `npm run test-storybook`). Stories live under `src/stories/components/` (mirroring component folders); see `src/stories/README.md` for story conventions and testing.
|
||||
|
||||
Tests and coverage badges: **`utils/Run-Tests/Run-Tests.bat`** (plugin config in `utils/Run-Tests/scriptsettings.json`; uses `NpmJestTest`).
|
||||
Tests and coverage badges: **`utils/src/Invoke-TestEngine.bat`** (plugin config in `utils/src/engines/test/scriptSettings.json`; uses `NpmJestTest`).
|
||||
|
||||
## Release to npmjs
|
||||
|
||||
1. Set **`NPMJS_MAKS_IT`** to your npm automation token (same pattern as `NUGET_MAKS_IT` for NuGet).
|
||||
2. Bump **`src/package.json`** `version` (and tag `vX.Y.Z` on `main` when using the publish guard).
|
||||
3. Run **`utils/Release-Package/Release-Package.bat`** (or `pwsh utils/Release-Package/Release-Package.ps1`).
|
||||
3. Run **`utils/src/Invoke-ReleasePackage.bat`** (or `pwsh utils/src/engines/release/Invoke-ReleasePackage.ps1`).
|
||||
|
||||
Configured plugins (see `utils/Release-Package/scriptsettings.json`):
|
||||
Configured plugins (see `utils/src/engines/release/scriptSettings.json`):
|
||||
|
||||
| Plugin | Role |
|
||||
|--------|------|
|
||||
@ -44,7 +44,7 @@ Configured plugins (see `utils/Release-Package/scriptsettings.json`):
|
||||
| `GitHub` | GitHub release (optional; needs `GITHUB_MAKS_IT_COM`) |
|
||||
| `NpmPublish` | Publish workspace packages in dependency order |
|
||||
|
||||
Refresh shared utils from repoutils: **`utils/Update-RepoUtils/Update-RepoUtils.bat`**.
|
||||
Refresh shared utils from repoutils: **`utils/src/Update-RepoUtils.bat`**.
|
||||
|
||||
## 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: 48%">
|
||||
<title>Branch Coverage: 48%</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 72%">
|
||||
<title>Branch Coverage: 72%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
@ -9,13 +9,13 @@
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="107.5" height="20" fill="#555"/>
|
||||
<rect x="107.5" width="40" height="20" fill="#a4a61d"/>
|
||||
<rect x="107.5" width="40" height="20" fill="#97ca00"/>
|
||||
<rect width="147.5" height="20" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
||||
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
||||
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">48%</text>
|
||||
<text x="127.5" y="14" fill="#fff">48%</text>
|
||||
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">72%</text>
|
||||
<text x="127.5" y="14" fill="#fff">72%</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,21 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="134.5" height="20" role="img" aria-label="Line Coverage: 42%">
|
||||
<title>Line Coverage: 42%</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 75.2%">
|
||||
<title>Line Coverage: 75.2%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="r">
|
||||
<rect width="134.5" height="20" rx="3" fill="#fff"/>
|
||||
<rect width="137" height="20" rx="3" fill="#fff"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="94.5" height="20" fill="#555"/>
|
||||
<rect x="94.5" width="40" height="20" fill="#a4a61d"/>
|
||||
<rect width="134.5" height="20" fill="url(#s)"/>
|
||||
<rect x="94.5" width="42.5" height="20" fill="#97ca00"/>
|
||||
<rect width="137" height="20" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
|
||||
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
|
||||
<text aria-hidden="true" x="114.5" y="15" fill="#010101" fill-opacity=".3">42%</text>
|
||||
<text x="114.5" y="14" fill="#fff">42%</text>
|
||||
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">75.2%</text>
|
||||
<text x="115.75" y="14" fill="#fff">75.2%</text>
|
||||
</g>
|
||||
</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: 20.3%">
|
||||
<title>Method Coverage: 20.3%</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 66.7%">
|
||||
<title>Method Coverage: 66.7%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
@ -9,13 +9,13 @@
|
||||
</clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="107.5" height="20" fill="#555"/>
|
||||
<rect x="107.5" width="42.5" height="20" fill="#dfb317"/>
|
||||
<rect x="107.5" width="42.5" height="20" fill="#97ca00"/>
|
||||
<rect width="150" height="20" fill="url(#s)"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
|
||||
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
|
||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">20.3%</text>
|
||||
<text x="128.75" y="14" fill="#fff">20.3%</text>
|
||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">66.7%</text>
|
||||
<text x="128.75" y="14" fill="#fff">66.7%</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -50,13 +50,13 @@ npm view @maks-it.com/webui-components version
|
||||
|
||||
## Release pipeline (recommended)
|
||||
|
||||
Use **`utils/Release-Package/Release-Package.bat`** (or `pwsh utils/Release-Package/Release-Package.ps1`):
|
||||
Use **`utils/src/Invoke-ReleasePackage.bat`** (or `pwsh utils/src/engines/release/Invoke-ReleasePackage.ps1`):
|
||||
|
||||
1. Bump version in `src/package.json` (or tag drives `NpmReleaseVersion`).
|
||||
2. Tag `HEAD` with exact semver, e.g. `git tag v0.2.0 && git push origin v0.2.0`.
|
||||
3. Set `NPMJS_MAKS_IT` and run the release engine.
|
||||
|
||||
`utils/Release-Package/scriptsettings.json` runs `NpmReleaseVersion`, `NpmBuild`, `ReleasePublishGuard`, optional `GitHub`, then `NpmPublish` in dependency order.
|
||||
`utils/src/engines/release/scriptSettings.json` runs `NpmReleaseVersion`, `NpmBuild`, `ReleasePublishGuard`, optional `GitHub`, then `NpmPublish` in dependency order.
|
||||
|
||||
## After publish — Certs UI / Vault
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{"total": {"lines":{"total":837,"covered":352,"skipped":0,"pct":42.05},"statements":{"total":902,"covered":377,"skipped":0,"pct":41.79},"functions":{"total":212,"covered":43,"skipped":0,"pct":20.28},"branches":{"total":404,"covered":194,"skipped":0,"pct":48.01},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
{"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}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\PagedRequest.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":3,"covered":3,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\PatchOperation.ts": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\PatchRequestModelBase.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
@ -8,54 +8,22 @@
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\login\\LoginRequest.ts": {"lines":{"total":9,"covered":9,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":9,"covered":9,"skipped":0,"pct":100},"branches":{"total":8,"covered":8,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\login\\RefreshTokenRequest.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\contracts\\src\\identity\\logout\\LogoutRequest.ts": {"lines":{"total":2,"covered":2,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":2,"covered":2,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\index.ts": {"lines":{"total":5,"covered":0,"skipped":0,"pct":0},"functions":{"total":4,"covered":0,"skipped":0,"pct":0},"statements":{"total":9,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\index.ts": {"lines":{"total":47,"covered":0,"skipped":0,"pct":0},"functions":{"total":38,"covered":0,"skipped":0,"pct":0},"statements":{"total":47,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\acl\\index.ts": {"lines":{"total":1,"covered":0,"skipped":0,"pct":0},"functions":{"total":2,"covered":0,"skipped":0,"pct":0},"statements":{"total":3,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\acl\\parseAclEntry.ts": {"lines":{"total":17,"covered":17,"skipped":0,"pct":100},"functions":{"total":4,"covered":4,"skipped":0,"pct":100},"statements":{"total":18,"covered":18,"skipped":0,"pct":100},"branches":{"total":6,"covered":6,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\dataTable\\dataTableFilters.ts": {"lines":{"total":5,"covered":5,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\dataTable\\dataTablePaged.ts": {"lines":{"total":4,"covered":4,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":4,"skipped":0,"pct":100},"branches":{"total":16,"covered":16,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\dataTable\\index.ts": {"lines":{"total":4,"covered":0,"skipped":0,"pct":0},"functions":{"total":2,"covered":0,"skipped":0,"pct":0},"statements":{"total":4,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\date\\dateTimeToUtcIsoSchema.ts": {"lines":{"total":8,"covered":0,"skipped":0,"pct":0},"functions":{"total":2,"covered":0,"skipped":0,"pct":0},"statements":{"total":8,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\date\\formatISODateString.ts": {"lines":{"total":10,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":10,"covered":0,"skipped":0,"pct":0},"branches":{"total":4,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\date\\index.ts": {"lines":{"total":12,"covered":0,"skipped":0,"pct":0},"functions":{"total":9,"covered":0,"skipped":0,"pct":0},"statements":{"total":13,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\date\\isValidDateString.ts": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":7,"covered":7,"skipped":0,"pct":100},"branches":{"total":2,"covered":2,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepCopy.ts": {"lines":{"total":17,"covered":16,"skipped":0,"pct":94.11},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":17,"covered":16,"skipped":0,"pct":94.11},"branches":{"total":12,"covered":11,"skipped":0,"pct":91.66}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepDelta.ts": {"lines":{"total":183,"covered":119,"skipped":0,"pct":65.02},"functions":{"total":18,"covered":14,"skipped":0,"pct":77.77},"statements":{"total":199,"covered":130,"skipped":0,"pct":65.32},"branches":{"total":137,"covered":92,"skipped":0,"pct":67.15}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\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\\deepMerge.ts": {"lines":{"total":21,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":24,"covered":0,"skipped":0,"pct":0},"branches":{"total":17,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\deepPatternMatch.ts": {"lines":{"total":10,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":14,"covered":0,"skipped":0,"pct":0},"branches":{"total":10,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\index.ts": {"lines":{"total":15,"covered":0,"skipped":0,"pct":0},"functions":{"total":9,"covered":0,"skipped":0,"pct":0},"statements":{"total":15,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\deep\\patchCollectionPolicies.ts": {"lines":{"total":16,"covered":0,"skipped":0,"pct":0},"functions":{"total":5,"covered":0,"skipped":0,"pct":0},"statements":{"total":16,"covered":0,"skipped":0,"pct":0},"branches":{"total":16,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\enumToArr.ts": {"lines":{"total":16,"covered":0,"skipped":0,"pct":0},"functions":{"total":3,"covered":0,"skipped":0,"pct":0},"statements":{"total":19,"covered":0,"skipped":0,"pct":0},"branches":{"total":10,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\enumToObj.ts": {"lines":{"total":9,"covered":0,"skipped":0,"pct":0},"functions":{"total":2,"covered":0,"skipped":0,"pct":0},"statements":{"total":11,"covered":0,"skipped":0,"pct":0},"branches":{"total":4,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\enumToString.ts": {"lines":{"total":11,"covered":0,"skipped":0,"pct":0},"functions":{"total":3,"covered":0,"skipped":0,"pct":0},"statements":{"total":12,"covered":0,"skipped":0,"pct":0},"branches":{"total":6,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\enum\\flagsToString.ts": {"lines":{"total":6,"covered":0,"skipped":0,"pct":0},"functions":{"total":3,"covered":0,"skipped":0,"pct":0},"statements":{"total":6,"covered":0,"skipped":0,"pct":0},"branches":{"total":4,"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\\index.ts": {"lines":{"total":14,"covered":0,"skipped":0,"pct":0},"functions":{"total":7,"covered":0,"skipped":0,"pct":0},"statements":{"total":14,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"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\\file\\index.ts": {"lines":{"total":2,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":2,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\file\\saveBinaryToDisk.ts": {"lines":{"total":11,"covered":0,"skipped":0,"pct":0},"functions":{"total":2,"covered":0,"skipped":0,"pct":0},"statements":{"total":12,"covered":0,"skipped":0,"pct":0},"branches":{"total":2,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\guid\\index.ts": {"lines":{"total":2,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":2,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\guid\\isGuid.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":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\headers\\extractFilenameFromHeaders.ts": {"lines":{"total":17,"covered":15,"skipped":0,"pct":88.23},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":17,"covered":15,"skipped":0,"pct":88.23},"branches":{"total":15,"covered":14,"skipped":0,"pct":93.33}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\headers\\index.ts": {"lines":{"total":2,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":2,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\zod\\applyFormBulkChange.ts": {"lines":{"total":3,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":3,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\zod\\applyFormFieldChange.ts": {"lines":{"total":9,"covered":0,"skipped":0,"pct":0},"functions":{"total":2,"covered":0,"skipped":0,"pct":0},"statements":{"total":10,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\zod\\createFormBulkUpdater.ts": {"lines":{"total":4,"covered":0,"skipped":0,"pct":0},"functions":{"total":2,"covered":0,"skipped":0,"pct":0},"statements":{"total":5,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\zod\\createFormFieldUpdater.ts": {"lines":{"total":4,"covered":0,"skipped":0,"pct":0},"functions":{"total":2,"covered":0,"skipped":0,"pct":0},"statements":{"total":5,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\zod\\emptyFormErrors.ts": {"lines":{"total":3,"covered":3,"skipped":0,"pct":100},"functions":{"total":2,"covered":2,"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\\zod\\flattenFormValidationIssues.ts": {"lines":{"total":7,"covered":7,"skipped":0,"pct":100},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":7,"covered":7,"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\\zod\\index.ts": {"lines":{"total":12,"covered":0,"skipped":0,"pct":0},"functions":{"total":6,"covered":0,"skipped":0,"pct":0},"statements":{"total":12,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\functions\\zod\\validateFormState.ts": {"lines":{"total":7,"covered":7,"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\\http\\authInterceptors.ts": {"lines":{"total":60,"covered":0,"skipped":0,"pct":0},"functions":{"total":9,"covered":0,"skipped":0,"pct":0},"statements":{"total":60,"covered":0,"skipped":0,"pct":0},"branches":{"total":38,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\http\\config.ts": {"lines":{"total":18,"covered":0,"skipped":0,"pct":0},"functions":{"total":4,"covered":0,"skipped":0,"pct":0},"statements":{"total":18,"covered":0,"skipped":0,"pct":0},"branches":{"total":13,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\http\\createWebUiHttpClient.ts": {"lines":{"total":49,"covered":0,"skipped":0,"pct":0},"functions":{"total":22,"covered":0,"skipped":0,"pct":0},"statements":{"total":50,"covered":0,"skipped":0,"pct":0},"branches":{"total":15,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\http\\errorHandler.ts": {"lines":{"total":12,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":12,"covered":0,"skipped":0,"pct":0},"branches":{"total":13,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\http\\formData.ts": {"lines":{"total":9,"covered":0,"skipped":0,"pct":0},"functions":{"total":3,"covered":0,"skipped":0,"pct":0},"statements":{"total":10,"covered":0,"skipped":0,"pct":0},"branches":{"total":4,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\http\\index.ts": {"lines":{"total":5,"covered":0,"skipped":0,"pct":0},"functions":{"total":5,"covered":0,"skipped":0,"pct":0},"statements":{"total":10,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\http\\problemDetails.ts": {"lines":{"total":5,"covered":5,"skipped":0,"pct":100},"functions":{"total":3,"covered":3,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":9,"covered":8,"skipped":0,"pct":88.88}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\localStorage\\identity.ts": {"lines":{"total":9,"covered":0,"skipped":0,"pct":0},"functions":{"total":3,"covered":0,"skipped":0,"pct":0},"statements":{"total":12,"covered":0,"skipped":0,"pct":0},"branches":{"total":2,"covered":0,"skipped":0,"pct":0}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\signalr\\useWebUiHub.ts": {"lines":{"total":61,"covered":10,"skipped":0,"pct":16.39},"functions":{"total":12,"covered":1,"skipped":0,"pct":8.33},"statements":{"total":64,"covered":12,"skipped":0,"pct":18.75},"branches":{"total":33,"covered":7,"skipped":0,"pct":21.21}}
|
||||
,"E:\\Users\\maksym\\source\\repos\\MaksIT\\maksit-webui\\src\\packages\\core\\src\\types\\ScopePermissions.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\\core\\src\\types\\index.ts": {"lines":{"total":1,"covered":0,"skipped":0,"pct":0},"functions":{"total":1,"covered":0,"skipped":0,"pct":0},"statements":{"total":2,"covered":0,"skipped":0,"pct":0},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}}
|
||||
}
|
||||
|
||||
@ -2,12 +2,11 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/packages/core/src', '<rootDir>/packages/contracts/src'],
|
||||
roots: ['<rootDir>/packages/core/test', '<rootDir>/packages/contracts/test'],
|
||||
testMatch: ['**/*.test.ts'],
|
||||
collectCoverageFrom: [
|
||||
'packages/core/src/**/*.ts',
|
||||
'packages/contracts/src/**/*.ts',
|
||||
'!**/*.test.ts',
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['json-summary', 'text'],
|
||||
|
||||
1310
src/package-lock.json
generated
1310
src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "maksit-webui",
|
||||
"private": true,
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Shared React UI library for MaksIT Certs UI and Vault WebUI",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@maks-it.com/webui-components",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Shared React components for MaksIT WebUI apps",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
@ -33,8 +33,8 @@
|
||||
"directory": "src/packages/components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@maks-it.com/webui-contracts": "^0.3.2",
|
||||
"@maks-it.com/webui-core": "^0.3.2"
|
||||
"@maks-it.com/webui-contracts": "^0.3.3",
|
||||
"@maks-it.com/webui-core": "^0.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-table": "^8.0.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@maks-it.com/webui-contracts",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Shared TypeScript contracts for MaksIT WebUI apps",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { PatchOperation } from './PatchOperation'
|
||||
import { LoginRequestSchema } from './identity/login/LoginRequest'
|
||||
import { PagedRequestSchema } from './PagedRequest'
|
||||
import { PatchRequestModelBaseSchema } from './PatchRequestModelBase'
|
||||
import { RefreshTokenRequestSchema } from './identity/login/RefreshTokenRequest'
|
||||
import { PatchOperation } from '../src/PatchOperation'
|
||||
import { LoginRequestSchema } from '../src/identity/login/LoginRequest'
|
||||
import { PagedRequestSchema } from '../src/PagedRequest'
|
||||
import { PatchRequestModelBaseSchema } from '../src/PatchRequestModelBase'
|
||||
import { RefreshTokenRequestSchema } from '../src/identity/login/RefreshTokenRequest'
|
||||
|
||||
describe('LoginRequestSchema', () => {
|
||||
it('accepts valid credentials', () => {
|
||||
@ -4,6 +4,5 @@
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ Depends on `@maks-it.com/webui-contracts`. Install peer dependencies in the host
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install @maks-it.com/webui-core @maks-it.com/webui-contracts axios react zod
|
||||
npm install @maks-it.com/webui-core @maks-it.com/webui-contracts @microsoft/signalr axios react zod
|
||||
```
|
||||
|
||||
## Highlights
|
||||
@ -20,6 +20,7 @@ npm install @maks-it.com/webui-core @maks-it.com/webui-contracts axios react zod
|
||||
| DataTable | `mapPagedToDataTable`, `extractPropFilter`, `DataTablePageView` |
|
||||
| ACL | `parseAclEntry`, `parseAclEntries` |
|
||||
| HTTP | `createWebUiHttpClient`, auth interceptors, Problem Details helpers |
|
||||
| SignalR | `useWebUiHub` |
|
||||
| Enum / flags | `enumToArr`, `flagsToString`, `hasFlag`, `toggleFlag` |
|
||||
| Identity storage | `readIdentity`, `writeIdentity`, `removeIdentity` |
|
||||
|
||||
@ -40,6 +41,25 @@ function MyForm() {
|
||||
}
|
||||
```
|
||||
|
||||
## Example — SignalR hub
|
||||
|
||||
```tsx
|
||||
import { readIdentity, useWebUiHub } from '@maks-it.com/webui-core'
|
||||
|
||||
function JobProgressPanel({ canSubscribe }: { canSubscribe: boolean }) {
|
||||
const [progress, setProgress] = useState<number | undefined>()
|
||||
|
||||
const { connectionState } = useWebUiHub({
|
||||
hubUrl: '/hubs/jobs',
|
||||
enabled: canSubscribe,
|
||||
accessToken: () => readIdentity()?.token,
|
||||
events: { progressUpdated: setProgress },
|
||||
})
|
||||
|
||||
// connectionState: idle | connecting | connected | reconnecting | disconnected
|
||||
}
|
||||
```
|
||||
|
||||
## Example — PATCH delta
|
||||
|
||||
```ts
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@maks-it.com/webui-core",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Shared utilities and hooks for MaksIT WebUI apps",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
@ -34,15 +34,17 @@
|
||||
"directory": "src/packages/core"
|
||||
},
|
||||
"dependencies": {
|
||||
"@maks-it.com/webui-contracts": "^0.3.2",
|
||||
"@maks-it.com/webui-contracts": "^0.3.3",
|
||||
"date-fns": "^4.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@microsoft/signalr": "^8.0.0",
|
||||
"axios": "^1.16.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"zod": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@types/react": "^19.2.15",
|
||||
"axios": "^1.16.1",
|
||||
"react": "^19.2.6",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from './functions'
|
||||
export * from './types'
|
||||
export * from './http'
|
||||
export * from './signalr'
|
||||
export { readIdentity, writeIdentity, removeIdentity } from './localStorage/identity'
|
||||
export { useFormState } from './hooks/useFormState'
|
||||
|
||||
7
src/packages/core/src/signalr/index.ts
Normal file
7
src/packages/core/src/signalr/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export { useWebUiHub } from './useWebUiHub'
|
||||
export type {
|
||||
UseWebUiHubOptions,
|
||||
UseWebUiHubResult,
|
||||
WebUiHubAccessTokenFactory,
|
||||
WebUiHubConnectionState,
|
||||
} from './useWebUiHub'
|
||||
139
src/packages/core/src/signalr/useWebUiHub.ts
Normal file
139
src/packages/core/src/signalr/useWebUiHub.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { HubConnectionBuilder, HubConnectionState } from '@microsoft/signalr'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export type WebUiHubConnectionState =
|
||||
| 'idle'
|
||||
| 'connecting'
|
||||
| 'connected'
|
||||
| 'reconnecting'
|
||||
| 'disconnected'
|
||||
|
||||
export type WebUiHubAccessTokenFactory = () => string | undefined | Promise<string | undefined>
|
||||
|
||||
export interface UseWebUiHubOptions {
|
||||
/** Absolute URL or path (e.g. `/hubs/my-hub`). Paths resolve against `window.location.origin`. */
|
||||
hubUrl: string
|
||||
/** When false, no connection is opened. Default: true. */
|
||||
enabled?: boolean
|
||||
accessToken: WebUiHubAccessTokenFactory
|
||||
/** Hub method name -> handler. */
|
||||
events: Record<string, (payload: unknown) => void>
|
||||
automaticReconnect?: boolean
|
||||
}
|
||||
|
||||
export interface UseWebUiHubResult {
|
||||
connectionState: WebUiHubConnectionState
|
||||
lastError: unknown
|
||||
}
|
||||
|
||||
export const resolveHubUrl = (hubUrl: string): string => {
|
||||
if (/^https?:\/\//i.test(hubUrl))
|
||||
return hubUrl
|
||||
|
||||
const base = typeof globalThis !== 'undefined' && 'location' in globalThis
|
||||
? globalThis.location.origin
|
||||
: ''
|
||||
|
||||
const path = hubUrl.startsWith('/') ? hubUrl : `/${hubUrl}`
|
||||
return `${base}${path}`
|
||||
}
|
||||
|
||||
const eventKeys = (events: Record<string, unknown>): string =>
|
||||
Object.keys(events).sort().join('\0')
|
||||
|
||||
/** JWT-authenticated SignalR hub with reconnect handling. */
|
||||
export const useWebUiHub = (options: UseWebUiHubOptions): UseWebUiHubResult => {
|
||||
const {
|
||||
hubUrl,
|
||||
enabled = true,
|
||||
accessToken,
|
||||
events,
|
||||
automaticReconnect,
|
||||
} = options
|
||||
|
||||
const accessTokenRef = useRef(accessToken)
|
||||
accessTokenRef.current = accessToken
|
||||
|
||||
const eventsRef = useRef(events)
|
||||
eventsRef.current = events
|
||||
|
||||
const [connectionState, setConnectionState] = useState<WebUiHubConnectionState>(
|
||||
enabled ? 'connecting' : 'idle'
|
||||
)
|
||||
const [lastError, setLastError] = useState<unknown>(undefined)
|
||||
|
||||
const subscribedEvents = eventKeys(events)
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setConnectionState('idle')
|
||||
setLastError(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
let disposed = false
|
||||
const wrappers = new Map<string, (payload: unknown) => void>()
|
||||
|
||||
const builder = new HubConnectionBuilder()
|
||||
.withUrl(resolveHubUrl(hubUrl), {
|
||||
accessTokenFactory: async () => (await accessTokenRef.current()) ?? '',
|
||||
})
|
||||
|
||||
if (automaticReconnect !== false)
|
||||
builder.withAutomaticReconnect()
|
||||
|
||||
const connection = builder.build()
|
||||
|
||||
for (const eventName of Object.keys(eventsRef.current)) {
|
||||
const wrapper = (payload: unknown) => eventsRef.current[eventName]?.(payload)
|
||||
wrappers.set(eventName, wrapper)
|
||||
connection.on(eventName, wrapper)
|
||||
}
|
||||
|
||||
connection.onreconnecting(() => {
|
||||
if (!disposed)
|
||||
setConnectionState('reconnecting')
|
||||
})
|
||||
|
||||
connection.onreconnected(() => {
|
||||
if (!disposed)
|
||||
setConnectionState('connected')
|
||||
})
|
||||
|
||||
connection.onclose(error => {
|
||||
if (!disposed) {
|
||||
setConnectionState('disconnected')
|
||||
if (error)
|
||||
setLastError(error)
|
||||
}
|
||||
})
|
||||
|
||||
setConnectionState('connecting')
|
||||
setLastError(undefined)
|
||||
|
||||
void connection.start()
|
||||
.then(() => {
|
||||
if (!disposed)
|
||||
setConnectionState('connected')
|
||||
})
|
||||
.catch(error => {
|
||||
if (!disposed) {
|
||||
setConnectionState('disconnected')
|
||||
setLastError(error)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
disposed = true
|
||||
for (const [eventName, wrapper] of wrappers)
|
||||
connection.off(eventName, wrapper)
|
||||
|
||||
if (connection.state === HubConnectionState.Connected
|
||||
|| connection.state === HubConnectionState.Reconnecting) {
|
||||
void connection.stop()
|
||||
}
|
||||
}
|
||||
}, [enabled, hubUrl, subscribedEvents, automaticReconnect])
|
||||
|
||||
return { connectionState, lastError }
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { ScopePermission } from '../../types/ScopePermissions'
|
||||
import { parseAclEntry, parseAclEntries } from './parseAclEntry'
|
||||
import { ScopePermission } from '../../../src/types/ScopePermissions'
|
||||
import { parseAclEntry, parseAclEntries } from '../../../src/functions/acl/parseAclEntry'
|
||||
|
||||
const entityTypeMap = { O: 1, V: 2 } as const
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { extractPropFilter } from './dataTableFilters'
|
||||
import { extractPropFilter } from '../../../src/functions/dataTable/dataTableFilters'
|
||||
|
||||
describe('extractPropFilter', () => {
|
||||
it('extracts Contains filter values', () => {
|
||||
@ -1,5 +1,5 @@
|
||||
import type { PagedResponse } from '@maks-it.com/webui-contracts'
|
||||
import { mapPagedToDataTable } from './dataTablePaged'
|
||||
import { mapPagedToDataTable } from '../../../src/functions/dataTable/dataTablePaged'
|
||||
|
||||
describe('mapPagedToDataTable', () => {
|
||||
it('returns an empty page for missing responses', () => {
|
||||
@ -1,4 +1,4 @@
|
||||
import { isValidISODateString } from './isValidDateString'
|
||||
import { isValidISODateString } from '../../../src/functions/date/isValidDateString'
|
||||
|
||||
describe('isValidISODateString', () => {
|
||||
it('accepts valid ISO date strings', () => {
|
||||
@ -1,5 +1,5 @@
|
||||
import { COLLECTION_ITEM_OPERATION, PatchOperation } from '@maks-it.com/webui-contracts'
|
||||
import { deepDelta, deltaHasOperations } from './deepDelta'
|
||||
import { deepDelta, deltaHasOperations } from '../../../src/functions/deep/deepDelta'
|
||||
|
||||
describe('deepDelta', () => {
|
||||
it('detects primitive field changes', () => {
|
||||
@ -1,4 +1,4 @@
|
||||
import { deepEqual, deepEqualArrays } from './deepEqual'
|
||||
import { deepEqual, deepEqualArrays } from '../../../src/functions/deep/deepEqual'
|
||||
|
||||
describe('deepEqual', () => {
|
||||
it('returns true for identical primitives', () => {
|
||||
@ -1,6 +1,6 @@
|
||||
import { hasAnyFlag } from './hasAnyFlag'
|
||||
import { hasFlag } from './hasFlag'
|
||||
import { toggleFlag } from './toggleFlag'
|
||||
import { hasAnyFlag } from '../../../src/functions/enum/hasAnyFlag'
|
||||
import { hasFlag } from '../../../src/functions/enum/hasFlag'
|
||||
import { toggleFlag } from '../../../src/functions/enum/toggleFlag'
|
||||
|
||||
describe('hasFlag', () => {
|
||||
it('returns true when all flag bits are set', () => {
|
||||
@ -1,4 +1,4 @@
|
||||
import { isGuid } from './isGuid'
|
||||
import { isGuid } from '../../../src/functions/guid/isGuid'
|
||||
|
||||
describe('isGuid', () => {
|
||||
it('accepts valid GUIDs', () => {
|
||||
@ -1,4 +1,4 @@
|
||||
import { extractFilenameFromHeaders } from './extractFilenameFromHeaders'
|
||||
import { extractFilenameFromHeaders } from '../../../src/functions/headers/extractFilenameFromHeaders'
|
||||
|
||||
describe('extractFilenameFromHeaders', () => {
|
||||
it('returns fallback when header is missing', () => {
|
||||
@ -1,5 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
import { validateFormState } from './validateFormState'
|
||||
import { validateFormState } from '../../../src/functions/zod/validateFormState'
|
||||
|
||||
describe('validateFormState', () => {
|
||||
const schema = z.object({
|
||||
@ -1,4 +1,4 @@
|
||||
import { formatProblemDetailsMessage } from './problemDetails'
|
||||
import { formatProblemDetailsMessage } from '../../src/http/problemDetails'
|
||||
|
||||
describe('formatProblemDetailsMessage', () => {
|
||||
it('combines detail and field errors', () => {
|
||||
31
src/packages/core/test/signalr/useWebUiHub.test.ts
Normal file
31
src/packages/core/test/signalr/useWebUiHub.test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { resolveHubUrl, useWebUiHub } from '../../src/signalr/useWebUiHub'
|
||||
|
||||
describe('resolveHubUrl', () => {
|
||||
it('returns absolute URLs unchanged', () => {
|
||||
expect(resolveHubUrl('https://api.example/hubs/jobs')).toBe('https://api.example/hubs/jobs')
|
||||
})
|
||||
|
||||
it('prefixes relative paths with origin', () => {
|
||||
Object.defineProperty(globalThis, 'location', {
|
||||
configurable: true,
|
||||
value: { origin: 'http://localhost:8080' },
|
||||
})
|
||||
|
||||
expect(resolveHubUrl('/hubs/key-migration')).toBe('http://localhost:8080/hubs/key-migration')
|
||||
})
|
||||
|
||||
it('adds leading slash when missing', () => {
|
||||
Object.defineProperty(globalThis, 'location', {
|
||||
configurable: true,
|
||||
value: { origin: 'http://localhost:8080' },
|
||||
})
|
||||
|
||||
expect(resolveHubUrl('hubs/events')).toBe('http://localhost:8080/hubs/events')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useWebUiHub', () => {
|
||||
it('exports the hook', () => {
|
||||
expect(typeof useWebUiHub).toBe('function')
|
||||
})
|
||||
})
|
||||
@ -4,6 +4,5 @@
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
},
|
||||
"include": [
|
||||
"packages/core/src/**/*.ts",
|
||||
"packages/contracts/src/**/*.ts"
|
||||
"packages/core/test/**/*.ts",
|
||||
"packages/contracts/src/**/*.ts",
|
||||
"packages/contracts/test/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Force-AmendTaggedCommit.ps1"
|
||||
pause
|
||||
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Generate-CoverageBadges.ps1"
|
||||
pause
|
||||
@ -1,22 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Legacy entry point — forwards to the Run-Tests plugin engine.
|
||||
|
||||
.DESCRIPTION
|
||||
Generate-CoverageBadges.ps1 is kept for backward compatibility.
|
||||
Configure plugins in src/Run-Tests/scriptsettings.json.
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$runTestsScript = Join-Path (Split-Path $PSScriptRoot -Parent) "Run-Tests\Run-Tests.ps1"
|
||||
if (-not (Test-Path $runTestsScript -PathType Leaf)) {
|
||||
Write-Error "Run-Tests engine not found at: $runTestsScript"
|
||||
exit 1
|
||||
}
|
||||
|
||||
& pwsh -NoProfile -ExecutionPolicy Bypass -File $runTestsScript
|
||||
exit $LASTEXITCODE
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"title": "Generate Coverage Badges Script Settings",
|
||||
"description": "Legacy settings file. Use utils/Run-Tests/scriptsettings.json instead.",
|
||||
"_forwardTo": "..\\Run-Tests\\scriptsettings.json"
|
||||
}
|
||||
@ -1,379 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||
Import-Module $loggingModulePath -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Import-PluginDependency {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ModuleName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RequiredCommand
|
||||
)
|
||||
|
||||
if (Get-Command $RequiredCommand -ErrorAction SilentlyContinue) {
|
||||
return
|
||||
}
|
||||
|
||||
$moduleRoot = Split-Path $PSScriptRoot -Parent
|
||||
$modulePath = Join-Path $moduleRoot "$ModuleName.psm1"
|
||||
if (Test-Path $modulePath -PathType Leaf) {
|
||||
# Import into the global session so the calling plugin can see the exported commands.
|
||||
# Importing only into this module's scope would make the dependency invisible to the plugin.
|
||||
Import-Module $modulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
|
||||
if (-not (Get-Command $RequiredCommand -ErrorAction SilentlyContinue)) {
|
||||
throw "Required command '$RequiredCommand' is still unavailable after importing module '$ModuleName'."
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ConfiguredPlugins {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[psobject]$Settings
|
||||
)
|
||||
|
||||
if (-not $Settings.PSObject.Properties['plugins'] -or $null -eq $Settings.plugins) {
|
||||
return @()
|
||||
}
|
||||
|
||||
# JSON can deserialize a single plugin as one object or multiple plugins as an array.
|
||||
# Always return an array so the engine can loop without special-case logic.
|
||||
if ($Settings.plugins -is [System.Collections.IEnumerable] -and -not ($Settings.plugins -is [string])) {
|
||||
return @($Settings.plugins)
|
||||
}
|
||||
|
||||
return @($Settings.plugins)
|
||||
}
|
||||
|
||||
function Get-PluginStageLabel {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugin
|
||||
)
|
||||
|
||||
if (-not $Plugin.PSObject.Properties['stageLabel'] -or [string]::IsNullOrWhiteSpace([string]$Plugin.stageLabel)) {
|
||||
return 'release'
|
||||
}
|
||||
|
||||
return [string]$Plugin.stageLabel
|
||||
}
|
||||
|
||||
function Get-PluginBranches {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugin
|
||||
)
|
||||
|
||||
if (-not $Plugin.PSObject.Properties['branches'] -or $null -eq $Plugin.branches) {
|
||||
return @()
|
||||
}
|
||||
|
||||
# Strings are also IEnumerable in PowerShell, so exclude them or we would split into characters.
|
||||
if ($Plugin.branches -is [System.Collections.IEnumerable] -and -not ($Plugin.branches -is [string])) {
|
||||
return @($Plugin.branches | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace([string]$Plugin.branches)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
return @([string]$Plugin.branches)
|
||||
}
|
||||
|
||||
function Test-PluginAllowedOnBranch {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugin,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CurrentBranch
|
||||
)
|
||||
|
||||
$allowedBranches = Get-PluginBranches -Plugin $Plugin
|
||||
if ($allowedBranches.Count -eq 0) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($allowedBranches -contains '*') {
|
||||
return $true
|
||||
}
|
||||
|
||||
return $allowedBranches -contains $CurrentBranch
|
||||
}
|
||||
|
||||
function Test-IsPublishPlugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugin
|
||||
)
|
||||
|
||||
if ($null -eq $Plugin -or [string]::IsNullOrWhiteSpace([string]$Plugin.name)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
return @('GitHub', 'DotNetNuGet', 'DockerPush', 'HelmPush', 'NpmPublish') -contains ([string]$Plugin.name)
|
||||
}
|
||||
|
||||
function Get-PluginSettingValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object[]]$Plugins,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PropertyName
|
||||
)
|
||||
|
||||
foreach ($plugin in $Plugins) {
|
||||
if ($null -eq $plugin -or [string]::IsNullOrWhiteSpace($plugin.name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $plugin.PSObject.Properties[$PropertyName]) {
|
||||
continue
|
||||
}
|
||||
|
||||
$value = $plugin.$PropertyName
|
||||
if ($null -eq $value) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($value -is [string] -and [string]::IsNullOrWhiteSpace($value)) {
|
||||
continue
|
||||
}
|
||||
|
||||
return $value
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-PluginPathListSetting {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object[]]$Plugins,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PropertyName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BasePath
|
||||
)
|
||||
|
||||
$rawPaths = @()
|
||||
$value = Get-PluginSettingValue -Plugins $Plugins -PropertyName $PropertyName
|
||||
|
||||
if ($null -eq $value) {
|
||||
return @()
|
||||
}
|
||||
|
||||
# Same rule as above: treat a string as one path, not a char-by-char sequence.
|
||||
if ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) {
|
||||
$rawPaths += $value
|
||||
}
|
||||
else {
|
||||
$rawPaths += $value
|
||||
}
|
||||
|
||||
$resolvedPaths = @()
|
||||
foreach ($path in $rawPaths) {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$path)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$resolvedPaths += [System.IO.Path]::GetFullPath((Join-Path $BasePath ([string]$path)))
|
||||
}
|
||||
|
||||
# Wrap again to stop PowerShell from unrolling a single-item array into a bare string.
|
||||
return @($resolvedPaths)
|
||||
}
|
||||
|
||||
function Get-PluginPathSetting {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object[]]$Plugins,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PropertyName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BasePath
|
||||
)
|
||||
|
||||
$value = Get-PluginSettingValue -Plugins $Plugins -PropertyName $PropertyName
|
||||
if ($null -eq $value -or [string]::IsNullOrWhiteSpace([string]$value)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return [System.IO.Path]::GetFullPath((Join-Path $BasePath ([string]$value)))
|
||||
}
|
||||
|
||||
function Get-ArchiveNamePattern {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object[]]$Plugins,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CurrentBranch
|
||||
)
|
||||
|
||||
foreach ($plugin in $Plugins) {
|
||||
if ($null -eq $plugin -or [string]::IsNullOrWhiteSpace($plugin.name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $plugin.enabled) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not (Test-PluginAllowedOnBranch -Plugin $plugin -CurrentBranch $CurrentBranch)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($plugin.PSObject.Properties['zipNamePattern'] -and -not [string]::IsNullOrWhiteSpace([string]$plugin.zipNamePattern)) {
|
||||
return [string]$plugin.zipNamePattern
|
||||
}
|
||||
}
|
||||
|
||||
return "release-{version}.zip"
|
||||
}
|
||||
|
||||
function Resolve-PluginModulePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugin,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PluginsDirectory
|
||||
)
|
||||
|
||||
$pluginFileName = "{0}.psm1" -f $Plugin.name
|
||||
$candidatePaths = @(
|
||||
(Join-Path $PluginsDirectory $pluginFileName),
|
||||
(Join-Path (Join-Path (Split-Path $PluginsDirectory -Parent) "CustomPlugins") $pluginFileName)
|
||||
)
|
||||
|
||||
foreach ($candidatePath in $candidatePaths) {
|
||||
if (Test-Path $candidatePath -PathType Leaf) {
|
||||
return $candidatePath
|
||||
}
|
||||
}
|
||||
|
||||
return $candidatePaths[0]
|
||||
}
|
||||
|
||||
function Test-PluginRunnable {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugin,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[psobject]$SharedSettings,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PluginsDirectory,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[bool]$WriteLogs = $true
|
||||
)
|
||||
|
||||
if ($null -eq $Plugin -or [string]::IsNullOrWhiteSpace($Plugin.name)) {
|
||||
if ($WriteLogs) {
|
||||
Write-Log -Level "WARN" -Message "Skipping plugin entry with no name."
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
if (-not $Plugin.enabled) {
|
||||
if ($WriteLogs) {
|
||||
Write-Log -Level "WARN" -Message "Skipping plugin '$($Plugin.name)' (disabled)."
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -PluginsDirectory $PluginsDirectory
|
||||
if (-not (Test-Path $pluginModulePath -PathType Leaf)) {
|
||||
if ($WriteLogs) {
|
||||
Write-Log -Level "ERROR" -Message "Plugin module not found: $pluginModulePath"
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
function New-PluginInvocationSettings {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugin,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[psobject]$SharedSettings
|
||||
)
|
||||
|
||||
$properties = @{}
|
||||
foreach ($property in $Plugin.PSObject.Properties) {
|
||||
$properties[$property.Name] = $property.Value
|
||||
}
|
||||
|
||||
# Plugins receive their own config plus shared runtime context.
|
||||
$properties['context'] = $SharedSettings
|
||||
return [pscustomobject]$properties
|
||||
}
|
||||
|
||||
function Invoke-ConfiguredPlugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugin,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[psobject]$SharedSettings,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PluginsDirectory,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[bool]$ContinueOnError = $false
|
||||
)
|
||||
|
||||
if (-not (Test-PluginRunnable -Plugin $Plugin -SharedSettings $SharedSettings -PluginsDirectory $PluginsDirectory -WriteLogs:$true)) {
|
||||
if ($Plugin.enabled) {
|
||||
return $false
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
if ((Test-IsPublishPlugin -Plugin $Plugin) -and ($SharedSettings.PSObject.Properties.Name -contains 'skipPublishPlugins') -and $SharedSettings.skipPublishPlugins) {
|
||||
Write-Log -Level "INFO" -Message "Skipping plugin '$($Plugin.name)' (ReleasePublishGuard suppressed publish)."
|
||||
return $true
|
||||
}
|
||||
|
||||
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -PluginsDirectory $PluginsDirectory
|
||||
Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.name)'..."
|
||||
|
||||
try {
|
||||
$moduleInfo = Import-Module $pluginModulePath -Force -PassThru -ErrorAction Stop
|
||||
# Resolve Invoke-Plugin from the imported module explicitly so we call the plugin we just loaded,
|
||||
# not some command with the same name from another module already in session.
|
||||
$invokeCommand = Get-Command -Name "Invoke-Plugin" -Module $moduleInfo.Name -ErrorAction Stop
|
||||
$pluginSettings = New-PluginInvocationSettings -Plugin $Plugin -SharedSettings $SharedSettings
|
||||
|
||||
& $invokeCommand -Settings $pluginSettings
|
||||
Write-Log -Level "OK" -Message " Plugin '$($Plugin.name)' completed."
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level "ERROR" -Message " Plugin '$($Plugin.name)' failed: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@ -1,46 +0,0 @@
|
||||
# Release-Package
|
||||
|
||||
Plugin-driven release engine. Run `Release-Package.ps1` from this directory (or `Release-Package.bat`). Configuration: `scriptsettings.json` (see `_comments` for plugin keys).
|
||||
|
||||
Canonical source: this folder in **maksit-repoutils**. Product repositories refresh via `Update-RepoUtils` or by copying from here.
|
||||
|
||||
## Modules (orchestration)
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `Release-Package.ps1` | Loads settings, builds `New-EngineContext`, runs plugins in order. |
|
||||
| `PluginSupport.psm1` | Plugin discovery, `Invoke-ConfiguredPlugin`; publish plugins honor `skipPublishPlugins` from `ReleasePublishGuard` (no per-plugin `branches` for GitHub/NuGet/Docker/Helm). |
|
||||
| `ReleaseContext.psm1` | Resolves semver via `Resolve-ReleaseVersion` from `DotNetReleaseVersion.projectFiles` (first `.csproj` `<Version>`) or `NpmReleaseVersion.packageJsonPath`. |
|
||||
| `EngineSupport.psm1` | Warn-only dirty-tree preflight; default `context.tag` = `v{version}` from the configured version plugin; `Initialize-ReleaseStageContext` sets `releaseDir` only. |
|
||||
|
||||
Shared module `../ChangelogSupport.psm1` (repo `src/`) parses release notes for the GitHub plugin: only `## [semver] - YYYY-MM-DD` version lines (Keep a Changelog). The latest header must match `context.version` from the version plugin.
|
||||
|
||||
## Plugins
|
||||
|
||||
`CorePlugins/` — e.g. `DotNetReleaseVersion`, `NpmReleaseVersion`, `NpmBuild`, `NpmPublish`, `DockerPush`, `HelmPush`, `ReleasePublishGuard`. Optional `CustomPlugins/`.
|
||||
|
||||
`DotNetPack` and `QualityGate` (when used) can declare their own `projectFiles`; semver still comes from the configured version plugin (`DotNetReleaseVersion` or `NpmReleaseVersion`).
|
||||
|
||||
## `ReleasePublishGuard`
|
||||
|
||||
Configure this plugin **immediately before** `DockerPush`, `HelmPush`, `GitHub`, `DotNetNuGet`, and `NpmPublish`. It sets shared `skipPublishPlugins` when branch/tag rules fail (`whenRequirementsNotMet`: `skip` or `fail`). Those publish plugins no longer use their own `branches` key — list allowed branches on the guard only. Preflight does not read git tags; the guard sets `context.tag` from `HEAD` when `requireExactTagOnHead` is true. **`context.version` stays from the version plugin** (`DotNetReleaseVersion` or `NpmReleaseVersion`; the guard does not override it). Use `tagVersionMustMatchReleaseVersion` (or legacy `tagVersionMustMatchDotNetRelease` / `tagVersionMustMatchNpmRelease`) to require the git tag semver to match `context.version`.
|
||||
|
||||
## npm workspaces
|
||||
|
||||
For TypeScript monorepos published to npmjs:
|
||||
|
||||
1. `NpmReleaseVersion` — reads `packageJsonPath` (workspace root `package.json`), optional `syncWorkspaceVersions: true` to align `packages/*/package.json` versions.
|
||||
2. `NpmBuild` — `npm ci` + `npm run build` in `workspaceRoot` (defaults to shared `npmWorkspaceRoot` from step 1).
|
||||
3. `ReleasePublishGuard` — same tag/branch rules; set `tagVersionMustMatchReleaseVersion: true`.
|
||||
4. `NpmPublish` — `publishOrder` workspace package names, `npmApiKey` env var name (e.g. `NPMJS_MAKS_IT`), optional `registry` / `access`.
|
||||
|
||||
## `DotNetTest` and shared context
|
||||
|
||||
`DotNetTest` runs once and writes aggregated coverage and test metrics on the shared engine context (`qualityLineCoverage`, `coverageLineRate`, `testResult`, …). `QualityGate` reads those values for optional line-coverage thresholds; it does not re-run tests. Set `scanVulnerabilities` to false to skip `dotnet list package --vulnerable`.
|
||||
|
||||
## Helm charts in git
|
||||
|
||||
Commit `Chart.yaml` with placeholder `version` and `appVersion` (for example `0.0.0`) so `helm lint` stays valid. `HelmPush` temporarily replaces both with the **bare** release semver from `context.version` (`DotNetReleaseVersion`, e.g. `3.3.4` without a `v` prefix) before packaging and OCI push; if `version` were missing, it would fall back to stripping `v`/`V` from `context.tag`. Then it restores `Chart.yaml`. `DockerPush` tags images with the **bare** semver from `context.version` (e.g. `3.3.4`), also pushes `vX.Y.Z` and `shared.tag` when they differ, and optional `latest` — not from `Chart.yaml`; optionally use per-image `versionEnvFiles` to temporarily set `VITE_APP_VERSION={shared.version}` in frontend `.env` files during docker build, then restore originals. Each image may override the plugin `contextPath` with its own `contextPath` (paths relative to Release-Package); `dockerfile` and `versionEnvFiles` resolve against that per-image context.
|
||||
|
||||
Sample chart: repository `charts/my-service/` (matches the sample `chartPath` in `scriptsettings.json`). Product repos often use `src/helm/` instead.
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Release-Package.ps1"
|
||||
pause
|
||||
@ -1,199 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Plugin-driven release engine.
|
||||
|
||||
.DESCRIPTION
|
||||
This script is the orchestration layer for release automation.
|
||||
It loads scriptsettings.json, evaluates the configured plugins in order,
|
||||
builds shared execution context, and invokes each plugin's Invoke-Plugin
|
||||
entrypoint with that plugin's own settings object plus runtime context.
|
||||
|
||||
The engine is intentionally generic:
|
||||
- It does not embed release-provider-specific logic
|
||||
- It preserves plugin execution order from scriptsettings.json
|
||||
- It isolates plugin failures according to the stage/runtime policy
|
||||
- It keeps shared orchestration helpers in dedicated support modules
|
||||
|
||||
.REQUIREMENTS
|
||||
Tools (Required):
|
||||
- Shared support modules required by the engine
|
||||
- Any commands required by configured plugins or support helpers
|
||||
|
||||
.WORKFLOW
|
||||
1. Load and normalize plugin configuration
|
||||
2. Determine branch mode from configured plugin metadata
|
||||
3. Validate repository state and resolve the release version
|
||||
4. Build shared execution context
|
||||
5. Execute plugins one by one in configured order
|
||||
6. Initialize release-stage shared artifacts only when needed
|
||||
7. Report completion summary
|
||||
|
||||
.USAGE
|
||||
Configure plugin order and plugin settings in scriptsettings.json, then run:
|
||||
pwsh -File .\Release-Package.ps1
|
||||
|
||||
.CONFIGURATION
|
||||
All settings are stored in scriptsettings.json:
|
||||
- plugins: Ordered plugin definitions and plugin-specific settings
|
||||
|
||||
.NOTES
|
||||
Plugin-specific behavior belongs in the plugin modules, not in this engine.
|
||||
#>
|
||||
|
||||
# No parameters - behavior is controlled by configured plugin metadata:
|
||||
# - ReleasePublishGuard (before Docker/Helm/GitHub/NuGet): optional branches, tag on HEAD, remote tag; sets skipPublishPlugins.
|
||||
# - Publish plugins do not use per-plugin "branches"; centralize allowed branches on the guard.
|
||||
|
||||
# Get the directory of the current script (for loading settings and relative paths)
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
#region Import Modules
|
||||
|
||||
$utilsDir = Split-Path $scriptDir -Parent
|
||||
|
||||
# Import ScriptConfig module
|
||||
$scriptConfigModulePath = Join-Path $utilsDir "ScriptConfig.psm1"
|
||||
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $scriptConfigModulePath -Force
|
||||
|
||||
# Import Logging module
|
||||
$loggingModulePath = Join-Path $utilsDir "Logging.psm1"
|
||||
if (-not (Test-Path $loggingModulePath)) {
|
||||
Write-Error "Logging module not found at: $loggingModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $loggingModulePath -Force
|
||||
# Import PluginSupport module
|
||||
$pluginSupportModulePath = Join-Path $scriptDir "PluginSupport.psm1"
|
||||
if (-not (Test-Path $pluginSupportModulePath)) {
|
||||
Write-Error "PluginSupport module not found at: $pluginSupportModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $pluginSupportModulePath -Force
|
||||
|
||||
# Import ReleaseContext module (semver resolution for the engine)
|
||||
$releaseContextModulePath = Join-Path $scriptDir "ReleaseContext.psm1"
|
||||
if (-not (Test-Path $releaseContextModulePath)) {
|
||||
Write-Error "ReleaseContext module not found at: $releaseContextModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $releaseContextModulePath -Force
|
||||
|
||||
# Import EngineSupport module
|
||||
$engineSupportModulePath = Join-Path $scriptDir "EngineSupport.psm1"
|
||||
if (-not (Test-Path $engineSupportModulePath)) {
|
||||
Write-Error "EngineSupport module not found at: $engineSupportModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $engineSupportModulePath -Force
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load Settings
|
||||
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||
|
||||
#endregion
|
||||
|
||||
#region Configuration
|
||||
|
||||
$pluginsDir = Join-Path $scriptDir "CorePlugins"
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Main
|
||||
|
||||
Write-Log -Level "STEP" -Message "=================================================="
|
||||
Write-Log -Level "STEP" -Message "RELEASE ENGINE"
|
||||
Write-Log -Level "STEP" -Message "=================================================="
|
||||
|
||||
#region Preflight
|
||||
|
||||
$plugins = $configuredPlugins
|
||||
$engineContext = New-EngineContext -Plugins $plugins -ScriptDir $scriptDir -UtilsDir $utilsDir -Settings $settings
|
||||
Write-Log -Level "OK" -Message "All pre-flight checks passed!"
|
||||
$sharedPluginSettings = $engineContext
|
||||
|
||||
#endregion
|
||||
|
||||
#region Plugin Execution
|
||||
|
||||
$releaseStageInitialized = $false
|
||||
$releaseHadPluginFailures = $false
|
||||
|
||||
if ($plugins.Count -eq 0) {
|
||||
Write-Log -Level "WARN" -Message "No plugins configured in scriptsettings.json."
|
||||
}
|
||||
else {
|
||||
for ($pluginIndex = 0; $pluginIndex -lt $plugins.Count; $pluginIndex++) {
|
||||
$plugin = $plugins[$pluginIndex]
|
||||
$pluginStageLabel = Get-PluginStageLabel -Plugin $plugin
|
||||
|
||||
if ((Test-IsPublishPlugin -Plugin $plugin) -and -not $releaseStageInitialized) {
|
||||
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -PluginsDirectory $pluginsDir -WriteLogs:$false) {
|
||||
$remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)])
|
||||
Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory -Version $engineContext.version
|
||||
$releaseStageInitialized = $true
|
||||
}
|
||||
}
|
||||
|
||||
$continueOnError = $false
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -PluginsDirectory $pluginsDir -ContinueOnError:$continueOnError
|
||||
if (-not $pluginSucceeded) {
|
||||
$releaseHadPluginFailures = $true
|
||||
if (-not $continueOnError) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $releaseStageInitialized) {
|
||||
$noReleasePluginsLogLevel = if ($engineContext.isNonReleaseBranch) { "INFO" } else { "WARN" }
|
||||
Write-Log -Level $noReleasePluginsLogLevel -Message "No release-stage initialization ran (no enabled publish plugins reached, or none runnable)."
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Summary
|
||||
Write-Log -Level "OK" -Message "=================================================="
|
||||
if ($releaseHadPluginFailures) {
|
||||
Write-Log -Level "ERROR" -Message "RELEASE FAILED"
|
||||
}
|
||||
elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins) {
|
||||
Write-Log -Level "OK" -Message "RUN COMPLETE (publish skipped by ReleasePublishGuard)"
|
||||
}
|
||||
elseif ($engineContext.isNonReleaseBranch) {
|
||||
Write-Log -Level "OK" -Message "NON-RELEASE RUN COMPLETE"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "OK" -Message "RELEASE COMPLETE"
|
||||
}
|
||||
Write-Log -Level "OK" -Message "=================================================="
|
||||
|
||||
if ($engineContext.isNonReleaseBranch -and -not ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins)) {
|
||||
$preferredReleaseBranch = Get-PreferredReleaseBranch -EngineContext $engineContext
|
||||
Write-Log -Level "INFO" -Message "For publish, use an allowed branch (see ReleasePublishGuard.branches), e.g. '$preferredReleaseBranch', and satisfy the guard requirements."
|
||||
}
|
||||
|
||||
if ($releaseHadPluginFailures) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
.NET test plugin for executing automated tests.
|
||||
|
||||
.DESCRIPTION
|
||||
Resolves one or more .NET test projects (`project` or `projects`), runs tests once
|
||||
via TestRunner, then publishes metrics on the shared engine context for any later
|
||||
plugin: `qualityLineCoverage`, `testResult`, `coverageLineRate` / `coverageBranchRate` / `coverageMethodRate`,
|
||||
method counts, `testResultsDirectory`, `coverageCoberturaPaths`. Quality gates read
|
||||
those keys generically (not tied to this plugin by name). Cobertura files are removed
|
||||
after parsing unless TestRunner gains KeepResults.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
# Same fallback pattern as the other plugins: use the existing shared module if it is already loaded.
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Invoke-TestsWithCoverage"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$testResultsDirSetting = $pluginSettings.resultsDir
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
|
||||
$testProjectPaths = [System.Collections.Generic.List[string]]::new()
|
||||
if ($pluginSettings.PSObject.Properties.Name -contains 'projects' -and $pluginSettings.projects) {
|
||||
foreach ($rel in @($pluginSettings.projects)) {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$rel)) { continue }
|
||||
$testProjectPaths.Add([System.IO.Path]::GetFullPath((Join-Path $scriptDir $rel.Trim())))
|
||||
}
|
||||
}
|
||||
if ($testProjectPaths.Count -eq 0 -and $pluginSettings.project) {
|
||||
$testProjectPaths.Add([System.IO.Path]::GetFullPath((Join-Path $scriptDir $pluginSettings.project)))
|
||||
}
|
||||
if ($testProjectPaths.Count -eq 0) {
|
||||
throw "DotNetTest plugin requires 'project' or 'projects' in scriptsettings.json."
|
||||
}
|
||||
|
||||
$testResultsDir = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($testResultsDirSetting)) {
|
||||
$testResultsDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $testResultsDirSetting))
|
||||
}
|
||||
elseif ($testProjectPaths.Count -gt 1) {
|
||||
$testResultsDir = [System.IO.Path]::GetFullPath((Join-Path $scriptDir "TestResults"))
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Running tests..."
|
||||
|
||||
# Build a splatted hashtable so optional arguments can be added without duplicating the call site.
|
||||
$invokeTestParams = @{
|
||||
TestProjectPath = @($testProjectPaths)
|
||||
Silent = $true
|
||||
}
|
||||
if ($testResultsDir) {
|
||||
$invokeTestParams.ResultsDirectory = $testResultsDir
|
||||
}
|
||||
|
||||
$testResult = Invoke-TestsWithCoverage @invokeTestParams
|
||||
|
||||
if (-not $testResult.Success) {
|
||||
throw "Tests failed. $($testResult.Error)"
|
||||
}
|
||||
|
||||
$sharedSettings | Add-Member -NotePropertyName testResult -NotePropertyValue $testResult -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName qualityLineCoverage -NotePropertyValue $testResult.LineRate -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName coverageLineRate -NotePropertyValue $testResult.LineRate -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName coverageBranchRate -NotePropertyValue $testResult.BranchRate -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName coverageMethodRate -NotePropertyValue $testResult.MethodRate -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName coverageTotalMethods -NotePropertyValue $testResult.TotalMethods -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName coverageCoveredMethods -NotePropertyValue $testResult.CoveredMethods -Force
|
||||
if (($testResult.PSObject.Properties.Name -contains 'ResultsDirectory') -and $testResult.ResultsDirectory) {
|
||||
$sharedSettings | Add-Member -NotePropertyName testResultsDirectory -NotePropertyValue $testResult.ResultsDirectory -Force
|
||||
}
|
||||
if ($testResult.CoverageFiles) {
|
||||
$sharedSettings | Add-Member -NotePropertyName coverageCoberturaPaths -NotePropertyValue @($testResult.CoverageFiles) -Force
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " All tests passed!"
|
||||
Write-Log -Level "INFO" -Message " Line Coverage: $($testResult.LineRate)%"
|
||||
Write-Log -Level "INFO" -Message " Branch Coverage: $($testResult.BranchRate)%"
|
||||
Write-Log -Level "INFO" -Message " Method Coverage: $($testResult.MethodRate)%"
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin
|
||||
@ -1,184 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Quality gate plugin (coverage threshold + optional .NET vulnerability scan).
|
||||
|
||||
.DESCRIPTION
|
||||
Does not run tests or collect coverage. It reads whatever prior plugins left on the
|
||||
shared engine context (same object passed to every plugin as .context).
|
||||
|
||||
Line coverage for threshold checks is resolved in order (first present wins):
|
||||
- qualityLineCoverage (generic; any plugin may set this)
|
||||
- coverageLineRate (conventional flat metric)
|
||||
- testResult.LineRate (object from a test plugin; property name is conventional)
|
||||
|
||||
Configure coverageThreshold > 0 to require one of those inputs. With coverageThreshold 0
|
||||
and scanVulnerabilities false, the plugin is a no-op.
|
||||
|
||||
When scanVulnerabilities is true, runs dotnet list package --vulnerable on projectFiles.
|
||||
|
||||
Use stageLabel "qualityGate" in scriptsettings.json; plugin module: CorePlugins/QualityGate.psm1 (`"name": "QualityGate"`).
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Test-VulnerablePackagesInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$ProjectFiles
|
||||
)
|
||||
|
||||
$findings = @()
|
||||
|
||||
foreach ($projectPath in $ProjectFiles) {
|
||||
Write-Log -Level "STEP" -Message "Checking vulnerable packages: $([System.IO.Path]::GetFileName($projectPath))"
|
||||
|
||||
$output = & dotnet list $projectPath package --vulnerable --include-transitive 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet list package --vulnerable failed for $projectPath."
|
||||
}
|
||||
|
||||
$outputText = ($output | Out-String)
|
||||
if ($outputText -match "(?im)\bhas the following vulnerable packages\b" -or $outputText -match "(?im)^\s*>\s+[A-Za-z0-9_.-]+\s") {
|
||||
$findings += [pscustomobject]@{
|
||||
Project = $projectPath
|
||||
Output = $outputText.Trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $findings
|
||||
}
|
||||
|
||||
function Get-LineCoveragePercentFromSharedContext {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Shared
|
||||
)
|
||||
|
||||
foreach ($prop in @('qualityLineCoverage', 'coverageLineRate')) {
|
||||
if ($Shared.PSObject.Properties.Name -contains $prop) {
|
||||
$raw = $Shared.$prop
|
||||
if ($null -eq $raw) { continue }
|
||||
$asString = [string]$raw
|
||||
if ([string]::IsNullOrWhiteSpace($asString)) { continue }
|
||||
return [double]$asString
|
||||
}
|
||||
}
|
||||
|
||||
if ($Shared.PSObject.Properties.Name -contains 'testResult' -and $null -ne $Shared.testResult) {
|
||||
$tr = $Shared.testResult
|
||||
if ($tr.PSObject.Properties.Name -contains 'LineRate') {
|
||||
return [double]$tr.LineRate
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
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 "ReleaseContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
$coverageThresholdSetting = $pluginSettings.coverageThreshold
|
||||
$failOnVulnerabilitiesSetting = $pluginSettings.failOnVulnerabilities
|
||||
$scanVulnerabilities = $true
|
||||
if ($null -ne $pluginSettings.scanVulnerabilities) {
|
||||
$scanVulnerabilities = [bool]$pluginSettings.scanVulnerabilities
|
||||
}
|
||||
|
||||
if ($pluginSettings.PSObject.Properties['projectFiles'] -and $null -ne $pluginSettings.projectFiles) {
|
||||
$projectFiles = @(Resolve-RelativePaths -Value $pluginSettings.projectFiles -BasePath $scriptDir)
|
||||
}
|
||||
elseif ($sharedSettings.PSObject.Properties['projectFiles'] -and $null -ne $sharedSettings.projectFiles) {
|
||||
$projectFiles = @($sharedSettings.projectFiles)
|
||||
}
|
||||
else {
|
||||
$projectFiles = @()
|
||||
}
|
||||
|
||||
$coverageThreshold = 0
|
||||
if ($null -ne $coverageThresholdSetting) {
|
||||
$coverageThreshold = [double]$coverageThresholdSetting
|
||||
}
|
||||
|
||||
$needCoverageCheck = $coverageThreshold -gt 0
|
||||
if (-not $needCoverageCheck -and -not $scanVulnerabilities) {
|
||||
Write-Log -Level "INFO" -Message " Quality gate: no checks enabled (coverageThreshold 0, scanVulnerabilities false)."
|
||||
return
|
||||
}
|
||||
|
||||
$lineRate = $null
|
||||
if ($needCoverageCheck) {
|
||||
$lineRate = Get-LineCoveragePercentFromSharedContext -Shared $sharedSettings
|
||||
if ($null -eq $lineRate) {
|
||||
throw "coverageThreshold is $coverageThreshold but shared context has no line coverage. Set one of: qualityLineCoverage, coverageLineRate, or testResult.LineRate (from an earlier plugin)."
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Checking line coverage threshold against shared context..."
|
||||
if ($lineRate -lt $coverageThreshold) {
|
||||
throw "Line coverage $lineRate% is below the configured threshold of $coverageThreshold%."
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " Coverage threshold met: $lineRate% >= $coverageThreshold%"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "INFO" -Message " Coverage threshold check not required (coverageThreshold is 0)."
|
||||
}
|
||||
|
||||
if (-not $scanVulnerabilities) {
|
||||
Write-Log -Level "INFO" -Message " Vulnerability scan skipped (scanVulnerabilities is false)."
|
||||
return
|
||||
}
|
||||
|
||||
Assert-Command dotnet
|
||||
|
||||
$failOnVulnerabilities = $true
|
||||
if ($null -ne $failOnVulnerabilitiesSetting) {
|
||||
$failOnVulnerabilities = [bool]$failOnVulnerabilitiesSetting
|
||||
}
|
||||
|
||||
if ($projectFiles.Count -eq 0) {
|
||||
throw "QualityGate requires projectFiles when scanVulnerabilities is true."
|
||||
}
|
||||
|
||||
$vulnerabilities = Test-VulnerablePackagesInternal -ProjectFiles $projectFiles
|
||||
|
||||
if ($vulnerabilities.Count -eq 0) {
|
||||
Write-Log -Level "OK" -Message " No vulnerable packages detected."
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($finding in $vulnerabilities) {
|
||||
Write-Log -Level "WARN" -Message " Vulnerable packages detected in $([System.IO.Path]::GetFileName($finding.Project))"
|
||||
$finding.Output -split "`r?`n" | ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace($_)) {
|
||||
Write-Log -Level "WARN" -Message " $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($failOnVulnerabilities) {
|
||||
throw "Vulnerable packages were detected and failOnVulnerabilities is enabled."
|
||||
}
|
||||
|
||||
Write-Log -Level "WARN" -Message "Vulnerable packages detected, but failOnVulnerabilities is disabled."
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin
|
||||
@ -1,58 +0,0 @@
|
||||
# Run Tests
|
||||
|
||||
Plugin-driven test engine (same pattern as `src/Release-Package`).
|
||||
|
||||
## Run
|
||||
|
||||
```powershell
|
||||
pwsh -File .\src\Run-Tests\Run-Tests.ps1
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```bat
|
||||
src\Run-Tests\Run-Tests.bat
|
||||
```
|
||||
|
||||
## Core plugins
|
||||
|
||||
| Plugin | Role |
|
||||
|--------|------|
|
||||
| `DotNetTest` | `dotnet test` + Coverlet Cobertura (`.NET` repos) |
|
||||
| `NpmJestTest` | `npm test -- --coverage` + Jest `coverage-summary.json` |
|
||||
| `QualityGate` | Optional line-coverage threshold from shared context |
|
||||
| `CoverageBadges` | SVG badges for README (`assets/badges/`) |
|
||||
|
||||
Configure plugin order and settings in `scriptsettings.json`.
|
||||
|
||||
## Shared context
|
||||
|
||||
Test plugins publish metrics for downstream plugins:
|
||||
|
||||
- `qualityLineCoverage`, `coverageLineRate`, `coverageBranchRate`, `coverageMethodRate`
|
||||
- `testResult` (full result object from `TestRunner`)
|
||||
|
||||
`QualityGate` and `CoverageBadges` read these keys; they do not re-run tests.
|
||||
|
||||
## npm/Jest example
|
||||
|
||||
Replace `DotNetTest` with:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "NpmJestTest",
|
||||
"stageLabel": "test",
|
||||
"enabled": true,
|
||||
"workspaceRoot": "..\\..\\src",
|
||||
"testScript": "test",
|
||||
"coverageDirectory": "coverage"
|
||||
}
|
||||
```
|
||||
|
||||
## Legacy entry point
|
||||
|
||||
`src/Generate-CoverageBadges/Generate-CoverageBadges.ps1` forwards to this engine.
|
||||
|
||||
## Custom plugins
|
||||
|
||||
Add `CustomPlugins/YourPlugin.psm1` with `Invoke-Plugin`, then register it in `scriptsettings.json`.
|
||||
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Run-Tests.ps1"
|
||||
pause
|
||||
@ -1,91 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Plugin-driven test and coverage engine.
|
||||
|
||||
.DESCRIPTION
|
||||
Loads scriptsettings.json, builds shared execution context, and runs configured
|
||||
plugins in order. Each plugin implements Invoke-Plugin and receives its own
|
||||
settings plus shared context on Settings.context.
|
||||
|
||||
.USAGE
|
||||
pwsh -File .\Run-Tests.ps1
|
||||
#>
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$utilsDir = Split-Path $scriptDir -Parent
|
||||
|
||||
$scriptConfigModulePath = Join-Path $utilsDir "ScriptConfig.psm1"
|
||||
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||
exit 1
|
||||
}
|
||||
Import-Module $scriptConfigModulePath -Force
|
||||
|
||||
$loggingModulePath = Join-Path $utilsDir "Logging.psm1"
|
||||
if (-not (Test-Path $loggingModulePath)) {
|
||||
Write-Error "Logging module not found at: $loggingModulePath"
|
||||
exit 1
|
||||
}
|
||||
Import-Module $loggingModulePath -Force
|
||||
|
||||
$pluginSupportModulePath = Join-Path $scriptDir "PluginSupport.psm1"
|
||||
if (-not (Test-Path $pluginSupportModulePath)) {
|
||||
Write-Error "PluginSupport module not found at: $pluginSupportModulePath"
|
||||
exit 1
|
||||
}
|
||||
Import-Module $pluginSupportModulePath -Force
|
||||
|
||||
$engineSupportModulePath = Join-Path $scriptDir "EngineSupport.psm1"
|
||||
if (-not (Test-Path $engineSupportModulePath)) {
|
||||
Write-Error "EngineSupport module not found at: $engineSupportModulePath"
|
||||
exit 1
|
||||
}
|
||||
Import-Module $engineSupportModulePath -Force
|
||||
|
||||
$releaseContextModulePath = Join-Path $utilsDir "Release-Package\ReleaseContext.psm1"
|
||||
if (-not (Test-Path $releaseContextModulePath)) {
|
||||
Write-Error "ReleaseContext module not found at: $releaseContextModulePath"
|
||||
exit 1
|
||||
}
|
||||
Import-Module $releaseContextModulePath -Force
|
||||
|
||||
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||
$pluginsDir = Join-Path $scriptDir "CorePlugins"
|
||||
|
||||
Write-Log -Level "STEP" -Message "=================================================="
|
||||
Write-Log -Level "STEP" -Message "TEST ENGINE"
|
||||
Write-Log -Level "STEP" -Message "=================================================="
|
||||
|
||||
$engineContext = New-EngineContext -ScriptDir $scriptDir -UtilsDir $utilsDir -Settings $settings
|
||||
|
||||
if ($configuredPlugins.Count -eq 0) {
|
||||
Write-Log -Level "WARN" -Message "No plugins configured in scriptsettings.json."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$testHadPluginFailures = $false
|
||||
|
||||
foreach ($plugin in $configuredPlugins) {
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -PluginsDirectory $pluginsDir -ContinueOnError:$false
|
||||
if (-not $pluginSucceeded) {
|
||||
$testHadPluginFailures = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message "=================================================="
|
||||
if ($testHadPluginFailures) {
|
||||
Write-Log -Level "ERROR" -Message "TEST RUN FAILED"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "OK" -Message "TEST RUN COMPLETE"
|
||||
}
|
||||
Write-Log -Level "OK" -Message "=================================================="
|
||||
|
||||
if ($testHadPluginFailures) {
|
||||
exit 1
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Update-RepoUtils.ps1"
|
||||
pause
|
||||
3
utils/src/Force-AmendTaggedCommit.bat
Normal file
3
utils/src/Force-AmendTaggedCommit.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1" %*
|
||||
pause
|
||||
3
utils/src/Invoke-ReleasePackage.bat
Normal file
3
utils/src/Invoke-ReleasePackage.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0engines\release\Invoke-ReleasePackage.ps1" %*
|
||||
pause
|
||||
3
utils/src/Invoke-TestEngine.bat
Normal file
3
utils/src/Invoke-TestEngine.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0engines\test\Invoke-TestEngine.ps1" %*
|
||||
pause
|
||||
3
utils/src/Update-RepoUtils.bat
Normal file
3
utils/src/Update-RepoUtils.bat
Normal file
@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\Update-RepoUtils\Update-RepoUtils.ps1" %*
|
||||
pause
|
||||
80
utils/src/engines/release/Invoke-ReleasePackage.ps1
Normal file
80
utils/src/engines/release/Invoke-ReleasePackage.ps1
Normal file
@ -0,0 +1,80 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Plugin-driven release engine entry script.
|
||||
#>
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$srcDir = (Resolve-Path (Join-Path $scriptDir '..\..')).Path
|
||||
|
||||
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
|
||||
Import-EngineModules -Engine Release
|
||||
|
||||
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||
|
||||
Write-Log -Level 'STEP' -Message '=================================================='
|
||||
Write-Log -Level 'STEP' -Message 'RELEASE ENGINE'
|
||||
Write-Log -Level 'STEP' -Message '=================================================='
|
||||
|
||||
$plugins = $configuredPlugins
|
||||
$engineContext = New-EngineContext -Plugins $plugins -ScriptDir $scriptDir -SrcDir $srcDir -Settings $settings
|
||||
Write-Log -Level 'OK' -Message 'All pre-flight checks passed!'
|
||||
$sharedPluginSettings = $engineContext
|
||||
|
||||
$releaseStageInitialized = $false
|
||||
$releaseHadPluginFailures = $false
|
||||
|
||||
if ($plugins.Count -eq 0) {
|
||||
Write-Log -Level 'WARN' -Message 'No plugins configured in scriptSettings.json.'
|
||||
}
|
||||
else {
|
||||
for ($pluginIndex = 0; $pluginIndex -lt $plugins.Count; $pluginIndex++) {
|
||||
$plugin = $plugins[$pluginIndex]
|
||||
|
||||
if ((Test-IsPublishPlugin -Plugin $plugin) -and -not $releaseStageInitialized) {
|
||||
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $scriptDir -WriteLogs:$false) {
|
||||
$remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)])
|
||||
Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory -Version $engineContext.version
|
||||
$releaseStageInitialized = $true
|
||||
}
|
||||
}
|
||||
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $scriptDir -ContinueOnError:$false
|
||||
if (-not $pluginSucceeded) {
|
||||
$releaseHadPluginFailures = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $releaseStageInitialized) {
|
||||
$noReleasePluginsLogLevel = if ($engineContext.isNonReleaseBranch) { 'INFO' } else { 'WARN' }
|
||||
Write-Log -Level $noReleasePluginsLogLevel -Message 'No release-stage initialization ran (no enabled publish plugins reached, or none runnable).'
|
||||
}
|
||||
|
||||
Write-Log -Level 'OK' -Message '=================================================='
|
||||
if ($releaseHadPluginFailures) {
|
||||
Write-Log -Level 'ERROR' -Message 'RELEASE FAILED'
|
||||
}
|
||||
elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins) {
|
||||
Write-Log -Level 'OK' -Message 'RUN COMPLETE (publish skipped by ReleasePublishGuard)'
|
||||
}
|
||||
elseif ($engineContext.isNonReleaseBranch) {
|
||||
Write-Log -Level 'OK' -Message 'NON-RELEASE RUN COMPLETE'
|
||||
}
|
||||
else {
|
||||
Write-Log -Level 'OK' -Message 'RELEASE COMPLETE'
|
||||
}
|
||||
Write-Log -Level 'OK' -Message '=================================================='
|
||||
|
||||
if ($engineContext.isNonReleaseBranch -and -not ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -and $engineContext.skipPublishPlugins)) {
|
||||
$preferredReleaseBranch = Get-PreferredReleaseBranch -EngineContext $engineContext
|
||||
Write-Log -Level 'INFO' -Message "For publish, use an allowed branch (see ReleasePublishGuard.branches), e.g. '$preferredReleaseBranch', and satisfy the guard requirements."
|
||||
}
|
||||
|
||||
if ($releaseHadPluginFailures) {
|
||||
exit 1
|
||||
}
|
||||
@ -7,14 +7,14 @@
|
||||
"name": "NpmReleaseVersion",
|
||||
"stageLabel": "build",
|
||||
"enabled": true,
|
||||
"packageJsonPath": "..\\..\\src\\package.json",
|
||||
"packageJsonPath": "..\\..\\..\\..\\src\\package.json",
|
||||
"syncWorkspaceVersions": true
|
||||
},
|
||||
{
|
||||
"name": "NpmBuild",
|
||||
"stageLabel": "build",
|
||||
"enabled": true,
|
||||
"workspaceRoot": "..\\..\\src",
|
||||
"workspaceRoot": "..\\..\\..\\..\\src",
|
||||
"useCi": true,
|
||||
"buildScript": "build"
|
||||
},
|
||||
@ -39,7 +39,7 @@
|
||||
"npmApiKey": "NPMJS_MAKS_IT",
|
||||
"registry": "https://registry.npmjs.org",
|
||||
"access": "public",
|
||||
"workspaceRoot": "..\\..\\src",
|
||||
"workspaceRoot": "..\\..\\..\\..\\src",
|
||||
"publishOrder": [
|
||||
"@maks-it.com/webui-contracts",
|
||||
"@maks-it.com/webui-core",
|
||||
@ -53,7 +53,7 @@
|
||||
"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/Release-Package/README.md)."
|
||||
"maksit-repoutils": "Engine docs: https://github.com/MAKS-IT-COM/maksit-repoutils (src/engines/release/README.md)."
|
||||
}
|
||||
}
|
||||
}
|
||||
50
utils/src/engines/test/Invoke-TestEngine.ps1
Normal file
50
utils/src/engines/test/Invoke-TestEngine.ps1
Normal file
@ -0,0 +1,50 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Plugin-driven test and coverage engine entry script.
|
||||
#>
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$srcDir = (Resolve-Path (Join-Path $scriptDir '..\..')).Path
|
||||
|
||||
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
|
||||
Import-EngineModules -Engine Test
|
||||
|
||||
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||
|
||||
Write-Log -Level 'STEP' -Message '=================================================='
|
||||
Write-Log -Level 'STEP' -Message 'TEST ENGINE'
|
||||
Write-Log -Level 'STEP' -Message '=================================================='
|
||||
|
||||
$engineContext = New-EngineContext -ScriptDir $scriptDir -SrcDir $srcDir -Settings $settings
|
||||
|
||||
if ($configuredPlugins.Count -eq 0) {
|
||||
Write-Log -Level 'WARN' -Message 'No plugins configured in scriptSettings.json.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$testHadPluginFailures = $false
|
||||
|
||||
foreach ($plugin in $configuredPlugins) {
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -EngineDirectory $scriptDir -ContinueOnError:$false
|
||||
if (-not $pluginSucceeded) {
|
||||
$testHadPluginFailures = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Write-Log -Level 'OK' -Message '=================================================='
|
||||
if ($testHadPluginFailures) {
|
||||
Write-Log -Level 'ERROR' -Message 'TEST RUN FAILED'
|
||||
}
|
||||
else {
|
||||
Write-Log -Level 'OK' -Message 'TEST RUN COMPLETE'
|
||||
}
|
||||
Write-Log -Level 'OK' -Message '=================================================='
|
||||
|
||||
if ($testHadPluginFailures) {
|
||||
exit 1
|
||||
}
|
||||
@ -3,14 +3,14 @@
|
||||
"title": "Run Tests Script Settings",
|
||||
"description": "maksit-webui: plugin-driven Jest tests and coverage badges.",
|
||||
"paths": {
|
||||
"badgesDir": "..\\..\\assets\\badges"
|
||||
"badgesDir": "..\\..\\..\\..\\assets\\badges"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "NpmJestTest",
|
||||
"stageLabel": "test",
|
||||
"enabled": true,
|
||||
"workspaceRoot": "..\\..\\src",
|
||||
"workspaceRoot": "..\\..\\..\\..\\src",
|
||||
"testScript": "test",
|
||||
"coverageDirectory": "coverage"
|
||||
},
|
||||
@ -25,7 +25,7 @@
|
||||
"name": "CoverageBadges",
|
||||
"stageLabel": "report",
|
||||
"enabled": true,
|
||||
"badgesDir": "..\\..\\assets\\badges",
|
||||
"badgesDir": "..\\..\\..\\..\\assets\\badges",
|
||||
"badges": [
|
||||
{
|
||||
"name": "coverage-lines.svg",
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Helpers to resolve release semver from plugin configuration.
|
||||
Helpers to resolve engine semver and relative paths from plugin configuration.
|
||||
|
||||
.DESCRIPTION
|
||||
Used by New-EngineContext and version plugins:
|
||||
@ -119,7 +119,7 @@ function Resolve-DotNetReleaseVersion {
|
||||
|
||||
$releaseVersionPlugin = @($Plugins | Where-Object { $_.name -eq 'DotNetReleaseVersion' } | Select-Object -First 1)
|
||||
if ($releaseVersionPlugin.Count -eq 0 -or $null -eq $releaseVersionPlugin[0]) {
|
||||
Write-Error "Configure a DotNetReleaseVersion plugin in scriptsettings.json with projectFiles."
|
||||
Write-Error "Configure a DotNetReleaseVersion plugin in scriptSettings.json with projectFiles."
|
||||
exit 1
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ function Resolve-NpmReleaseVersion {
|
||||
|
||||
$releaseVersionPlugin = @($Plugins | Where-Object { $_.name -eq 'NpmReleaseVersion' } | Select-Object -First 1)
|
||||
if ($releaseVersionPlugin.Count -eq 0 -or $null -eq $releaseVersionPlugin[0]) {
|
||||
Write-Error "Configure an NpmReleaseVersion plugin in scriptsettings.json with packageJsonPath."
|
||||
Write-Error "Configure an NpmReleaseVersion plugin in scriptSettings.json with packageJsonPath."
|
||||
exit 1
|
||||
}
|
||||
|
||||
@ -215,7 +215,7 @@ function Resolve-ReleaseVersion {
|
||||
return Resolve-NpmReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
|
||||
}
|
||||
|
||||
Write-Error "Configure a DotNetReleaseVersion plugin (projectFiles) or NpmReleaseVersion plugin (packageJsonPath) in scriptsettings.json."
|
||||
Write-Error "Configure a DotNetReleaseVersion plugin (projectFiles) or NpmReleaseVersion plugin (packageJsonPath) in scriptSettings.json."
|
||||
exit 1
|
||||
}
|
||||
|
||||
35
utils/src/modules/Engine/Import-EngineModules.ps1
Normal file
35
utils/src/modules/Engine/Import-EngineModules.ps1
Normal file
@ -0,0 +1,35 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
function Import-EngineModules {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('Release', 'Test')]
|
||||
[string]$Engine
|
||||
)
|
||||
|
||||
$engineModuleDir = $PSScriptRoot
|
||||
$modulesDir = Split-Path $engineModuleDir -Parent
|
||||
$supportModules = @(
|
||||
(Join-Path $modulesDir 'ScriptConfig.psm1'),
|
||||
(Join-Path $modulesDir 'Logging.psm1'),
|
||||
(Join-Path $engineModuleDir 'PluginSupport.psm1'),
|
||||
(Join-Path $engineModuleDir 'EngineContext.psm1')
|
||||
)
|
||||
|
||||
if ($Engine -eq 'Release') {
|
||||
$supportModules += (Join-Path $engineModuleDir 'ReleaseSupport.psm1')
|
||||
}
|
||||
else {
|
||||
$supportModules += (Join-Path $engineModuleDir 'TestSupport.psm1')
|
||||
}
|
||||
|
||||
foreach ($modulePath in $supportModules) {
|
||||
if (-not (Test-Path $modulePath -PathType Leaf)) {
|
||||
throw "Required module not found at: $modulePath"
|
||||
}
|
||||
|
||||
Import-Module $modulePath -Force
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,16 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
function Get-RepoUtilsSrcDirectory {
|
||||
return (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent)
|
||||
}
|
||||
|
||||
function Get-RepoUtilsModulesDirectory {
|
||||
return Split-Path $PSScriptRoot -Parent
|
||||
}
|
||||
|
||||
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||
$loggingModulePath = Join-Path (Get-RepoUtilsModulesDirectory) "Logging.psm1"
|
||||
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||
Import-Module $loggingModulePath -Force
|
||||
}
|
||||
@ -21,11 +29,14 @@ function Import-PluginDependency {
|
||||
return
|
||||
}
|
||||
|
||||
$moduleRoot = Split-Path $PSScriptRoot -Parent
|
||||
$modulePath = Join-Path $moduleRoot "$ModuleName.psm1"
|
||||
$modulesDir = Get-RepoUtilsModulesDirectory
|
||||
$engineModuleDir = $PSScriptRoot
|
||||
$modulePath = Join-Path $modulesDir "$ModuleName.psm1"
|
||||
if (-not (Test-Path $modulePath -PathType Leaf)) {
|
||||
$modulePath = Join-Path $engineModuleDir "$ModuleName.psm1"
|
||||
}
|
||||
|
||||
if (Test-Path $modulePath -PathType Leaf) {
|
||||
# Import into the global session so the calling plugin can see the exported commands.
|
||||
# Importing only into this module's scope would make the dependency invisible to the plugin.
|
||||
Import-Module $modulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
|
||||
@ -44,8 +55,6 @@ function Get-ConfiguredPlugins {
|
||||
return @()
|
||||
}
|
||||
|
||||
# JSON can deserialize a single plugin as one object or multiple plugins as an array.
|
||||
# Always return an array so the engine can loop without special-case logic.
|
||||
if ($Settings.plugins -is [System.Collections.IEnumerable] -and -not ($Settings.plugins -is [string])) {
|
||||
return @($Settings.plugins)
|
||||
}
|
||||
@ -76,7 +85,6 @@ function Get-PluginBranches {
|
||||
return @()
|
||||
}
|
||||
|
||||
# Strings are also IEnumerable in PowerShell, so exclude them or we would split into characters.
|
||||
if ($Plugin.branches -is [System.Collections.IEnumerable] -and -not ($Plugin.branches -is [string])) {
|
||||
return @($Plugin.branches | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
}
|
||||
@ -119,7 +127,7 @@ function Test-IsPublishPlugin {
|
||||
return $false
|
||||
}
|
||||
|
||||
return @('GitHub', 'DotNetNuGet', 'DockerPush', 'HelmPush', 'NpmPublish') -contains ([string]$Plugin.name)
|
||||
return @('GitHub', 'DotNetNuGet', 'DotNetDockerPush', 'DotNetHelmPush', 'NpmPublish') -contains ([string]$Plugin.name)
|
||||
}
|
||||
|
||||
function Get-PluginSettingValue {
|
||||
@ -174,7 +182,6 @@ function Get-PluginPathListSetting {
|
||||
return @()
|
||||
}
|
||||
|
||||
# Same rule as above: treat a string as one path, not a char-by-char sequence.
|
||||
if ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) {
|
||||
$rawPaths += $value
|
||||
}
|
||||
@ -191,7 +198,6 @@ function Get-PluginPathListSetting {
|
||||
$resolvedPaths += [System.IO.Path]::GetFullPath((Join-Path $BasePath ([string]$path)))
|
||||
}
|
||||
|
||||
# Wrap again to stop PowerShell from unrolling a single-item array into a bare string.
|
||||
return @($resolvedPaths)
|
||||
}
|
||||
|
||||
@ -251,13 +257,17 @@ function Resolve-PluginModulePath {
|
||||
$Plugin,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PluginsDirectory
|
||||
[string]$EngineDirectory
|
||||
)
|
||||
|
||||
$srcDir = Split-Path (Split-Path $EngineDirectory -Parent) -Parent
|
||||
$pluginsRoot = Join-Path $srcDir "plugins"
|
||||
$pluginFileName = "{0}.psm1" -f $Plugin.name
|
||||
$candidatePaths = @(
|
||||
(Join-Path $PluginsDirectory $pluginFileName),
|
||||
(Join-Path (Join-Path (Split-Path $PluginsDirectory -Parent) "CustomPlugins") $pluginFileName)
|
||||
(Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName),
|
||||
(Join-Path (Join-Path $pluginsRoot "Platform") $pluginFileName),
|
||||
(Join-Path (Join-Path $pluginsRoot "DotNet") $pluginFileName),
|
||||
(Join-Path (Join-Path $pluginsRoot "Npm") $pluginFileName)
|
||||
)
|
||||
|
||||
foreach ($candidatePath in $candidatePaths) {
|
||||
@ -278,7 +288,7 @@ function Test-PluginRunnable {
|
||||
[psobject]$SharedSettings,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PluginsDirectory,
|
||||
[string]$EngineDirectory,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[bool]$WriteLogs = $true
|
||||
@ -298,7 +308,7 @@ function Test-PluginRunnable {
|
||||
return $false
|
||||
}
|
||||
|
||||
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -PluginsDirectory $PluginsDirectory
|
||||
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
|
||||
if (-not (Test-Path $pluginModulePath -PathType Leaf)) {
|
||||
if ($WriteLogs) {
|
||||
Write-Log -Level "ERROR" -Message "Plugin module not found: $pluginModulePath"
|
||||
@ -323,7 +333,6 @@ function New-PluginInvocationSettings {
|
||||
$properties[$property.Name] = $property.Value
|
||||
}
|
||||
|
||||
# Plugins receive their own config plus shared runtime context.
|
||||
$properties['context'] = $SharedSettings
|
||||
return [pscustomobject]$properties
|
||||
}
|
||||
@ -337,13 +346,13 @@ function Invoke-ConfiguredPlugin {
|
||||
[psobject]$SharedSettings,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PluginsDirectory,
|
||||
[string]$EngineDirectory,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[bool]$ContinueOnError = $false
|
||||
)
|
||||
|
||||
if (-not (Test-PluginRunnable -Plugin $Plugin -SharedSettings $SharedSettings -PluginsDirectory $PluginsDirectory -WriteLogs:$true)) {
|
||||
if (-not (Test-PluginRunnable -Plugin $Plugin -SharedSettings $SharedSettings -EngineDirectory $EngineDirectory -WriteLogs:$true)) {
|
||||
if ($Plugin.enabled) {
|
||||
return $false
|
||||
}
|
||||
@ -356,13 +365,11 @@ function Invoke-ConfiguredPlugin {
|
||||
return $true
|
||||
}
|
||||
|
||||
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -PluginsDirectory $PluginsDirectory
|
||||
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
|
||||
Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.name)'..."
|
||||
|
||||
try {
|
||||
$moduleInfo = Import-Module $pluginModulePath -Force -PassThru -ErrorAction Stop
|
||||
# Resolve Invoke-Plugin from the imported module explicitly so we call the plugin we just loaded,
|
||||
# not some command with the same name from another module already in session.
|
||||
$invokeCommand = Get-Command -Name "Invoke-Plugin" -Module $moduleInfo.Name -ErrorAction Stop
|
||||
$pluginSettings = New-PluginInvocationSettings -Plugin $Plugin -SharedSettings $SharedSettings
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
$modulesDir = Split-Path $PSScriptRoot -Parent
|
||||
|
||||
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||
$loggingModulePath = Join-Path $modulesDir "Logging.psm1"
|
||||
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||
Import-Module $loggingModulePath -Force
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Get-Command Get-CurrentBranch -ErrorAction SilentlyContinue)) {
|
||||
$gitToolsModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "GitTools.psm1"
|
||||
$gitToolsModulePath = Join-Path $modulesDir "GitTools.psm1"
|
||||
if (Test-Path $gitToolsModulePath -PathType Leaf) {
|
||||
Import-Module $gitToolsModulePath -Force
|
||||
}
|
||||
@ -23,9 +25,9 @@ if (-not (Get-Command Get-PluginStageLabel -ErrorAction SilentlyContinue) -or -n
|
||||
}
|
||||
|
||||
if (-not (Get-Command Resolve-ReleaseVersion -ErrorAction SilentlyContinue)) {
|
||||
$releaseContextModulePath = Join-Path $PSScriptRoot "ReleaseContext.psm1"
|
||||
if (Test-Path $releaseContextModulePath -PathType Leaf) {
|
||||
Import-Module $releaseContextModulePath -Force
|
||||
$engineContextModulePath = Join-Path $PSScriptRoot "EngineContext.psm1"
|
||||
if (Test-Path $engineContextModulePath -PathType Leaf) {
|
||||
Import-Module $engineContextModulePath -Force
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +75,7 @@ function New-EngineContext {
|
||||
[string]$ScriptDir,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$UtilsDir,
|
||||
[string]$SrcDir,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[psobject]$Settings
|
||||
@ -82,11 +84,11 @@ function New-EngineContext {
|
||||
$resolvedVersion = Resolve-ReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
|
||||
$version = $resolvedVersion.version
|
||||
$versionSource = $resolvedVersion.source
|
||||
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir '..\\..\\release'))
|
||||
$releaseRelative = '..\..\..\release'
|
||||
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $releaseRelative))
|
||||
|
||||
$currentBranch = Get-CurrentBranch
|
||||
|
||||
# Hint branches for messaging: ReleasePublishGuard.branches if present, else publish plugins' branches, else main.
|
||||
$releaseBranches = @()
|
||||
foreach ($p in $Plugins) {
|
||||
if (-not $p.enabled) { continue }
|
||||
@ -119,7 +121,8 @@ function New-EngineContext {
|
||||
|
||||
return [pscustomobject]@{
|
||||
scriptDir = $ScriptDir
|
||||
utilsDir = $UtilsDir
|
||||
srcDir = $SrcDir
|
||||
utilsDir = $SrcDir
|
||||
currentBranch = $currentBranch
|
||||
version = $version
|
||||
tag = $tag
|
||||
@ -146,6 +149,3 @@ function Get-PreferredReleaseBranch {
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Assert-WorkingTreeClean, Initialize-ReleaseStageContext, New-EngineContext, Get-PreferredReleaseBranch
|
||||
|
||||
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
$modulesDir = Split-Path $PSScriptRoot -Parent
|
||||
|
||||
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
|
||||
$loggingModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "Logging.psm1"
|
||||
$loggingModulePath = Join-Path $modulesDir "Logging.psm1"
|
||||
if (Test-Path $loggingModulePath -PathType Leaf) {
|
||||
Import-Module $loggingModulePath -Force
|
||||
}
|
||||
@ -14,7 +16,7 @@ function New-EngineContext {
|
||||
[string]$ScriptDir,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$UtilsDir,
|
||||
[string]$SrcDir,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[psobject]$Settings
|
||||
@ -27,7 +29,8 @@ function New-EngineContext {
|
||||
|
||||
return [pscustomobject]@{
|
||||
scriptDir = $ScriptDir
|
||||
utilsDir = $UtilsDir
|
||||
srcDir = $SrcDir
|
||||
utilsDir = $SrcDir
|
||||
badgesDir = $badgesDir
|
||||
}
|
||||
}
|
||||
@ -76,7 +76,7 @@ function Invoke-GitInternal {
|
||||
|
||||
# Used by:
|
||||
# - utils/Release-NuGetPackage/Release-NuGetPackage.ps1
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Resolve and print the current branch name.
|
||||
function Get-CurrentBranch {
|
||||
@ -89,7 +89,7 @@ function Get-CurrentBranch {
|
||||
|
||||
# Used by:
|
||||
# - utils/Release-NuGetPackage/Release-NuGetPackage.ps1
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Return `git status --short` output for pending-change checks.
|
||||
function Get-GitStatusShort {
|
||||
@ -112,7 +112,7 @@ function Get-CurrentCommitTag {
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Get all tag names pointing at HEAD.
|
||||
function Get-HeadTags {
|
||||
@ -144,7 +144,7 @@ function Test-RemoteTagExists {
|
||||
|
||||
# Used by:
|
||||
# - utils/Release-NuGetPackage/Release-NuGetPackage.ps1
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Push tag to remote (optionally with `--force`).
|
||||
function Push-TagToRemote {
|
||||
@ -169,7 +169,7 @@ function Push-TagToRemote {
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Push branch to remote (optionally with `--force`).
|
||||
function Push-BranchToRemote {
|
||||
@ -194,7 +194,7 @@ function Push-BranchToRemote {
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Get HEAD commit hash.
|
||||
function Get-HeadCommitHash {
|
||||
@ -208,7 +208,7 @@ function Get-HeadCommitHash {
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Get HEAD commit subject line.
|
||||
function Get-HeadCommitMessage {
|
||||
@ -216,7 +216,7 @@ function Get-HeadCommitMessage {
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Stage all changes (tracked, untracked, deletions).
|
||||
function Add-AllChanges {
|
||||
@ -224,7 +224,7 @@ function Add-AllChanges {
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Amend HEAD commit and keep existing commit message.
|
||||
function Update-HeadCommitNoEdit {
|
||||
@ -232,7 +232,7 @@ function Update-HeadCommitNoEdit {
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Delete local tag.
|
||||
function Remove-LocalTag {
|
||||
@ -245,7 +245,7 @@ function Remove-LocalTag {
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Create local tag.
|
||||
function New-LocalTag {
|
||||
@ -258,7 +258,7 @@ function New-LocalTag {
|
||||
}
|
||||
|
||||
# Used by:
|
||||
# - utils/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# - tools/Force-AmendTaggedCommit/Force-AmendTaggedCommit.ps1
|
||||
# Purpose:
|
||||
# - Get HEAD one-line commit info.
|
||||
function Get-HeadCommitOneLine {
|
||||
@ -7,7 +7,7 @@ function Get-ScriptSettings {
|
||||
[string]$ScriptDir,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$SettingsFileName = "scriptsettings.json"
|
||||
[string]$SettingsFileName = "scriptSettings.json"
|
||||
)
|
||||
|
||||
$settingsPath = Join-Path $ScriptDir $SettingsFileName
|
||||
@ -3,16 +3,17 @@
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Cleanup plugin for removing generated artifacts after pipeline completion.
|
||||
.NET artifact cleanup plugin — remove NuGet build outputs after release.
|
||||
|
||||
.DESCRIPTION
|
||||
This plugin removes files from the configured artifacts directory using
|
||||
glob patterns. It is typically placed at the end of the Release stage so
|
||||
cleanup becomes explicit and opt-in per repository.
|
||||
Removes files from the configured artifacts directory using glob patterns.
|
||||
Defaults target NuGet outputs (*.nupkg, *.snupkg). Typically placed at the
|
||||
end of the Release stage after DotNetCreateArchive or publish plugins.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -75,7 +76,7 @@ function Invoke-Plugin {
|
||||
$excludePatterns = Get-ExcludePatternsInternal -ConfiguredPatterns $pluginSettings.excludePatterns
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||
throw "CleanupArtifacts plugin requires an artifacts directory in the shared context."
|
||||
throw "DotNetCleanupArtifacts plugin requires an artifacts directory in the shared context."
|
||||
}
|
||||
|
||||
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
||||
@ -3,16 +3,17 @@
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a release zip from prepared build artifacts.
|
||||
.NET release archive plugin — zip from NuGet pack/publish artifacts.
|
||||
|
||||
.DESCRIPTION
|
||||
This plugin compresses the release artifact inputs prepared by an earlier
|
||||
producer plugin (for example DotNetPack or DotNetPublish) into a zip file
|
||||
This plugin compresses .NET release artifact inputs prepared by an earlier
|
||||
DotNet plugin (DotNetPack or DotNetPublish) into a zip file
|
||||
and exposes the resulting release assets for later publisher plugins.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -43,11 +44,11 @@ function Invoke-Plugin {
|
||||
}
|
||||
|
||||
if ($archiveInputs.Count -eq 0) {
|
||||
throw "CreateArchive plugin requires prepared artifacts. Run a producer plugin (for example DotNetPack or DotNetPublish) first."
|
||||
throw "DotNetCreateArchive plugin requires prepared artifacts. Run DotNetPack or DotNetPublish first."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||
throw "CreateArchive plugin requires an artifacts directory in the shared context."
|
||||
throw "DotNetCreateArchive plugin requires an artifacts directory in the shared context."
|
||||
}
|
||||
|
||||
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build and push Docker images to a container registry.
|
||||
.NET Docker publish plugin — build and push container images for .NET apps.
|
||||
|
||||
.DESCRIPTION
|
||||
Logs in with credentials from a Base64-encoded username:password environment variable,
|
||||
@ -15,7 +15,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -83,23 +84,23 @@ function Invoke-Plugin {
|
||||
Assert-Command docker
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.registryUrl)) {
|
||||
throw "DockerPush plugin requires 'registryUrl' (registry hostname, no scheme)."
|
||||
throw "DotNetDockerPush plugin requires 'registryUrl' (registry hostname, no scheme)."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.credentialsEnvVar)) {
|
||||
throw "DockerPush plugin requires 'credentialsEnvVar' (name of env var holding base64 username:password)."
|
||||
throw "DotNetDockerPush plugin requires 'credentialsEnvVar' (name of env var holding base64 username:password)."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.projectName)) {
|
||||
throw "DockerPush plugin requires 'projectName' (image path segment after registry)."
|
||||
throw "DotNetDockerPush plugin requires 'projectName' (image path segment after registry)."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.contextPath)) {
|
||||
throw "DockerPush plugin requires 'contextPath' (Docker build context, relative to Release-Package folder)."
|
||||
throw "DotNetDockerPush plugin requires 'contextPath' (Docker build context, relative to engines/release folder)."
|
||||
}
|
||||
|
||||
if (-not $pluginSettings.images -or @($pluginSettings.images).Count -eq 0) {
|
||||
throw "DockerPush plugin requires a non-empty 'images' array with 'service' and 'dockerfile' per entry."
|
||||
throw "DotNetDockerPush plugin requires a non-empty 'images' array with 'service' and 'dockerfile' per entry."
|
||||
}
|
||||
|
||||
$scriptDir = $shared.scriptDir
|
||||
@ -119,7 +120,7 @@ function Invoke-Plugin {
|
||||
$bareVersion = ([string]$shared.tag).Trim() -replace '^[vV]', ''
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($bareVersion)) {
|
||||
throw "DockerPush: could not derive version tag (need shared.version from DotNetReleaseVersion or shared.tag)."
|
||||
throw "DotNetDockerPush: could not derive version tag (need shared.version from DotNetReleaseVersion or shared.tag)."
|
||||
}
|
||||
|
||||
$imageTags = New-Object System.Collections.Generic.List[string]
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Package a Helm chart and push it to an OCI registry.
|
||||
.NET Helm publish plugin — package and push charts versioned from DotNetReleaseVersion.
|
||||
|
||||
.DESCRIPTION
|
||||
The chart in the repo should keep placeholder version and appVersion (e.g. 0.0.0); this plugin
|
||||
@ -16,7 +16,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -65,15 +66,15 @@ function Invoke-Plugin {
|
||||
$pushLatest = if ($null -ne $pluginSettings.pushLatest) { [bool]$pluginSettings.pushLatest } else { $false }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.chartPath)) {
|
||||
throw "HelmPush plugin requires 'chartPath' (chart directory, relative to Release-Package folder)."
|
||||
throw "DotNetHelmPush plugin requires 'chartPath' (chart directory, relative to engines/release folder)."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.ociRepository)) {
|
||||
throw "HelmPush plugin requires 'ociRepository' (e.g. oci://cr.maks-it.com/charts)."
|
||||
throw "DotNetHelmPush plugin requires 'ociRepository' (e.g. oci://cr.maks-it.com/charts)."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($pluginSettings.credentialsEnvVar)) {
|
||||
throw "HelmPush plugin requires 'credentialsEnvVar' (name of env var holding base64 username:password)."
|
||||
throw "DotNetHelmPush plugin requires 'credentialsEnvVar' (name of env var holding base64 username:password)."
|
||||
}
|
||||
|
||||
$scriptDir = $shared.ScriptDir
|
||||
@ -11,7 +11,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -38,7 +39,7 @@ function Invoke-Plugin {
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($nugetApiKeyEnvVar)) {
|
||||
throw "DotNetNuGet plugin requires 'nugetApiKey' in scriptsettings.json."
|
||||
throw "DotNetNuGet plugin requires 'nugetApiKey' in scriptSettings.json."
|
||||
}
|
||||
|
||||
$nugetApiKey = [System.Environment]::GetEnvironmentVariable($nugetApiKeyEnvVar)
|
||||
@ -13,7 +13,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
# Load this globally only as a fallback. Re-importing PluginSupport in its own execution path
|
||||
# can invalidate commands already resolved by the release engine.
|
||||
@ -29,7 +30,7 @@ function Invoke-Plugin {
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "ReleaseContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
@ -12,7 +12,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -7,11 +7,12 @@
|
||||
|
||||
.DESCRIPTION
|
||||
Dedicated version-loading plugin. It reads .csproj version via
|
||||
ReleaseContext helpers and writes Version into the shared runtime context.
|
||||
EngineContext helpers and writes Version into the shared runtime context.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -24,7 +25,7 @@ function Invoke-Plugin {
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ReleaseContext" -RequiredCommand "Resolve-DotNetReleaseVersion"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-DotNetReleaseVersion"
|
||||
|
||||
$shared = $Settings.context
|
||||
$resolved = Resolve-DotNetReleaseVersion -Plugins @($Settings) -ScriptDir $shared.scriptDir
|
||||
@ -15,7 +15,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
# Same fallback pattern as the other plugins: use the existing shared module if it is already loaded.
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
@ -47,7 +48,7 @@ function Invoke-Plugin {
|
||||
$testProjectPaths.Add([System.IO.Path]::GetFullPath((Join-Path $scriptDir $pluginSettings.project)))
|
||||
}
|
||||
if ($testProjectPaths.Count -eq 0) {
|
||||
throw "DotNetTest plugin requires 'project' or 'projects' in scriptsettings.json."
|
||||
throw "DotNetTest plugin requires 'project' or 'projects' in scriptSettings.json."
|
||||
}
|
||||
|
||||
$testResultsDir = $null
|
||||
@ -12,7 +12,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -26,7 +27,7 @@ function Invoke-Plugin {
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "ReleaseContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$shared = $Settings.context
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
npm/Jest test plugin for the Run-Tests engine.
|
||||
npm/Jest test plugin for the test engine.
|
||||
|
||||
.DESCRIPTION
|
||||
Runs Jest with coverage via TestRunner.Invoke-NpmJestTestsWithCoverage and publishes
|
||||
@ -11,7 +11,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -26,7 +27,7 @@ function Invoke-Plugin {
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Invoke-NpmJestTestsWithCoverage"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "ReleaseContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
@ -35,7 +36,7 @@ function Invoke-Plugin {
|
||||
Assert-Command npm
|
||||
|
||||
if (-not $pluginSettings.workspaceRoot) {
|
||||
throw "NpmJestTest plugin requires 'workspaceRoot' in scriptsettings.json."
|
||||
throw "NpmJestTest plugin requires 'workspaceRoot' in scriptSettings.json."
|
||||
}
|
||||
|
||||
$workspaceRoots = @(Resolve-RelativePaths -Value $pluginSettings.workspaceRoot -BasePath $scriptDir)
|
||||
@ -12,7 +12,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -26,7 +27,7 @@ function Invoke-Plugin {
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "ReleaseContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$shared = $Settings.context
|
||||
@ -35,7 +36,7 @@ function Invoke-Plugin {
|
||||
|
||||
$npmApiKeyEnvVar = $pluginSettings.npmApiKey
|
||||
if ([string]::IsNullOrWhiteSpace($npmApiKeyEnvVar)) {
|
||||
throw "NpmPublish plugin requires 'npmApiKey' in scriptsettings.json (environment variable name)."
|
||||
throw "NpmPublish plugin requires 'npmApiKey' in scriptSettings.json (environment variable name)."
|
||||
}
|
||||
|
||||
$npmApiKey = [System.Environment]::GetEnvironmentVariable($npmApiKeyEnvVar)
|
||||
@ -12,7 +12,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -63,14 +64,14 @@ function Invoke-Plugin {
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ReleaseContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$shared = $Settings.context
|
||||
|
||||
$packageJsonPaths = @(Resolve-RelativePaths -Value $pluginSettings.packageJsonPath -BasePath $shared.scriptDir)
|
||||
if ($packageJsonPaths.Count -eq 0) {
|
||||
throw "NpmReleaseVersion plugin requires 'packageJsonPath' in scriptsettings.json."
|
||||
throw "NpmReleaseVersion plugin requires 'packageJsonPath' in scriptSettings.json."
|
||||
}
|
||||
$packageJsonPath = $packageJsonPaths[0]
|
||||
|
||||
@ -3,14 +3,15 @@
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Coverage badge plugin for the Run-Tests engine.
|
||||
Coverage badge plugin for the test engine.
|
||||
|
||||
.DESCRIPTION
|
||||
Reads line/branch/method coverage from shared engine context and writes SVG badges.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -123,7 +124,7 @@ function Invoke-Plugin {
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ReleaseContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
@ -136,7 +137,7 @@ function Invoke-Plugin {
|
||||
$badgesDir = $badgesDirs[0]
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace([string]$badgesDir)) {
|
||||
throw "CoverageBadges requires badgesDir in plugin settings or paths.badgesDir in scriptsettings.json."
|
||||
throw "CoverageBadges requires badgesDir in plugin settings or paths.badgesDir in scriptSettings.json."
|
||||
}
|
||||
|
||||
if (-not (Test-Path $badgesDir)) {
|
||||
@ -14,7 +14,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -110,7 +111,7 @@ function Invoke-Plugin {
|
||||
Assert-Command gh
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($githubTokenEnvVar)) {
|
||||
throw "GitHub plugin requires 'githubToken' in scriptsettings.json."
|
||||
throw "GitHub plugin requires 'githubToken' in scriptSettings.json."
|
||||
}
|
||||
|
||||
$githubToken = [System.Environment]::GetEnvironmentVariable($githubTokenEnvVar)
|
||||
@ -119,7 +120,7 @@ function Invoke-Plugin {
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||
throw "GitHub plugin requires 'releaseNotesFile' in scriptsettings.json."
|
||||
throw "GitHub plugin requires 'releaseNotesFile' in scriptSettings.json."
|
||||
}
|
||||
|
||||
$releaseNotesFile = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $releaseNotesFileSetting))
|
||||
@ -19,11 +19,12 @@
|
||||
|
||||
When scanVulnerabilities is true, runs dotnet list package --vulnerable on projectFiles.
|
||||
|
||||
Use stageLabel "qualityGate" in scriptsettings.json; plugin module: CorePlugins/QualityGate.psm1 (`"name": "QualityGate"`).
|
||||
Use stageLabel "qualityGate" in scriptSettings.json; plugin: plugins/Platform/QualityGate.psm1 (`"name": "QualityGate"`).
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -91,7 +92,7 @@ function Invoke-Plugin {
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "ReleaseContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
@ -3,10 +3,10 @@
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Central gate for publish-stage plugins (Docker, Helm, GitHub, NuGet).
|
||||
Central gate for publish-stage plugins (DotNetDockerPush, DotNetHelmPush, GitHub, DotNetNuGet, NpmPublish).
|
||||
|
||||
.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
|
||||
shared context skipPublishPlugins to false when all configured requirements pass, or true
|
||||
when they do not (whenRequirementsNotMet: skip). Publish plugins no longer use per-plugin
|
||||
branch lists; put allowed branches here instead.
|
||||
@ -19,7 +19,8 @@
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$pluginSupportModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
$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
|
||||
}
|
||||
@ -65,8 +66,7 @@ function Invoke-Plugin {
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
$pluginSupportPath = Join-Path (Split-Path $PSScriptRoot -Parent) "PluginSupport.psm1"
|
||||
Import-Module $pluginSupportPath -Force -Global -ErrorAction Stop
|
||||
Import-PluginDependency -ModuleName "PluginSupport" -RequiredCommand "Get-PluginBranches"
|
||||
|
||||
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Get-GitStatusShort"
|
||||
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Test-RemoteTagExists"
|
||||
@ -13,7 +13,7 @@
|
||||
4. Deletes and recreates the tag on the amended commit
|
||||
5. Force pushes the branch and tag to remote
|
||||
|
||||
All configuration is in scriptsettings.json.
|
||||
All configuration is in scriptSettings.json.
|
||||
|
||||
.PARAMETER DryRun
|
||||
If specified, shows what would be done without making changes.
|
||||
@ -25,7 +25,7 @@
|
||||
pwsh -File .\Force-AmendTaggedCommit.ps1 -DryRun
|
||||
|
||||
.NOTES
|
||||
CONFIGURATION (scriptsettings.json):
|
||||
CONFIGURATION (scriptSettings.json):
|
||||
- git.remote: Remote name to push to (default: "origin")
|
||||
- git.confirmBeforeAmend: Prompt before amending (default: true)
|
||||
- git.confirmWhenNoChanges: Prompt if no pending changes (default: true)
|
||||
@ -37,27 +37,26 @@ param(
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
# Get the directory of the current script (for loading settings and relative paths)
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$utilsDir = Split-Path $scriptDir -Parent
|
||||
$srcDir = Split-Path (Split-Path $scriptDir -Parent) -Parent
|
||||
$modulesDir = Join-Path $srcDir 'modules'
|
||||
|
||||
#region Import Modules
|
||||
|
||||
# Import shared ScriptConfig module (settings loading + dependency checks)
|
||||
$scriptConfigModulePath = Join-Path $utilsDir "ScriptConfig.psm1"
|
||||
$scriptConfigModulePath = Join-Path $modulesDir "ScriptConfig.psm1"
|
||||
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Import shared GitTools module (git operations used by this script)
|
||||
$gitToolsModulePath = Join-Path $utilsDir "GitTools.psm1"
|
||||
$gitToolsModulePath = Join-Path $modulesDir "GitTools.psm1"
|
||||
if (-not (Test-Path $gitToolsModulePath)) {
|
||||
Write-Error "GitTools module not found at: $gitToolsModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$loggingModulePath = Join-Path $utilsDir "Logging.psm1"
|
||||
$loggingModulePath = Join-Path $modulesDir "Logging.psm1"
|
||||
if (-not (Test-Path $loggingModulePath)) {
|
||||
Write-Error "Logging module not found at: $loggingModulePath"
|
||||
exit 1
|
||||
@ -8,16 +8,16 @@
|
||||
.DESCRIPTION
|
||||
This script clones the configured repository into a temporary directory,
|
||||
refreshes the parent directory of this script, preserves existing
|
||||
scriptsettings.json files in subfolders, and copies the cloned source
|
||||
scriptSettings.json files in subfolders, and copies the cloned source
|
||||
contents into that parent directory.
|
||||
|
||||
All configuration is stored in scriptsettings.json.
|
||||
All configuration is stored in scriptSettings.json.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh -File .\Update-RepoUtils.ps1
|
||||
|
||||
.NOTES
|
||||
CONFIGURATION (scriptsettings.json):
|
||||
CONFIGURATION (scriptSettings.json):
|
||||
- dryRun: If true, logs the planned update without modifying files
|
||||
- repository.url: Git repository to clone
|
||||
- repository.sourceSubdirectory: Folder copied into the target directory
|
||||
@ -37,19 +37,19 @@ param(
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Get the directory of the current script (for loading settings and relative paths)
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$utilsDir = Split-Path $scriptDir -Parent
|
||||
$srcDir = Split-Path (Split-Path $scriptDir -Parent) -Parent
|
||||
$modulesDir = Join-Path $srcDir 'modules'
|
||||
|
||||
# Refresh the parent directory that contains the shared modules and sibling tools.
|
||||
# Refresh the src directory that contains modules, engines, plugins, and tools.
|
||||
$targetDirectory = if ([string]::IsNullOrWhiteSpace($TargetDirectoryOverride)) {
|
||||
Split-Path $scriptDir -Parent
|
||||
$srcDir
|
||||
}
|
||||
else {
|
||||
[System.IO.Path]::GetFullPath($TargetDirectoryOverride)
|
||||
}
|
||||
$currentScriptPath = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Path)
|
||||
$selfUpdateDirectory = 'Update-RepoUtils'
|
||||
$selfUpdateDirectory = [System.IO.Path]::Combine('tools', 'Update-RepoUtils')
|
||||
|
||||
function ConvertTo-NormalizedRelativePath {
|
||||
param(
|
||||
@ -90,13 +90,13 @@ function Test-IsInRelativeDirectory {
|
||||
|
||||
#region Import Modules
|
||||
|
||||
$scriptConfigModulePath = Join-Path $utilsDir "ScriptConfig.psm1"
|
||||
$scriptConfigModulePath = Join-Path $modulesDir "ScriptConfig.psm1"
|
||||
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$loggingModulePath = Join-Path $utilsDir "Logging.psm1"
|
||||
$loggingModulePath = Join-Path $modulesDir "Logging.psm1"
|
||||
if (-not (Test-Path $loggingModulePath)) {
|
||||
Write-Error "Logging module not found at: $loggingModulePath"
|
||||
exit 1
|
||||
@ -118,7 +118,7 @@ $settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||
$repositoryUrl = $settings.repository.url
|
||||
$dryRun = if ($null -ne $settings.dryRun) { [bool]$settings.dryRun } else { $false }
|
||||
$sourceSubdirectory = if ($settings.repository.sourceSubdirectory) { $settings.repository.sourceSubdirectory } else { 'src' }
|
||||
$preserveFileName = if ($settings.repository.preserveFileName) { $settings.repository.preserveFileName } else { 'scriptsettings.json' }
|
||||
$preserveFileName = if ($settings.repository.preserveFileName) { $settings.repository.preserveFileName } else { 'scriptSettings.json' }
|
||||
$cloneDepth = if ($settings.repository.cloneDepth) { [int]$settings.repository.cloneDepth } else { 1 }
|
||||
[string[]]$skippedRelativeDirectories = if ($settings.repository.skippedRelativeDirectories) {
|
||||
@(
|
||||
@ -129,7 +129,10 @@ $cloneDepth = if ($settings.repository.cloneDepth) { [int]$settings.repository.c
|
||||
)
|
||||
}
|
||||
else {
|
||||
@([System.IO.Path]::Combine('Release-Package', 'CustomPlugins'))
|
||||
@(
|
||||
[System.IO.Path]::Combine('engines', 'release', 'custom'),
|
||||
[System.IO.Path]::Combine('engines', 'test', 'custom')
|
||||
)
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -140,7 +143,7 @@ Assert-Command git
|
||||
Assert-Command pwsh
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($repositoryUrl)) {
|
||||
Write-Error "repository.url is required in scriptsettings.json."
|
||||
Write-Error "repository.url is required in scriptSettings.json."
|
||||
exit 1
|
||||
}
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
"repository": {
|
||||
"url": "https://github.com/MAKS-IT-COM/maksit-repoutils.git",
|
||||
"sourceSubdirectory": "src",
|
||||
"preserveFileName": "scriptsettings.json",
|
||||
"preserveFileName": "scriptSettings.json",
|
||||
"cloneDepth": 1,
|
||||
"skippedRelativeDirectories": [
|
||||
"Release-Package/CustomPlugins",
|
||||
"Run-Tests/CustomPlugins"
|
||||
"engines/release/custom",
|
||||
"engines/test/custom"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user