mirror of
https://github.com/MAKS-IT-COM/maksit-webui.git
synced 2026-09-30 20:28:09 +02:00
(bugfix): reset DataTable page scroll, key external login buttons, and sync RepoUtils
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
cd575eb926
commit
18b74758bc
15
CHANGELOG.md
15
CHANGELOG.md
@ -4,6 +4,21 @@ 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.4.6] - 2026-09-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- `DataTable`: after a page change, the grid scroll position resets (top for next/buttons, bottom when scrolling into the previous page) so the scrollbar no longer stays parked on the old edge.
|
||||
- `ExternalLoginButtons`: each provider button is keyed, so React no longer warns about a missing key when several providers render.
|
||||
|
||||
### Changed
|
||||
|
||||
- RepoUtils: SemVer prerelease (`X.Y.Z-alpha.1` / `beta` / `rc`) in changelog parsing, version files, publish-guard tags (`v0.1.0-alpha.1`), GitHub `--prerelease`, and npm dist-tags (prerelease does not move `latest`).
|
||||
- RepoUtils: optional Vault secret loading. Release settings declare `RepoUtilsSecretsShared` / `RepoUtilsSecrets`; GitHub uses the `GitClone` slot and npm uses `Npm`. `NpmPublish` stages with `npm stage publish` (npm CLI 11.15+) so a maintainer approves the version with 2FA. Plugin success requires an exact `$true` (CLI stdout mixed into the return value no longer counts as success).
|
||||
- RepoUtils: dropped unused DotNet pack/publish/test plugins and CI Docker/npm pack templates. Added `CollectCoverage`, `ContainerEngineProbe`, and `DiscoverPackageArtifacts`.
|
||||
- README: release steps document prerelease tags and link to maksit-repoutils.
|
||||
- `src/package-lock.json` refreshed for npm 12, and `allowScripts` permits the `esbuild`, `msw`, and `unrs-resolver` install scripts that `npm ci` otherwise blocks.
|
||||
|
||||
## [0.4.5] - 2026-08-12
|
||||
|
||||
### Changed
|
||||
|
||||
10
README.md
10
README.md
@ -30,8 +30,8 @@ Tests and coverage badges (shields.io URLs in this README): **`utils\Invoke-Test
|
||||
|
||||
## Release to npmjs
|
||||
|
||||
1. Set **`Npm`** environment variable to your npm automation token (logical secret name in `scriptSettings.json`).
|
||||
2. Bump **`src/package.json`** `version` (and tag `vX.Y.Z` on `main` when using the publish guard).
|
||||
1. Set **`RepoUtilsSecretsShared`** and **`RepoUtilsSecrets`** to JSON objects. Slots used here: **`Npm`** (npm automation token) and **`GitClone`** (GitHub token). The repo pack overrides the shared pack.
|
||||
2. Bump **`src/package.json`** `version` (and tag `v{version}` on `main` when using the publish guard — `v1.2.3` or SemVer prerelease such as `v0.1.0-alpha.1`). Prerelease versions publish with npm dist-tag `alpha`/`beta`/`rc` and do not move `latest`.
|
||||
3. Run **`utils\Invoke-ReleasePackage.bat`** (or `pwsh utils\engines\release\Invoke-ReleasePackage.ps1`).
|
||||
|
||||
Configured plugins (see `utils\engines\release\scriptSettings.json`):
|
||||
@ -41,10 +41,10 @@ Configured plugins (see `utils\engines\release\scriptSettings.json`):
|
||||
| `NpmReleaseVersion` | Read semver from `src/package.json` |
|
||||
| `NpmBuild` | `npm ci` + `npm run build` |
|
||||
| `ReleasePublishGuard` | Branch/tag checks before publish |
|
||||
| `GitHub` | GitHub release (optional; set `GitHub` env var) |
|
||||
| `NpmPublish` | Publish `@maks-it.com/webui` |
|
||||
| `GitHub` | GitHub release (optional; `GitClone` slot in the secrets pack) |
|
||||
| `NpmPublish` | Stage `@maks-it.com/webui` (`npm stage publish`). A maintainer approves with 2FA (`npm stage approve`). Requires npm CLI 11.15 or newer. |
|
||||
|
||||
Refresh shared utils from **maksit-repoutils** via local-copy sync (no Update-RepoUtils in product repos).
|
||||
Refresh shared utils from **[maksit-repoutils](https://git.maks-it.com/MAKS-IT/maksit-repoutils)** via local-copy sync (no Update-RepoUtils in product repos).
|
||||
|
||||
## Consume in product repos
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ Published package:
|
||||
3. Create an **Automation** token (recommended) or Granular Access token with **Publish** on `@maks-it.com/*`:
|
||||
- https://www.npmjs.com/settings/maks-it.com/tokens
|
||||
4. Store the token for release tooling:
|
||||
- **CI / release engine:** set environment variable **`Npm`** (logical secret name in `scriptSettings.json`).
|
||||
- **CI / release engine:** set **`RepoUtilsSecretsShared`** and **`RepoUtilsSecrets`** to JSON objects. The **`Npm`** slot is the automation token; **`GitClone`** is the GitHub token. The repo pack overrides the shared pack.
|
||||
- **Local one-off publish:** `npm login` or a user-level `~/.npmrc` entry:
|
||||
```
|
||||
//registry.npmjs.org/:_authToken=YOUR_TOKEN
|
||||
@ -48,7 +48,7 @@ Use **`utils\Invoke-ReleasePackage-Single.bat`** (or `pwsh utils\engines\release
|
||||
|
||||
1. Bump version in `src/package.json` (or tag drives `NpmReleaseVersion`).
|
||||
2. Tag `HEAD` with exact semver, e.g. `git tag v0.4.0 && git push origin v0.4.0`.
|
||||
3. Set `$env:Npm` and run the release engine.
|
||||
3. Set `$env:RepoUtilsSecretsShared` and `$env:RepoUtilsSecrets` (JSON packs with `Npm` and `GitClone`) and run the release engine. `NpmPublish` runs `npm stage publish` (npm CLI 11.15+). Approve the staged version with 2FA: `npm stage list @maks-it.com/webui`, then `npm stage approve <stage-id>`.
|
||||
|
||||
`utils\engines\release\scriptSettings.json` runs `NpmReleaseVersion`, `NpmBuild`, `ReleasePublishGuard`, optional `GitHub`, then `NpmPublish`.
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { AutoSizer, MultiGrid, GridCellProps } from 'react-virtualized'
|
||||
import { mapPagedToDataTable, type DataTablePageView, type PagedResponse } from '@webui/core'
|
||||
import { Plus, Trash2, Edit } from 'lucide-react'
|
||||
import { debounce, colSpanClass, type GridColSpan } from '../../functions'
|
||||
import { dataTableScrollEdge, forcedScrollTopFor, SCROLL_EDGE_PX, type DataTableScrollReset } from './dataTableScroll'
|
||||
|
||||
|
||||
interface FilterProps {
|
||||
@ -90,6 +91,10 @@ const DataTable = <T extends Record<string, unknown>,>(props: DataTableProps<T>)
|
||||
|
||||
const gridRef = useRef<MultiGrid>(null)
|
||||
const filterMeasureRef = useRef<HTMLDivElement>(null)
|
||||
const pendingReset = useRef<DataTableScrollReset | null>(null)
|
||||
const suppressEdge = useRef(false)
|
||||
const scrollTopRef = useRef(0)
|
||||
const [forcedScrollTop, setForcedScrollTop] = useState<number | undefined>(undefined)
|
||||
|
||||
const [selectedRowIndex, setSelectedRowIndex] = useState<number | null>(null)
|
||||
const [measuredFilterRowHeight, setMeasuredFilterRowHeight] = useState(0)
|
||||
@ -214,8 +219,35 @@ const DataTable = <T extends Record<string, unknown>,>(props: DataTableProps<T>)
|
||||
debouncedOnFilterChange?.(linqQueries)
|
||||
}
|
||||
|
||||
const handlePreviousPage = () => onPreviousPage?.(pageNumber - 1)
|
||||
const handleNextPage = () => onNextPage?.(pageNumber + 1)
|
||||
const handlePreviousPage = () => {
|
||||
if (pendingReset.current)
|
||||
return
|
||||
|
||||
pendingReset.current = 'top'
|
||||
onPreviousPage?.(pageNumber - 1)
|
||||
}
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (pendingReset.current)
|
||||
return
|
||||
|
||||
pendingReset.current = 'top'
|
||||
onNextPage?.(pageNumber + 1)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const target = pendingReset.current
|
||||
if (!target)
|
||||
return
|
||||
|
||||
pendingReset.current = null
|
||||
|
||||
if (target === 'top' && scrollTopRef.current <= SCROLL_EDGE_PX)
|
||||
return
|
||||
|
||||
suppressEdge.current = true
|
||||
setForcedScrollTop(forcedScrollTopFor(target))
|
||||
}, [pageNumber, items])
|
||||
|
||||
|
||||
const getRealIdsFromRow = (rowIndex: number) => {
|
||||
@ -423,12 +455,32 @@ const DataTable = <T extends Record<string, unknown>,>(props: DataTableProps<T>)
|
||||
clientHeight: number
|
||||
scrollHeight: number
|
||||
}) => {
|
||||
if (scrollTop + clientHeight >= scrollHeight - 2 && hasNextPage) {
|
||||
handleNextPage()
|
||||
}
|
||||
if (scrollTop <= 2 && hasPreviousPage) {
|
||||
handlePreviousPage()
|
||||
scrollTopRef.current = scrollTop
|
||||
|
||||
if (suppressEdge.current) {
|
||||
suppressEdge.current = false
|
||||
setForcedScrollTop(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
const edge = dataTableScrollEdge({
|
||||
scrollTop,
|
||||
clientHeight,
|
||||
scrollHeight,
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
pendingReset: pendingReset.current !== null,
|
||||
})
|
||||
|
||||
if (edge.type === 'none')
|
||||
return
|
||||
|
||||
pendingReset.current = edge.reset
|
||||
|
||||
if (edge.type === 'next')
|
||||
onNextPage?.(pageNumber + 1)
|
||||
else
|
||||
onPreviousPage?.(pageNumber - 1)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -463,6 +515,7 @@ const DataTable = <T extends Record<string, unknown>,>(props: DataTableProps<T>)
|
||||
rowCount={items.length + HEADER_ROWS}
|
||||
rowHeight={({ index }) => index === 1 ? measuredFilterRowHeight : ROW_HEIGHT}
|
||||
width={width}
|
||||
{...(forcedScrollTop !== undefined ? { scrollTop: forcedScrollTop } : {})}
|
||||
onScroll={({ scrollTop, clientHeight, scrollHeight }) =>
|
||||
handleGridScroll({ scrollTop, clientHeight, scrollHeight })
|
||||
}
|
||||
|
||||
49
src/components/components/DataTable/dataTableScroll.ts
Normal file
49
src/components/components/DataTable/dataTableScroll.ts
Normal file
@ -0,0 +1,49 @@
|
||||
export const SCROLL_EDGE_PX = 2
|
||||
|
||||
export type DataTableScrollReset = 'top' | 'bottom'
|
||||
|
||||
export type DataTableScrollEdge =
|
||||
| { type: 'none' }
|
||||
| { type: 'next'; reset: 'top' }
|
||||
| { type: 'previous'; reset: 'bottom' }
|
||||
|
||||
export const dataTableScrollEdge = ({
|
||||
scrollTop,
|
||||
clientHeight,
|
||||
scrollHeight,
|
||||
hasNextPage,
|
||||
hasPreviousPage,
|
||||
pendingReset,
|
||||
}: {
|
||||
scrollTop: number
|
||||
clientHeight: number
|
||||
scrollHeight: number
|
||||
hasNextPage: boolean
|
||||
hasPreviousPage: boolean
|
||||
pendingReset: boolean
|
||||
}): DataTableScrollEdge => {
|
||||
if (pendingReset)
|
||||
return { type: 'none' }
|
||||
|
||||
const overflows = scrollHeight > clientHeight + SCROLL_EDGE_PX
|
||||
if (!overflows)
|
||||
return { type: 'none' }
|
||||
|
||||
const atBottom = scrollTop + clientHeight >= scrollHeight - SCROLL_EDGE_PX
|
||||
if (atBottom && hasNextPage)
|
||||
return { type: 'next', reset: 'top' }
|
||||
|
||||
const atTop = scrollTop <= SCROLL_EDGE_PX
|
||||
if (atTop && hasPreviousPage)
|
||||
return { type: 'previous', reset: 'bottom' }
|
||||
|
||||
return { type: 'none' }
|
||||
}
|
||||
|
||||
/** Pixel `scrollTop` passed to MultiGrid. `undefined` leaves the grid uncontrolled. */
|
||||
export const forcedScrollTopFor = (reset: DataTableScrollReset): number => {
|
||||
if (reset === 'top')
|
||||
return 0
|
||||
|
||||
return Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { Fragment, type FC, type ReactNode } from 'react'
|
||||
import { LoginProviderExternal } from '@webui/contracts'
|
||||
|
||||
export interface ExternalLoginProviderConfig {
|
||||
@ -45,14 +45,16 @@ const ExternalLoginButtons: FC<ExternalLoginButtonsProps> = ({
|
||||
renderButton = defaultRenderButton,
|
||||
}) => (
|
||||
<div className={className ?? 'flex flex-col gap-2'}>
|
||||
{providers.map(({ provider, label, iconSrc }) =>
|
||||
renderButton({
|
||||
provider,
|
||||
label,
|
||||
iconSrc,
|
||||
onClick: () => onSelect(provider),
|
||||
}),
|
||||
)}
|
||||
{providers.map(({ provider, label, iconSrc }) => (
|
||||
<Fragment key={provider}>
|
||||
{renderButton({
|
||||
provider,
|
||||
label,
|
||||
iconSrc,
|
||||
onClick: () => onSelect(provider),
|
||||
})}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
142
src/package-lock.json
generated
142
src/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@maks-it.com/webui",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@maks-it.com/webui",
|
||||
"version": "0.4.3",
|
||||
"version": "0.4.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"date-fns": "^4.4.0"
|
||||
@ -131,7 +131,6 @@
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@ -705,7 +704,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@ -729,7 +727,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@ -757,6 +754,17 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
@ -3566,7 +3574,6 @@
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@ -3863,7 +3870,6 @@
|
||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@ -3874,7 +3880,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@ -4294,7 +4299,6 @@
|
||||
"integrity": "sha512-UDwuWGwXj646CBx/bQHOaJSX7np0I8JL/UKQYa1e4QrVHH8VdWtx8eaOuf8sy0ShwDgR6NjJAsp5eF6vjF6qng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@blazediff/core": "1.9.1",
|
||||
"@vitest/mocker": "4.1.10",
|
||||
@ -4318,7 +4322,6 @@
|
||||
"integrity": "sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/browser": "4.1.10",
|
||||
"@vitest/mocker": "4.1.10",
|
||||
@ -4343,7 +4346,6 @@
|
||||
"integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bcoe/v8-coverage": "^1.0.2",
|
||||
"@vitest/utils": "4.1.10",
|
||||
@ -4493,7 +4495,6 @@
|
||||
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.10",
|
||||
"pathe": "^2.0.3"
|
||||
@ -4915,7 +4916,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
@ -5714,7 +5714,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@ -6622,7 +6621,6 @@
|
||||
"integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.4.2",
|
||||
"@jest/types": "30.4.1",
|
||||
@ -7280,7 +7278,6 @@
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
@ -8035,7 +8032,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/confirm": "^6.0.11",
|
||||
"@mswjs/interceptors": "^0.41.3",
|
||||
@ -8674,7 +8670,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
@ -8838,7 +8833,6 @@
|
||||
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -8881,7 +8875,6 @@
|
||||
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@ -9118,7 +9111,6 @@
|
||||
"integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@ -9372,7 +9364,6 @@
|
||||
"integrity": "sha512-bmLxPsxVSPnbeiZqYQpozyNOiJXfk+pf7WfHZflvPkwT6Y+rvYz3Cj/D6H4Kf2jHpuDNiMXBKO3yawLN2OWirg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/icons": "^2.0.2",
|
||||
@ -10053,7 +10044,6 @@
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -10237,7 +10227,6 @@
|
||||
"integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
@ -10797,7 +10786,6 @@
|
||||
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.1.10",
|
||||
"@vitest/mocker": "4.1.10",
|
||||
@ -11200,110 +11188,6 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"packages/components": {
|
||||
"name": "@maks-it.com/webui-components",
|
||||
"version": "0.3.4",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"@maks-it.com/webui-contracts": "^0.3.4",
|
||||
"@maks-it.com/webui-core": "^0.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/react": "^19.2.15",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-virtualized": "^9.22.3",
|
||||
"lucide-react": "^1.16.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-router-dom": "^7.15.1",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^6.0.3",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-table": "^8.0.0",
|
||||
"lucide-react": "^1.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0",
|
||||
"react-router-dom": "^7.0.0",
|
||||
"react-virtualized": "^9.22.0",
|
||||
"zod": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"packages/contracts": {
|
||||
"name": "@maks-it.com/webui-contracts",
|
||||
"version": "0.3.4",
|
||||
"extraneous": true,
|
||||
"devDependencies": {
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^6.0.3",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@maks-it.com/webui-core",
|
||||
"version": "0.3.4",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"@maks-it.com/webui-contracts": "^0.3.4",
|
||||
"date-fns": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@types/react": "^19.2.15",
|
||||
"axios": "^1.16.1",
|
||||
"react": "^19.2.6",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^6.0.3",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@microsoft/signalr": "^8.0.0",
|
||||
"axios": "^1.16.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"zod": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"packages/webui": {
|
||||
"name": "@maks-it.com/webui",
|
||||
"version": "0.4.0",
|
||||
"extraneous": true,
|
||||
"dependencies": {
|
||||
"date-fns": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/react": "^19.2.15",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-virtualized": "^9.22.3",
|
||||
"axios": "^1.16.1",
|
||||
"lucide-react": "^1.16.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-router-dom": "^7.15.1",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^6.0.3",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@microsoft/signalr": "^8.0.0",
|
||||
"@tanstack/react-table": "^8.0.0",
|
||||
"axios": "^1.16.0",
|
||||
"lucide-react": "^1.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0",
|
||||
"react-router-dom": "^7.0.0",
|
||||
"react-virtualized": "^9.22.0",
|
||||
"zod": "^4.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@maks-it.com/webui",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.6",
|
||||
"description": "Shared contracts, utilities, and React components for MaksIT WebUI apps",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
@ -106,5 +106,11 @@
|
||||
"workerDirectory": [
|
||||
"public"
|
||||
]
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild@0.27.7": true,
|
||||
"esbuild@0.25.12": true,
|
||||
"msw@2.15.0": true,
|
||||
"unrs-resolver@1.12.2": true
|
||||
}
|
||||
}
|
||||
|
||||
59
src/test/components/DataTable/dataTableScroll.test.ts
Normal file
59
src/test/components/DataTable/dataTableScroll.test.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { dataTableScrollEdge, forcedScrollTopFor } from '@webui/components/components/DataTable/dataTableScroll'
|
||||
|
||||
const overflowing = {
|
||||
scrollTop: 0,
|
||||
clientHeight: 200,
|
||||
scrollHeight: 800,
|
||||
hasNextPage: true,
|
||||
hasPreviousPage: true,
|
||||
pendingReset: false,
|
||||
}
|
||||
|
||||
describe('dataTableScrollEdge', () => {
|
||||
it('loads the next page from the bottom and asks to jump to the top', () => {
|
||||
expect(dataTableScrollEdge({ ...overflowing, scrollTop: 600 })).toEqual({
|
||||
type: 'next',
|
||||
reset: 'top',
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the previous page from the top and asks to jump to the bottom', () => {
|
||||
expect(dataTableScrollEdge({ ...overflowing, scrollTop: 1 })).toEqual({
|
||||
type: 'previous',
|
||||
reset: 'bottom',
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores edges while a page reset is in flight', () => {
|
||||
expect(dataTableScrollEdge({ ...overflowing, scrollTop: 600, pendingReset: true })).toEqual({
|
||||
type: 'none',
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores edges when the page does not overflow', () => {
|
||||
expect(dataTableScrollEdge({
|
||||
...overflowing,
|
||||
scrollTop: 0,
|
||||
clientHeight: 400,
|
||||
scrollHeight: 400,
|
||||
})).toEqual({ type: 'none' })
|
||||
})
|
||||
|
||||
it('does not request a page that does not exist', () => {
|
||||
expect(dataTableScrollEdge({
|
||||
...overflowing,
|
||||
scrollTop: 600,
|
||||
hasNextPage: false,
|
||||
})).toEqual({ type: 'none' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('forcedScrollTopFor', () => {
|
||||
it('maps a top reset to scrollTop 0', () => {
|
||||
expect(forcedScrollTopFor('top')).toBe(0)
|
||||
})
|
||||
|
||||
it('maps a bottom reset to a clamped large scrollTop', () => {
|
||||
expect(forcedScrollTopFor('bottom')).toBe(Number.MAX_SAFE_INTEGER)
|
||||
})
|
||||
})
|
||||
@ -35,6 +35,7 @@ else {
|
||||
}
|
||||
|
||||
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
|
||||
Initialize-RepoUtilsVaultSecrets -Settings $settings -Plugins $configuredPlugins
|
||||
|
||||
$releaseBanner = if ($null -ne $releaseExtension) {
|
||||
$releaseExtension.StepBanner
|
||||
@ -81,7 +82,8 @@ else {
|
||||
}
|
||||
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot
|
||||
if (-not $pluginSucceeded) {
|
||||
# Exact $true only: polluted arrays (CLI stdout + $false) are truthy under -not.
|
||||
if ($pluginSucceeded -ne $true) {
|
||||
$releaseHadPluginFailures = $true
|
||||
break
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"title": "Release Package Script Settings",
|
||||
"description": "maksit-webui: npm workspace publish (@maks-it.com/webui). Semver from NpmReleaseVersion (src/package.json).",
|
||||
"repoUtilsSecretsShared": "RepoUtilsSecretsShared",
|
||||
"repoUtilsSecrets": "RepoUtilsSecrets",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "NpmReleaseVersion",
|
||||
@ -46,7 +48,7 @@
|
||||
"name": "GitHub",
|
||||
"stageLabel": "release",
|
||||
"enabled": true,
|
||||
"githubSecret": "GitHub",
|
||||
"githubSecret": "GitClone",
|
||||
"repository": "https://github.com/MAKS-IT-COM/maksit-webui",
|
||||
"releaseNotesFile": "..\\..\\..\\CHANGELOG.md",
|
||||
"releaseTitlePattern": "Release {version}"
|
||||
|
||||
@ -32,7 +32,8 @@ $testHadPluginFailures = $false
|
||||
|
||||
foreach ($plugin in $configuredPlugins) {
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -EngineDirectory $scriptDir
|
||||
if (-not $pluginSucceeded) {
|
||||
# Exact $true only: polluted arrays (CLI stdout + $false) are truthy under -not.
|
||||
if ($pluginSucceeded -ne $true) {
|
||||
$testHadPluginFailures = $true
|
||||
break
|
||||
}
|
||||
|
||||
@ -6,16 +6,65 @@
|
||||
Keep a Changelog header parsing and section extraction.
|
||||
|
||||
.DESCRIPTION
|
||||
Supports only the standard Keep a Changelog version line:
|
||||
Supports Keep a Changelog version lines and shared SemVer checks, including prerelease:
|
||||
## [1.0.0] - 2026-05-24
|
||||
## [0.1.0-alpha.1] - 2026-08-21
|
||||
## [0.1.0-beta.1] - 2026-08-21
|
||||
## [0.1.0-rc.1] - 2026-08-21
|
||||
#>
|
||||
|
||||
function Get-ChangelogSemverPattern {
|
||||
return '\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?'
|
||||
}
|
||||
|
||||
function Test-ReleaseSemver {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
return [bool]($Version -match ('^' + (Get-ChangelogSemverPattern) + '$'))
|
||||
}
|
||||
|
||||
function Test-ReleaseSemverPrerelease {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
return (Test-ReleaseSemver -Version $Version) -and ($Version -match '-')
|
||||
}
|
||||
|
||||
function Get-ReleaseSemverPrereleaseLabel {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
if (-not (Test-ReleaseSemverPrerelease -Version $Version)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($Version -match '^\d+\.\d+\.\d+-([A-Za-z][0-9A-Za-z]*)') {
|
||||
return $Matches[1].ToLowerInvariant()
|
||||
}
|
||||
|
||||
return 'next'
|
||||
}
|
||||
|
||||
function Get-ChangelogVersionHeaderPattern {
|
||||
return '(?m)^##\s+\[(\d+\.\d+\.\d+)\]\s*-\s*\d{4}-\d{2}-\d{2}\s*$'
|
||||
return '(?m)^##\s+\[(' + (Get-ChangelogSemverPattern) + ')\]\s*-\s*\d{4}-\d{2}-\d{2}\s*$'
|
||||
}
|
||||
|
||||
function Get-ChangelogNextVersionHeaderPattern {
|
||||
return '(?m)^##\s+\[\d+\.\d+\.\d+\]\s*-\s*\d{4}-\d{2}-\d{2}\s*$'
|
||||
return '(?m)^##\s+\[' + (Get-ChangelogSemverPattern) + '\]\s*-\s*\d{4}-\d{2}-\d{2}\s*$'
|
||||
}
|
||||
|
||||
function Get-LatestChangelogVersion {
|
||||
@ -53,4 +102,4 @@ function Get-ChangelogReleaseNotesSection {
|
||||
return $match.Value.Trim()
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Get-ChangelogVersionHeaderPattern, Get-ChangelogNextVersionHeaderPattern, Get-LatestChangelogVersion, Get-ChangelogReleaseNotesSection
|
||||
Export-ModuleMember -Function Get-ChangelogSemverPattern, Test-ReleaseSemver, Test-ReleaseSemverPrerelease, Get-ReleaseSemverPrereleaseLabel, Get-ChangelogVersionHeaderPattern, Get-ChangelogNextVersionHeaderPattern, Get-LatestChangelogVersion, Get-ChangelogReleaseNotesSection
|
||||
|
||||
@ -321,6 +321,37 @@ function Get-EngineState {
|
||||
return $null
|
||||
}
|
||||
|
||||
function Add-ReleaseAssetPaths {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[psobject]$Context,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyCollection()]
|
||||
[string[]]$Path
|
||||
)
|
||||
|
||||
$list = [System.Collections.Generic.List[string]]::new()
|
||||
$existing = Get-EngineFact -Context $Context -Namespace 'release' -Name 'assetPaths' -Default @() -LegacyProperty @('releaseAssetPaths')
|
||||
foreach ($item in @($existing)) {
|
||||
$value = [string]$item
|
||||
if (-not [string]::IsNullOrWhiteSpace($value) -and -not $list.Contains($value)) {
|
||||
$list.Add($value)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($item in @($Path)) {
|
||||
$value = [string]$item
|
||||
if ([string]::IsNullOrWhiteSpace($value) -or $list.Contains($value)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$list.Add($value)
|
||||
}
|
||||
|
||||
Set-EngineFact -Context $Context -Namespace 'release' -Name 'assetPaths' -Value @($list) -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function `
|
||||
Resolve-RelativePaths, `
|
||||
Initialize-EngineFactsBag, `
|
||||
@ -329,4 +360,5 @@ Export-ModuleMember -Function `
|
||||
Test-EngineFact, `
|
||||
Set-EngineState, `
|
||||
Add-EnginePublishCompletion, `
|
||||
Add-ReleaseAssetPaths, `
|
||||
Get-EngineState
|
||||
|
||||
@ -15,6 +15,7 @@ function Import-EngineModules {
|
||||
(Join-Path $modulesDir 'ScriptConfig.psm1'),
|
||||
(Join-Path $modulesDir 'Logging.psm1'),
|
||||
(Join-Path $engineModuleDir 'PluginSupport.psm1'),
|
||||
(Join-Path $engineModuleDir 'VaultSupport.psm1'),
|
||||
(Join-Path $engineModuleDir 'EngineContext.psm1')
|
||||
)
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ function Test-IsEngineRuntimeModuleName {
|
||||
'TestRunner',
|
||||
'EngineContext',
|
||||
'PluginSupport',
|
||||
'VaultSupport',
|
||||
'ReleaseSupport',
|
||||
'TestSupport'
|
||||
),
|
||||
@ -53,7 +54,7 @@ function Get-PluginDependencyGroupDirectories {
|
||||
}
|
||||
|
||||
# Prefer Shared (helpers), then stock host groups; any other plugins/{Group}/ is discovered.
|
||||
$preferred = @('Shared', 'Platform', 'DotNet', 'Npm')
|
||||
$preferred = @('Shared', 'Platform', 'DotNet', 'Npm', 'Desktop')
|
||||
$dirs = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($name in $preferred) {
|
||||
$path = Join-Path $PluginsRoot $name
|
||||
@ -293,129 +294,586 @@ function Test-PluginMutatesRemote {
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-SecretEnvironmentValue {
|
||||
function Get-RepoUtilsEnvironmentVariable {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reads a secret value from an environment variable by logical name.
|
||||
Reads a named environment variable from Process, then Windows User, then Machine.
|
||||
|
||||
.DESCRIPTION
|
||||
Plugins never store secret material in scriptSettings.json. Settings hold a
|
||||
logical name (e.g. "GitHub", "NuGet"); the process environment variable with
|
||||
that same name must be set before the engine runs.
|
||||
|
||||
.PARAMETER Name
|
||||
Logical secret name — also the environment variable name to read.
|
||||
|
||||
.OUTPUTS
|
||||
System.String. The environment variable value, or $null when unset.
|
||||
|
||||
.EXAMPLE
|
||||
$token = Get-SecretEnvironmentValue -Name 'GitHub'
|
||||
Process wins (CICD sandbox inject, `$env:Name`, explicit session values). When the
|
||||
current process was started before a User-level pack was set, User/Machine still
|
||||
apply so laptop releases do not require a new shell. An explicit process value —
|
||||
including empty JSON `{}` — shadows User/Machine.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
return [Environment]::GetEnvironmentVariable($Name)
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($Name)) {
|
||||
throw "Environment variable name is required."
|
||||
}
|
||||
|
||||
function Resolve-PluginSecretName {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Resolves a logical secret name from a plugin's scriptSettings entry.
|
||||
$processValue = [Environment]::GetEnvironmentVariable($Name, 'Process')
|
||||
# Empty string is what SetEnvironmentVariable($null, Process) leaves behind;
|
||||
# treat it as unset so Windows User packs still apply. Explicit '{}' shadows User.
|
||||
if (-not [string]::IsNullOrWhiteSpace($processValue)) {
|
||||
return $processValue
|
||||
}
|
||||
|
||||
.DESCRIPTION
|
||||
Reads a string property such as githubSecret / nugetSecret / npmSecret /
|
||||
containerRegistrySecret from the plugin settings object. Returns $null when
|
||||
the property is missing or blank.
|
||||
foreach ($target in @('User', 'Machine')) {
|
||||
try {
|
||||
$value = [Environment]::GetEnvironmentVariable($Name, $target)
|
||||
}
|
||||
catch {
|
||||
continue
|
||||
}
|
||||
|
||||
.PARAMETER PluginSettings
|
||||
Plugin settings object from scriptSettings.json (the enabled plugin entry).
|
||||
|
||||
.PARAMETER PropertyName
|
||||
Settings property that holds the logical secret name (e.g. 'githubSecret').
|
||||
|
||||
.OUTPUTS
|
||||
System.String. Trimmed logical secret name, or $null.
|
||||
|
||||
.EXAMPLE
|
||||
$name = Resolve-PluginSecretName -PluginSettings $plugin -PropertyName 'nugetSecret'
|
||||
$key = Get-SecretEnvironmentValue -Name $name
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$PluginSettings,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PropertyName
|
||||
)
|
||||
|
||||
if ($PluginSettings.PSObject.Properties.Name -contains $PropertyName) {
|
||||
$value = [string]$PluginSettings.$PropertyName
|
||||
if (-not [string]::IsNullOrWhiteSpace($value)) {
|
||||
return $value.Trim()
|
||||
return $value
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-RepoUtilsSecretsEnvNames {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reads declared pack environment variable names from scriptSettings / engine context.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
$sharedName = $null
|
||||
$packName = $null
|
||||
if ($null -ne $Settings) {
|
||||
if ($Settings.PSObject.Properties.Name -contains 'repoUtilsSecretsShared') {
|
||||
$sharedName = [string]$Settings.repoUtilsSecretsShared
|
||||
}
|
||||
|
||||
if ($Settings.PSObject.Properties.Name -contains 'repoUtilsSecrets') {
|
||||
$packName = [string]$Settings.repoUtilsSecrets
|
||||
}
|
||||
}
|
||||
|
||||
$sharedName = if ($null -eq $sharedName) { '' } else { $sharedName.Trim() }
|
||||
$packName = if ($null -eq $packName) { '' } else { $packName.Trim() }
|
||||
if ([string]::IsNullOrWhiteSpace($sharedName) -or [string]::IsNullOrWhiteSpace($packName)) {
|
||||
throw "scriptSettings.json must declare repoUtilsSecretsShared and repoUtilsSecrets (environment variable names, e.g. RepoUtilsSecretsShared / RepoUtilsSecrets)."
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
SharedEnv = $sharedName
|
||||
PackEnv = $packName
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertFrom-RepoUtilsSecretsPackJson {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Parses a RepoUtilsSecrets JSON object. Empty input is {}. Bare non-JSON throws.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Raw,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourceName
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Raw)) {
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
|
||||
$trimmed = $Raw.Trim()
|
||||
if (-not $trimmed.StartsWith('{')) {
|
||||
throw "${SourceName} must be a JSON object (RepoUtilsSecrets pack), not a bare string."
|
||||
}
|
||||
|
||||
try {
|
||||
$parsed = $trimmed | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
throw "${SourceName} is not valid JSON: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($null -eq $parsed -or $parsed -is [System.Collections.IEnumerable]) {
|
||||
throw "${SourceName} JSON must be an object."
|
||||
}
|
||||
|
||||
return $parsed
|
||||
}
|
||||
|
||||
function ConvertTo-OrdinalPropertyMap {
|
||||
param($Object)
|
||||
|
||||
$map = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal)
|
||||
if ($null -eq $Object) {
|
||||
return $map
|
||||
}
|
||||
|
||||
if ($Object -is [System.Collections.IDictionary]) {
|
||||
foreach ($key in @($Object.Keys)) {
|
||||
$name = [string]$key
|
||||
if ([string]::IsNullOrWhiteSpace($name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$map[$name] = $Object[$key]
|
||||
}
|
||||
|
||||
return $map
|
||||
}
|
||||
|
||||
foreach ($property in $Object.PSObject.Properties) {
|
||||
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||
continue
|
||||
}
|
||||
|
||||
$map[[string]$property.Name] = $property.Value
|
||||
}
|
||||
|
||||
return $map
|
||||
}
|
||||
|
||||
function ConvertFrom-OrdinalPropertyMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.Generic.Dictionary[string, object]]$Map
|
||||
)
|
||||
|
||||
$properties = [ordered]@{}
|
||||
foreach ($key in $Map.Keys) {
|
||||
$properties[$key] = $Map[$key]
|
||||
}
|
||||
|
||||
return [pscustomobject]$properties
|
||||
}
|
||||
|
||||
function Merge-RepoUtilsSecretsPackObjects {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Appsettings-style merge: org-pack first, slug overlay. Nested merge for any object-valued slot (typically ContainerRegistry).
|
||||
#>
|
||||
param(
|
||||
$Base,
|
||||
$Overlay
|
||||
)
|
||||
|
||||
$result = ConvertTo-OrdinalPropertyMap -Object $Base
|
||||
$overlayMap = ConvertTo-OrdinalPropertyMap -Object $Overlay
|
||||
foreach ($key in @($overlayMap.Keys)) {
|
||||
if (-not (Test-ContainerRegistryCatalogKey -Key $key)) {
|
||||
throw "RepoUtilsSecrets pack key '$key' must be PascalCase (e.g. GitClone, ContainerRegistry)."
|
||||
}
|
||||
|
||||
$overlayValue = $overlayMap[$key]
|
||||
$overlayIsObject = ($null -ne $overlayValue) -and -not ($overlayValue -is [string]) -and -not ($overlayValue -is [ValueType]) -and -not ($overlayValue -is [System.Collections.IEnumerable])
|
||||
if ($overlayIsObject) {
|
||||
$baseCatalog = $null
|
||||
if ($result.ContainsKey($key)) {
|
||||
$baseCatalog = $result[$key]
|
||||
}
|
||||
|
||||
$mergedCatalog = ConvertTo-OrdinalPropertyMap -Object $baseCatalog
|
||||
$overlayCatalogMap = ConvertTo-OrdinalPropertyMap -Object $overlayValue
|
||||
foreach ($catalogKey in @($overlayCatalogMap.Keys)) {
|
||||
if (-not (Test-ContainerRegistryCatalogKey -Key $catalogKey)) {
|
||||
throw "Catalog key '$catalogKey' in pack slot '$key' must be PascalCase (e.g. Harbor, InCluster)."
|
||||
}
|
||||
|
||||
$mergedCatalog[$catalogKey] = $overlayCatalogMap[$catalogKey]
|
||||
}
|
||||
|
||||
$result[$key] = ConvertFrom-OrdinalPropertyMap -Map $mergedCatalog
|
||||
continue
|
||||
}
|
||||
|
||||
$result[$key] = $overlayValue
|
||||
}
|
||||
|
||||
foreach ($key in @($result.Keys)) {
|
||||
if (-not (Test-ContainerRegistryCatalogKey -Key $key)) {
|
||||
throw "RepoUtilsSecrets pack key '$key' must be PascalCase (e.g. GitClone, ContainerRegistry)."
|
||||
}
|
||||
}
|
||||
|
||||
return ConvertFrom-OrdinalPropertyMap -Map $result
|
||||
}
|
||||
|
||||
function Get-MergedRepoUtilsSecretsPack {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Merges $env:RepoUtilsSecretsShared then $env:RepoUtilsSecrets (names from settings).
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
$names = Get-RepoUtilsSecretsEnvNames -Settings $Settings
|
||||
$sharedRaw = Get-RepoUtilsEnvironmentVariable -Name $names.SharedEnv
|
||||
$packRaw = Get-RepoUtilsEnvironmentVariable -Name $names.PackEnv
|
||||
$sharedObject = ConvertFrom-RepoUtilsSecretsPackJson -Raw $sharedRaw -SourceName $names.SharedEnv
|
||||
$packObject = ConvertFrom-RepoUtilsSecretsPackJson -Raw $packRaw -SourceName $names.PackEnv
|
||||
return Merge-RepoUtilsSecretsPackObjects -Base $sharedObject -Overlay $packObject
|
||||
}
|
||||
|
||||
function Get-RepoUtilsSecretSlot {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reads a scalar pack slot (GitClone, NuGet, Npm, CosignKey, …) after merge.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
$Settings,
|
||||
|
||||
[switch]$AllowMissing
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Name) -or -not (Test-ContainerRegistryCatalogKey -Key $Name)) {
|
||||
throw "RepoUtilsSecrets slot '$Name' must be PascalCase (e.g. GitClone, NuGet)."
|
||||
}
|
||||
|
||||
$merged = Get-MergedRepoUtilsSecretsPack -Settings $Settings
|
||||
$match = $null
|
||||
foreach ($property in $merged.PSObject.Properties) {
|
||||
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ([string]::Equals([string]$property.Name, $Name, [System.StringComparison]::Ordinal)) {
|
||||
$match = $property
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $match) {
|
||||
if ($AllowMissing) {
|
||||
return $null
|
||||
}
|
||||
|
||||
throw "RepoUtilsSecrets slot '$Name' is missing after merging org-pack and slug-pack."
|
||||
}
|
||||
|
||||
$value = $match.Value
|
||||
if ($value -is [string] -or $null -eq $value -or $value -is [ValueType]) {
|
||||
$text = if ($null -eq $value) { '' } else { [string]$value }
|
||||
if ([string]::IsNullOrWhiteSpace($text) -and -not $AllowMissing) {
|
||||
throw "RepoUtilsSecrets slot '$Name' is empty."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($text)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return $text
|
||||
}
|
||||
|
||||
throw "RepoUtilsSecrets slot '$Name' must be a string (nested maps are only allowed for ContainerRegistry)."
|
||||
}
|
||||
|
||||
function Copy-RepoUtilsSecretsEnvNamesToContext {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Context,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
if ($null -eq $Settings) {
|
||||
return $Context
|
||||
}
|
||||
|
||||
foreach ($name in @('repoUtilsSecretsShared', 'repoUtilsSecrets', 'vaultRepoUtilsApplication')) {
|
||||
if ($Settings.PSObject.Properties.Name -contains $name -and -not [string]::IsNullOrWhiteSpace([string]$Settings.$name)) {
|
||||
$Context | Add-Member -NotePropertyName $name -NotePropertyValue ([string]$Settings.$name).Trim() -Force
|
||||
}
|
||||
}
|
||||
|
||||
return $Context
|
||||
}
|
||||
|
||||
function Test-ContainerRegistryCatalogKey {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
True when Key is PascalCase (Harbor, InCluster), matching env-slot names.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Key
|
||||
)
|
||||
|
||||
return $Key -cmatch '^[A-Z][A-Za-z0-9]*$'
|
||||
}
|
||||
|
||||
function Resolve-PluginSecretName {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reads a plugin setting that names a RepoUtilsSecrets pack subkey.
|
||||
|
||||
.DESCRIPTION
|
||||
Settings such as githubSecret / nugetSecret / npmSecret / containerRegistrySecret /
|
||||
cosignKeySecret hold the PascalCase pack slot to read (e.g. GitClone, NuGet).
|
||||
They are not environment variable names.
|
||||
|
||||
.PARAMETER PluginSettings
|
||||
Plugin settings object from scriptSettings.json.
|
||||
|
||||
.PARAMETER PropertyName
|
||||
Settings property that holds the pack subkey name (e.g. 'githubSecret').
|
||||
|
||||
.PARAMETER PluginDisplayName
|
||||
Plugin name used in required/validation errors.
|
||||
|
||||
.PARAMETER Required
|
||||
Throw when the property is missing or blank.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$PluginSettings,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PropertyName,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$PluginDisplayName,
|
||||
|
||||
[switch]$Required
|
||||
)
|
||||
|
||||
$display = if ([string]::IsNullOrWhiteSpace($PluginDisplayName)) { 'Plugin' } else { $PluginDisplayName }
|
||||
$value = $null
|
||||
if ($null -ne $PluginSettings -and $PluginSettings.PSObject.Properties.Name -contains $PropertyName) {
|
||||
$raw = [string]$PluginSettings.$PropertyName
|
||||
if (-not [string]::IsNullOrWhiteSpace($raw)) {
|
||||
$value = $raw.Trim()
|
||||
}
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
if ($Required) {
|
||||
throw "$display requires '$PropertyName' (PascalCase pack subkey, e.g. GitClone, NuGet, ContainerRegistry)."
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not (Test-ContainerRegistryCatalogKey -Key $value)) {
|
||||
throw "$display '$PropertyName' '$value' must be PascalCase (e.g. GitClone, NuGet, ContainerRegistry)."
|
||||
}
|
||||
|
||||
return $value
|
||||
}
|
||||
|
||||
function Assert-RetiredContainerRegistrySettingsAbsent {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Throws when obsolete per-registry secret settings are present.
|
||||
#>
|
||||
param(
|
||||
$Object,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Context
|
||||
)
|
||||
|
||||
if ($null -eq $Object) {
|
||||
return
|
||||
}
|
||||
|
||||
$retired = @(
|
||||
'imagesCredentialsSecret',
|
||||
'additionalImageRegistries',
|
||||
'additionalImageRegistryUrls',
|
||||
'additionalImagesCredentialsSecret',
|
||||
'helmOciCredentialsSecret'
|
||||
)
|
||||
|
||||
foreach ($name in $retired) {
|
||||
$present = $false
|
||||
if ($Object -is [System.Collections.IDictionary]) {
|
||||
$present = $Object.Contains($name)
|
||||
}
|
||||
elseif ($Object.PSObject.Properties.Name -contains $name) {
|
||||
$present = $true
|
||||
}
|
||||
|
||||
if ($present) {
|
||||
throw "${Context}: '$name' is not supported. Use the ContainerRegistry JSON catalog with PascalCase containerRegistryKey / helmRegistryKey."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertFrom-RegistryCredentialPayload {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Payload,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ContextName
|
||||
)
|
||||
|
||||
try {
|
||||
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Payload.Trim()))
|
||||
}
|
||||
catch {
|
||||
throw "Failed to decode '$ContextName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$parts = $decoded -split ':', 2
|
||||
if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) {
|
||||
throw "Decoded '$ContextName' must be in the form 'username:password'."
|
||||
}
|
||||
|
||||
return @{ User = $parts[0]; Password = $parts[1] }
|
||||
}
|
||||
|
||||
function Get-ContainerRegistryCatalogObject {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates a PascalCase JSON map of Base64(username:password) values.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Catalog,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$SourceName = 'ContainerRegistry'
|
||||
)
|
||||
|
||||
if ($Catalog -is [string]) {
|
||||
$raw = [string]$Catalog
|
||||
$trimmed = $raw.Trim()
|
||||
if (-not $trimmed.StartsWith('{')) {
|
||||
throw "$SourceName must be a JSON catalog object { `"Harbor`": `"<Base64>`", `"InCluster`": `"<Base64>`" }."
|
||||
}
|
||||
|
||||
try {
|
||||
$Catalog = $trimmed | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
throw "$SourceName is not valid JSON: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $Catalog -or $Catalog -is [string] -or $Catalog -is [ValueType] -or $Catalog -is [System.Collections.IEnumerable]) {
|
||||
throw "$SourceName JSON catalog must be an object of PascalCase keys to Base64(username:password)."
|
||||
}
|
||||
|
||||
$keyCount = 0
|
||||
foreach ($property in $Catalog.PSObject.Properties) {
|
||||
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||
continue
|
||||
}
|
||||
|
||||
$keyCount++
|
||||
$keyName = [string]$property.Name
|
||||
if (-not (Test-ContainerRegistryCatalogKey -Key $keyName)) {
|
||||
throw "Catalog key '$keyName' in '$SourceName' must be PascalCase (e.g. Harbor, InCluster)."
|
||||
}
|
||||
}
|
||||
|
||||
if ($keyCount -eq 0) {
|
||||
throw "$SourceName JSON catalog has no PascalCase keys."
|
||||
}
|
||||
|
||||
return $Catalog
|
||||
}
|
||||
|
||||
function Get-RegistryCredentialsFromRuntime {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Loads container-registry username/password from a logical secret name.
|
||||
Loads container-registry username/password from the merged RepoUtilsSecrets pack.
|
||||
|
||||
.DESCRIPTION
|
||||
Looks up the environment variable named by SecretName. The value must be
|
||||
Base64(UTF8('username:password')). Used by registry login and image-pull
|
||||
secret creation — never pass the password itself as a parameter.
|
||||
Reads a nested catalog slot (named by -Slot, typically ContainerRegistry) after
|
||||
org-pack + slug-pack merge. -Key (PascalCase, ordinal match) selects an entry.
|
||||
|
||||
.PARAMETER SecretName
|
||||
Logical secret name (environment variable name), not a password or token.
|
||||
.PARAMETER Key
|
||||
PascalCase catalog key (e.g. Harbor). Required.
|
||||
|
||||
.PARAMETER Slot
|
||||
PascalCase pack subkey that holds the catalog object (from containerRegistrySecret).
|
||||
|
||||
.PARAMETER SharedSettings
|
||||
Optional engine shared context (reserved for callers that thread context).
|
||||
Engine shared context (must declare repoUtilsSecretsShared / repoUtilsSecrets).
|
||||
|
||||
.OUTPUTS
|
||||
Hashtable with User and Password keys (decoded credential material).
|
||||
|
||||
.EXAMPLE
|
||||
$creds = Get-RegistryCredentialsFromRuntime -SecretName 'ContainerRegistry'
|
||||
# $creds.User / $creds.Password
|
||||
#>
|
||||
# SecretName is a logical env-var name from scriptSettings, not a password value.
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
||||
'PSAvoidUsingPlainTextForPassword',
|
||||
'SecretName',
|
||||
Justification = 'Logical secret name for env lookup (Base64 username:password); not a credential value.'
|
||||
)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SecretName,
|
||||
[string]$Key,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Slot,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[psobject]$SharedSettings
|
||||
)
|
||||
|
||||
$raw = Get-SecretEnvironmentValue -Name $SecretName
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||
throw "Environment variable '$SecretName' is not set."
|
||||
if ([string]::IsNullOrWhiteSpace($Slot) -or -not (Test-ContainerRegistryCatalogKey -Key $Slot)) {
|
||||
throw "containerRegistrySecret '$Slot' must be PascalCase (e.g. ContainerRegistry)."
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($raw))
|
||||
}
|
||||
catch {
|
||||
throw "Failed to decode '$SecretName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
|
||||
if ([string]::IsNullOrWhiteSpace($Key)) {
|
||||
throw "Pass -Key (PascalCase, e.g. Harbor) to select an entry in pack slot '$Slot'."
|
||||
}
|
||||
|
||||
$parts = $decoded -split ':', 2
|
||||
if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) {
|
||||
throw "Decoded '$SecretName' must be in the form 'username:password'."
|
||||
if (-not (Test-ContainerRegistryCatalogKey -Key $Key)) {
|
||||
throw "containerRegistryKey '$Key' must be PascalCase (e.g. Harbor, InCluster)."
|
||||
}
|
||||
|
||||
return @{ User = $parts[0]; Password = $parts[1] }
|
||||
$merged = Get-MergedRepoUtilsSecretsPack -Settings $SharedSettings
|
||||
$catalogProperty = $null
|
||||
foreach ($property in $merged.PSObject.Properties) {
|
||||
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ([string]::Equals([string]$property.Name, $Slot, [System.StringComparison]::Ordinal)) {
|
||||
$catalogProperty = $property
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $catalogProperty -or $null -eq $catalogProperty.Value) {
|
||||
throw "RepoUtilsSecrets slot '$Slot' is missing after merging org-pack and slug-pack."
|
||||
}
|
||||
|
||||
$catalog = Get-ContainerRegistryCatalogObject -Catalog $catalogProperty.Value -SourceName $Slot
|
||||
|
||||
$match = $null
|
||||
foreach ($property in $catalog.PSObject.Properties) {
|
||||
if ($property.MemberType -notin @('NoteProperty', 'Property')) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ([string]::Equals([string]$property.Name, $Key, [System.StringComparison]::Ordinal)) {
|
||||
$match = $property
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $match) {
|
||||
throw "Catalog key '$Key' was not found in '$Slot' (ordinal PascalCase match)."
|
||||
}
|
||||
|
||||
$payload = [string]$match.Value
|
||||
if ([string]::IsNullOrWhiteSpace($payload)) {
|
||||
throw "Catalog key '$Key' in '$Slot' is empty."
|
||||
}
|
||||
|
||||
return ConvertFrom-RegistryCredentialPayload -Payload $payload -ContextName "$Slot.$Key"
|
||||
}
|
||||
|
||||
function Resolve-EngineDirectoryFromSharedSettings {
|
||||
@ -596,7 +1054,7 @@ function Resolve-PluginModulePath {
|
||||
$candidatePaths = [System.Collections.Generic.List[string]]::new()
|
||||
$candidatePaths.Add((Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName))
|
||||
|
||||
$preferredGroups = @('Platform', 'DotNet', 'Npm')
|
||||
$preferredGroups = @('Platform', 'DotNet', 'Npm', 'Desktop')
|
||||
$candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $preferredGroups[0]) $pluginFileName))
|
||||
|
||||
if (Get-Command Get-ExtensionPluginModulePaths -ErrorAction SilentlyContinue) {
|
||||
@ -737,12 +1195,16 @@ function Invoke-ConfiguredPlugin {
|
||||
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
|
||||
Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.name)'..."
|
||||
|
||||
# Sink plugin success-stream output to the host so it cannot pollute this
|
||||
# function's return value. Otherwise `return $false` after CLI stdout becomes
|
||||
# @("helm-line…", $false), which is truthy under `if (-not $result)` and
|
||||
# causes RELEASE COMPLETE / exit 0 after a failed plugin.
|
||||
try {
|
||||
$moduleInfo = Import-Module $pluginModulePath -Force -PassThru -ErrorAction Stop
|
||||
$invokeCommand = Get-Command -Name "Invoke-Plugin" -Module $moduleInfo.Name -ErrorAction Stop
|
||||
$pluginSettings = New-PluginInvocationSettings -Plugin $Plugin -SharedSettings $SharedSettings
|
||||
|
||||
& $invokeCommand -Settings $pluginSettings
|
||||
& $invokeCommand -Settings $pluginSettings | ForEach-Object { Write-Host $_ }
|
||||
Write-Log -Level "OK" -Message " Plugin '$($Plugin.name)' completed."
|
||||
return $true
|
||||
}
|
||||
@ -752,4 +1214,4 @@ function Invoke-ConfiguredPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStageLabel, Get-PluginBranches, Get-PluginMetadataObject, Test-PluginCompatible, Test-PluginMutatesRemote, Resolve-PluginSecretName, Get-SecretEnvironmentValue, Get-RegistryCredentialsFromRuntime, Test-PluginSkipsRemoteMutation, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin
|
||||
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStageLabel, Get-PluginBranches, Get-PluginMetadataObject, Test-PluginCompatible, Test-PluginMutatesRemote, Get-RepoUtilsEnvironmentVariable, Get-RepoUtilsSecretsEnvNames, ConvertFrom-RepoUtilsSecretsPackJson, Merge-RepoUtilsSecretsPackObjects, Get-MergedRepoUtilsSecretsPack, Get-RepoUtilsSecretSlot, Copy-RepoUtilsSecretsEnvNamesToContext, Resolve-PluginSecretName, Test-ContainerRegistryCatalogKey, Assert-RetiredContainerRegistrySettingsAbsent, Get-ContainerRegistryCatalogObject, Get-RegistryCredentialsFromRuntime, Test-PluginSkipsRemoteMutation, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin
|
||||
|
||||
@ -213,6 +213,7 @@ function New-EngineContext {
|
||||
skipPublishPlugins = $false
|
||||
facts = [ordered]@{}
|
||||
}
|
||||
$context = Copy-RepoUtilsSecretsEnvNamesToContext -Context $context -Settings $Settings
|
||||
|
||||
$versionSource = Resolve-EngineContextVersion -Plugins $Plugins -Context $context -ScriptDir $ScriptDir
|
||||
$version = [string](Get-EngineState -Context $context -Name 'version' -Required)
|
||||
|
||||
@ -38,6 +38,7 @@ function New-EngineContext {
|
||||
utilsDir = $SrcDir
|
||||
facts = [ordered]@{}
|
||||
}
|
||||
$context = Copy-RepoUtilsSecretsEnvNamesToContext -Context $context -Settings $Settings
|
||||
|
||||
$expandContext = Get-Command Expand-ExtensionEngineContext -ErrorAction SilentlyContinue
|
||||
if ($expandContext) {
|
||||
|
||||
373
utils/modules/Engine/VaultSupport.psm1
Normal file
373
utils/modules/Engine/VaultSupport.psm1
Normal file
@ -0,0 +1,373 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
function Test-RepoUtilsUseVaultEnabled {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
if ($null -eq $Settings -or -not ($Settings.PSObject.Properties.Name -contains 'useVault')) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$value = $Settings.useVault
|
||||
if ($value -is [bool]) {
|
||||
return $value
|
||||
}
|
||||
|
||||
return [string]$value -eq 'true'
|
||||
}
|
||||
|
||||
function ConvertFrom-VaultConnectionSecret {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Raw
|
||||
)
|
||||
|
||||
$trimmed = $Raw.Trim()
|
||||
$separator = $trimmed.IndexOf('|')
|
||||
if ($separator -lt 1 -or $separator -ge ($trimmed.Length - 1)) {
|
||||
throw "Vault connection must be 'baseAddress|apiKey' (pipe separator). Example: http://172.16.0.14|your-key"
|
||||
}
|
||||
|
||||
$baseAddress = $trimmed.Substring(0, $separator).Trim().TrimEnd('/')
|
||||
$apiKey = $trimmed.Substring($separator + 1).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($baseAddress) -or [string]::IsNullOrWhiteSpace($apiKey)) {
|
||||
throw "Vault connection is missing base address or API key."
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
BaseAddress = $baseAddress
|
||||
ApiKey = $apiKey
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RepoUtilsVaultScope {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
$organization = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_ORGANIZATION')
|
||||
if ([string]::IsNullOrWhiteSpace($organization) -and ($Settings.PSObject.Properties.Name -contains 'vaultOrganization')) {
|
||||
$organization = [string]$Settings.vaultOrganization
|
||||
}
|
||||
|
||||
$application = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_APPLICATION')
|
||||
if ([string]::IsNullOrWhiteSpace($application) -and ($Settings.PSObject.Properties.Name -contains 'vaultApplication')) {
|
||||
$application = [string]$Settings.vaultApplication
|
||||
}
|
||||
|
||||
$organization = if ($null -eq $organization) { '' } else { $organization.Trim() }
|
||||
$application = if ($null -eq $application) { '' } else { $application.Trim() }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($organization) -or [string]::IsNullOrWhiteSpace($application)) {
|
||||
throw "useVault is true but vault organization/application are not set. Set vaultOrganization/vaultApplication in scriptSettings.json or MAKSIT_VAULT_ORGANIZATION / MAKSIT_VAULT_APPLICATION."
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Organization = $organization
|
||||
Application = $application
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RepoUtilsVaultConnectionSecretName {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
if (($Settings.PSObject.Properties.Name -contains 'vaultConnectionSecret') -and
|
||||
-not [string]::IsNullOrWhiteSpace([string]$Settings.vaultConnectionSecret)) {
|
||||
return ([string]$Settings.vaultConnectionSecret).Trim()
|
||||
}
|
||||
|
||||
return 'MAKSIT_VAULT'
|
||||
}
|
||||
|
||||
function Get-RepoUtilsVaultOrgPackApplicationName {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
$application = $null
|
||||
if ($Settings.PSObject.Properties.Name -contains 'vaultRepoUtilsApplication') {
|
||||
$application = [string]$Settings.vaultRepoUtilsApplication
|
||||
}
|
||||
|
||||
$application = if ($null -eq $application) { '' } else { $application.Trim() }
|
||||
if ([string]::IsNullOrWhiteSpace($application)) {
|
||||
throw "useVault is true but vaultRepoUtilsApplication is not set in scriptSettings.json (Vault application for the org-pack, e.g. Shared)."
|
||||
}
|
||||
|
||||
return $application
|
||||
}
|
||||
|
||||
function Get-VaultNameFilterExpression {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
$escaped = $Name.Replace('\', '\\').Replace('"', '\"')
|
||||
return "Name == `"$escaped`""
|
||||
}
|
||||
|
||||
function Import-RepoUtilsVaultClientModule {
|
||||
$modulePath = [Environment]::GetEnvironmentVariable('MAKSIT_VAULT_MODULE_PATH')
|
||||
if (-not [string]::IsNullOrWhiteSpace($modulePath)) {
|
||||
Import-Module $modulePath -Force -ErrorAction Stop
|
||||
return
|
||||
}
|
||||
|
||||
if (Get-Command Connect-Vault -ErrorAction SilentlyContinue) {
|
||||
return
|
||||
}
|
||||
|
||||
Import-Module 'MaksIT.Vault.Client.PowerShell' -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Find-RepoUtilsVaultSecretMatch {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OrganizationName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ApplicationName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SecretName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Connection,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('Cmdlet', 'Rest')]
|
||||
[string]$Mode
|
||||
)
|
||||
|
||||
$orgFilter = Get-VaultNameFilterExpression -Name $OrganizationName
|
||||
$appFilter = Get-VaultNameFilterExpression -Name $ApplicationName
|
||||
$pageNumber = 1
|
||||
|
||||
while ($true) {
|
||||
$items = @()
|
||||
$hasNext = $false
|
||||
|
||||
if ($Mode -eq 'Cmdlet') {
|
||||
$response = Search-VaultSecrets -PageNumber $pageNumber -PageSize 100 `
|
||||
-OrganizationFilters $orgFilter -ApplicationFilters $appFilter
|
||||
if ($null -ne $response -and $null -ne $response.Items) {
|
||||
$items = @($response.Items)
|
||||
}
|
||||
if ($null -ne $response) {
|
||||
$hasNext = [bool]$response.HasNextPage
|
||||
}
|
||||
}
|
||||
else {
|
||||
$body = @{
|
||||
pageNumber = $pageNumber
|
||||
pageSize = 100
|
||||
organizationFilters = $orgFilter
|
||||
applicationFilters = $appFilter
|
||||
} | ConvertTo-Json -Compress
|
||||
$uri = "$($Connection.BaseAddress)/api/vault/secrets"
|
||||
$headers = @{ 'X-API-KEY' = $Connection.ApiKey }
|
||||
$response = Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -ContentType 'application/json' -Body $body
|
||||
if ($null -ne $response.items) {
|
||||
$items = @($response.items)
|
||||
}
|
||||
elseif ($null -ne $response.Items) {
|
||||
$items = @($response.Items)
|
||||
}
|
||||
$hasNext = [bool]($response.hasNextPage)
|
||||
if (-not $hasNext) {
|
||||
$hasNext = [bool]($response.HasNextPage)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($item in $items) {
|
||||
$itemName = [string]$(if ($item.PSObject.Properties.Name -contains 'Name') { $item.Name } else { $item.name })
|
||||
if ([string]::Equals($itemName, $SecretName, [System.StringComparison]::Ordinal)) {
|
||||
return $item
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $hasNext) {
|
||||
break
|
||||
}
|
||||
|
||||
$pageNumber++
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-RepoUtilsVaultSecretValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Match,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Connection,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('Cmdlet', 'Rest')]
|
||||
[string]$Mode
|
||||
)
|
||||
|
||||
$organizationId = if ($Match.PSObject.Properties.Name -contains 'OrganizationId') { $Match.OrganizationId } else { $Match.organizationId }
|
||||
$applicationId = if ($Match.PSObject.Properties.Name -contains 'ApplicationId') { $Match.ApplicationId } else { $Match.applicationId }
|
||||
$secretId = if ($Match.PSObject.Properties.Name -contains 'Id') { $Match.Id } else { $Match.id }
|
||||
|
||||
if ($Mode -eq 'Cmdlet') {
|
||||
$version = Get-VaultSecret -OrganizationId $organizationId -ApplicationId $applicationId -SecretId $secretId -SecretVersion 'current'
|
||||
if ($null -eq $version -or [string]::IsNullOrWhiteSpace([string]$version.Value)) {
|
||||
return $null
|
||||
}
|
||||
return [string]$version.Value
|
||||
}
|
||||
|
||||
$uri = "$($Connection.BaseAddress)/api/vault/organization/$organizationId/application/$applicationId/secret/$secretId" +
|
||||
'?secretVersion=current'
|
||||
$headers = @{ 'X-API-KEY' = $Connection.ApiKey }
|
||||
$version = Invoke-RestMethod -Method Get -Uri $uri -Headers $headers
|
||||
$value = if ($version.PSObject.Properties.Name -contains 'Value') { $version.Value } else { $version.value }
|
||||
if ([string]::IsNullOrWhiteSpace([string]$value)) {
|
||||
return $null
|
||||
}
|
||||
return [string]$value
|
||||
}
|
||||
|
||||
function Resolve-RepoUtilsVaultSecretValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OrganizationName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ApplicationName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SecretName,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[pscustomobject]$Connection,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Mode
|
||||
)
|
||||
|
||||
$match = Find-RepoUtilsVaultSecretMatch `
|
||||
-OrganizationName $OrganizationName `
|
||||
-ApplicationName $ApplicationName `
|
||||
-SecretName $SecretName `
|
||||
-Connection $Connection `
|
||||
-Mode $Mode
|
||||
if ($null -eq $match) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$value = Get-RepoUtilsVaultSecretValue -Match $match -Connection $Connection -Mode $Mode
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Log -Level 'INFO' -Message "Resolved Vault secret '$SecretName' from $OrganizationName/$ApplicationName"
|
||||
return $value
|
||||
}
|
||||
|
||||
function Initialize-RepoUtilsVaultSecrets {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Plugins
|
||||
)
|
||||
|
||||
if (-not (Test-RepoUtilsUseVaultEnabled -Settings $Settings)) {
|
||||
return
|
||||
}
|
||||
|
||||
$names = Get-RepoUtilsSecretsEnvNames -Settings $Settings
|
||||
$sharedRaw = Get-RepoUtilsEnvironmentVariable -Name $names.SharedEnv
|
||||
$packRaw = Get-RepoUtilsEnvironmentVariable -Name $names.PackEnv
|
||||
$sharedPresent = -not [string]::IsNullOrWhiteSpace($sharedRaw)
|
||||
$packPresent = -not [string]::IsNullOrWhiteSpace($packRaw)
|
||||
if ($sharedPresent -and $packPresent) {
|
||||
[void](ConvertFrom-RepoUtilsSecretsPackJson -Raw $sharedRaw -SourceName $names.SharedEnv)
|
||||
[void](ConvertFrom-RepoUtilsSecretsPackJson -Raw $packRaw -SourceName $names.PackEnv)
|
||||
Write-Log -Level 'INFO' -Message "Vault mode: pack env vars '$($names.SharedEnv)' and '$($names.PackEnv)' already set; skipping Vault fetch."
|
||||
return
|
||||
}
|
||||
|
||||
$connectionName = Get-RepoUtilsVaultConnectionSecretName -Settings $Settings
|
||||
$raw = [Environment]::GetEnvironmentVariable($connectionName)
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||
throw "useVault is true but environment variable '$connectionName' is not set (expected baseAddress|apiKey)."
|
||||
}
|
||||
|
||||
$connection = ConvertFrom-VaultConnectionSecret -Raw $raw
|
||||
$scope = Get-RepoUtilsVaultScope -Settings $Settings
|
||||
$orgPackApplication = Get-RepoUtilsVaultOrgPackApplicationName -Settings $Settings
|
||||
|
||||
Write-Log -Level 'INFO' -Message "Vault mode: loading RepoUtilsSecrets packs for $($scope.Organization)/$orgPackApplication and $($scope.Organization)/$($scope.Application)"
|
||||
|
||||
$mode = 'Rest'
|
||||
try {
|
||||
Import-RepoUtilsVaultClientModule
|
||||
if (Get-Command Connect-Vault -ErrorAction SilentlyContinue) {
|
||||
Connect-Vault -BaseAddress $connection.BaseAddress -ApiKey $connection.ApiKey
|
||||
$mode = 'Cmdlet'
|
||||
Write-Log -Level 'INFO' -Message 'Connected to Vault with MaksIT.Vault.Client.PowerShell'
|
||||
}
|
||||
else {
|
||||
Write-Log -Level 'INFO' -Message 'MaksIT.Vault.Client.PowerShell not found; using Vault HTTP API'
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Log -Level 'INFO' -Message "Vault PowerShell module not loaded ($($_.Exception.Message)); using Vault HTTP API"
|
||||
$mode = 'Rest'
|
||||
}
|
||||
|
||||
if (-not $sharedPresent) {
|
||||
$sharedValue = Resolve-RepoUtilsVaultSecretValue `
|
||||
-OrganizationName $scope.Organization `
|
||||
-ApplicationName $orgPackApplication `
|
||||
-SecretName $names.SharedEnv `
|
||||
-Connection $connection `
|
||||
-Mode $mode
|
||||
if ([string]::IsNullOrWhiteSpace($sharedValue)) {
|
||||
$sharedValue = '{}'
|
||||
Write-Log -Level 'INFO' -Message "Vault secret '$($names.SharedEnv)' was not found for $($scope.Organization)/$orgPackApplication; using empty org-pack."
|
||||
}
|
||||
|
||||
[Environment]::SetEnvironmentVariable($names.SharedEnv, $sharedValue, 'Process')
|
||||
}
|
||||
|
||||
if (-not $packPresent) {
|
||||
$packValue = Resolve-RepoUtilsVaultSecretValue `
|
||||
-OrganizationName $scope.Organization `
|
||||
-ApplicationName $scope.Application `
|
||||
-SecretName $names.PackEnv `
|
||||
-Connection $connection `
|
||||
-Mode $mode
|
||||
if ([string]::IsNullOrWhiteSpace($packValue)) {
|
||||
$packValue = '{}'
|
||||
Write-Log -Level 'INFO' -Message "Vault secret '$($names.PackEnv)' was not found for $($scope.Organization)/$($scope.Application); using empty slug-pack."
|
||||
}
|
||||
|
||||
[Environment]::SetEnvironmentVariable($names.PackEnv, $packValue, 'Process')
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Test-RepoUtilsUseVaultEnabled',
|
||||
'ConvertFrom-VaultConnectionSecret',
|
||||
'Get-RepoUtilsVaultScope',
|
||||
'Get-RepoUtilsVaultConnectionSecretName',
|
||||
'Get-RepoUtilsVaultOrgPackApplicationName',
|
||||
'Initialize-RepoUtilsVaultSecrets'
|
||||
)
|
||||
@ -107,7 +107,7 @@ function Invoke-ExternalCommand {
|
||||
if ($ThrowOnError -and $exitCode -ne 0) {
|
||||
$preview = ($output | ForEach-Object {
|
||||
if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { [string]$_ }
|
||||
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 8) -join ' '
|
||||
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Last 12) -join ' '
|
||||
if ([string]::IsNullOrWhiteSpace($preview)) {
|
||||
throw "External command '$Name' failed with exit code $exitCode."
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
.DESCRIPTION
|
||||
Provides the Invoke-TestsWithCoverage function for running .NET tests
|
||||
with Coverlet code coverage collection and parsing results.
|
||||
with Microsoft.Testing.Platform and coverlet.MTP code coverage.
|
||||
|
||||
.NOTES
|
||||
Author: MaksIT
|
||||
@ -65,6 +65,20 @@ function Write-TestRunnerLogInternal {
|
||||
Write-Host $Message -ForegroundColor Gray
|
||||
}
|
||||
|
||||
function Get-CoberturaCoverageFiles {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ResultsDirectory
|
||||
)
|
||||
|
||||
# coverlet.MTP: [prefix.]coverage.cobertura[.timestamp].xml
|
||||
$files = @(
|
||||
Get-ChildItem -Path $ResultsDirectory -Recurse -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like '*coverage.cobertura*.xml' }
|
||||
)
|
||||
return @($files | Sort-Object FullName -Unique)
|
||||
}
|
||||
|
||||
function Invoke-TestsWithCoverage {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
@ -155,7 +169,7 @@ function Invoke-TestsWithCoverage {
|
||||
New-Item -ItemType Directory -Path $ResultsDir -Force | Out-Null
|
||||
|
||||
if (-not $Silent) {
|
||||
Write-TestRunnerLogInternal -Level "STEP" -Message "Running tests with code coverage..."
|
||||
Write-TestRunnerLogInternal -Level "STEP" -Message "Running tests with code coverage (Microsoft.Testing.Platform / coverlet.MTP)..."
|
||||
foreach ($d in $resolvedProjectDirs) {
|
||||
Write-TestRunnerLogInternal -Level "INFO" -Message "Test Project: $d"
|
||||
}
|
||||
@ -164,11 +178,15 @@ function Invoke-TestsWithCoverage {
|
||||
foreach ($TestProjectDir in $resolvedProjectDirs) {
|
||||
Push-Location $TestProjectDir
|
||||
try {
|
||||
$projectName = [System.IO.Path]::GetFileName($TestProjectDir)
|
||||
$dotnetArgs = @(
|
||||
"test"
|
||||
"--collect:XPlat Code Coverage"
|
||||
"--results-directory", $ResultsDir
|
||||
"--verbosity", $(if ($Silent) { "quiet" } else { "normal" })
|
||||
"--coverlet"
|
||||
"--coverlet-output-format", "cobertura"
|
||||
"--coverlet-file-prefix", $projectName
|
||||
"--coverlet-include", "[MaksIT.*]*"
|
||||
)
|
||||
|
||||
Import-ExternalCommandSupportInternal
|
||||
@ -192,7 +210,7 @@ function Invoke-TestsWithCoverage {
|
||||
}
|
||||
}
|
||||
|
||||
$coverageFiles = @(Get-ChildItem -Path $ResultsDir -Filter "coverage.cobertura.xml" -Recurse | Sort-Object FullName)
|
||||
$coverageFiles = @(Get-CoberturaCoverageFiles -ResultsDirectory $ResultsDir)
|
||||
|
||||
if ($coverageFiles.Count -eq 0) {
|
||||
return [PSCustomObject]@{
|
||||
@ -455,7 +473,7 @@ function Get-DotNetCoverageFromResultsDirectory {
|
||||
[switch]$Silent
|
||||
)
|
||||
|
||||
$coverageFiles = @(Get-ChildItem -Path $ResultsDirectory -Filter 'coverage.cobertura.xml' -Recurse -ErrorAction SilentlyContinue | Sort-Object FullName)
|
||||
$coverageFiles = @(Get-CoberturaCoverageFiles -ResultsDirectory $ResultsDirectory)
|
||||
if ($coverageFiles.Count -eq 0) {
|
||||
return [PSCustomObject]@{
|
||||
Success = $false
|
||||
@ -566,7 +584,7 @@ function Get-CoverageFromResultsDirectory {
|
||||
}
|
||||
}
|
||||
|
||||
$hasDotNet = @(Get-ChildItem -Path $resolvedDirectory -Filter 'coverage.cobertura.xml' -Recurse -ErrorAction SilentlyContinue).Count -gt 0
|
||||
$hasDotNet = @(Get-CoberturaCoverageFiles -ResultsDirectory $resolvedDirectory).Count -gt 0
|
||||
$jestSummary = Join-Path $resolvedDirectory 'coverage-summary.json'
|
||||
$hasNpm = Test-Path -LiteralPath $jestSummary -PathType Leaf
|
||||
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Discovers existing .NET NuGet package artifacts.
|
||||
|
||||
.DESCRIPTION
|
||||
Scans artifactsDir for .nupkg/.snupkg matching the release version and populates shared
|
||||
context (packageFile, symbolsPackageFile, releaseArchiveInputs) for downstream plugins.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||
Import-PluginDependency -ModuleName 'DotNetArtifactSupport' -RequiredCommand 'Resolve-DotNetPackageArtifacts'
|
||||
Import-PluginDependency -ModuleName 'EngineContext' -RequiredCommand 'Set-EngineFact'
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
$version = $sharedSettings.version
|
||||
|
||||
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
|
||||
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir)))
|
||||
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
|
||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||
}
|
||||
else {
|
||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||
throw 'DiscoverDotNetPackageArtifacts requires artifactsDir in plugin settings or artifactsDirectory on shared context.'
|
||||
}
|
||||
|
||||
Write-Log -Level 'STEP' -Message "Discovering NuGet package artifacts in $artifactsDirectory ..."
|
||||
$resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
|
||||
|
||||
Write-Log -Level 'OK' -Message " Package ready: $($resolved.PackageFile.FullName)"
|
||||
if ($resolved.SymbolsPackageFile) {
|
||||
Write-Log -Level 'OK' -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level 'WARN' -Message " Symbols package (.snupkg) not found for version $version."
|
||||
}
|
||||
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $false }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
@ -1,63 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Shared helpers for discovering .NET NuGet package artifacts on disk.
|
||||
#>
|
||||
|
||||
function Resolve-DotNetPackageArtifacts {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ArtifactsDirectory,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
if (-not (Test-Path $ArtifactsDirectory -PathType Container)) {
|
||||
throw "Artifacts directory not found: $ArtifactsDirectory"
|
||||
}
|
||||
|
||||
$packageFile = $null
|
||||
$newestNupkgWrite = [datetime]::MinValue
|
||||
$nupkgCandidates = Get-ChildItem -Path $ArtifactsDirectory -Filter '*.nupkg'
|
||||
foreach ($candidate in $nupkgCandidates) {
|
||||
if (($candidate.Name -like "*$Version*.nupkg") -and ($candidate.Name -notlike '*.symbols.nupkg') -and ($candidate.Name -notlike '*.snupkg')) {
|
||||
if ($candidate.LastWriteTime -gt $newestNupkgWrite) {
|
||||
$newestNupkgWrite = $candidate.LastWriteTime
|
||||
$packageFile = $candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $packageFile) {
|
||||
throw "Could not locate generated NuGet package for version $Version in: $ArtifactsDirectory"
|
||||
}
|
||||
|
||||
$releaseArchiveInputs = @($packageFile.FullName)
|
||||
|
||||
$symbolsPackageFile = $null
|
||||
$newestSnupkgWrite = [datetime]::MinValue
|
||||
$snupkgCandidates = Get-ChildItem -Path $ArtifactsDirectory -Filter '*.snupkg'
|
||||
foreach ($candidate in $snupkgCandidates) {
|
||||
if ($candidate.Name -like "*$Version*.snupkg") {
|
||||
if ($candidate.LastWriteTime -gt $newestSnupkgWrite) {
|
||||
$newestSnupkgWrite = $candidate.LastWriteTime
|
||||
$symbolsPackageFile = $candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($symbolsPackageFile) {
|
||||
$releaseArchiveInputs += $symbolsPackageFile.FullName
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
PackageFile = $packageFile
|
||||
SymbolsPackageFile = $symbolsPackageFile
|
||||
ReleaseArchiveInputs = $releaseArchiveInputs
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Resolve-DotNetPackageArtifacts
|
||||
@ -1,122 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
.NET artifact cleanup plugin — remove NuGet build outputs after release.
|
||||
|
||||
.DESCRIPTION
|
||||
Removes files from the configured artifacts directory using glob patterns.
|
||||
Defaults target NuGet outputs (*.nupkg, *.snupkg). Typically placed at the
|
||||
end of the Release stage after DotNetCreateArchive or publish plugins.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Get-CleanupPatternsInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
$ConfiguredPatterns
|
||||
)
|
||||
|
||||
if ($null -eq $ConfiguredPatterns) {
|
||||
return @('*.nupkg', '*.snupkg')
|
||||
}
|
||||
|
||||
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
|
||||
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
|
||||
return @('*.nupkg', '*.snupkg')
|
||||
}
|
||||
|
||||
return @([string]$ConfiguredPatterns)
|
||||
}
|
||||
|
||||
function Get-ExcludePatternsInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
$ConfiguredPatterns
|
||||
)
|
||||
|
||||
if ($null -eq $ConfiguredPatterns) {
|
||||
return @()
|
||||
}
|
||||
|
||||
if ($ConfiguredPatterns -is [System.Collections.IEnumerable] -and -not ($ConfiguredPatterns -is [string])) {
|
||||
return @($ConfiguredPatterns | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace([string]$ConfiguredPatterns)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
return @([string]$ConfiguredPatterns)
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||
$patterns = Get-CleanupPatternsInternal -ConfiguredPatterns $pluginSettings.includePatterns
|
||||
$excludePatterns = Get-ExcludePatternsInternal -ConfiguredPatterns $pluginSettings.excludePatterns
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||
throw "DotNetCleanupArtifacts plugin requires an artifacts directory in the shared context."
|
||||
}
|
||||
|
||||
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
||||
Write-Log -Level "WARN" -Message " Artifacts directory not found: $artifactsDirectory"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Cleaning generated artifacts..."
|
||||
|
||||
$itemsToRemove = @()
|
||||
foreach ($pattern in $patterns) {
|
||||
$matchedItems = @(
|
||||
Get-ChildItem -Path $artifactsDirectory -Force -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like $pattern }
|
||||
)
|
||||
|
||||
if ($excludePatterns.Count -gt 0) {
|
||||
$matchedItems = @(
|
||||
$matchedItems |
|
||||
Where-Object {
|
||||
$item = $_
|
||||
-not ($excludePatterns | Where-Object { $item.Name -like $_ } | Select-Object -First 1)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$itemsToRemove += @($matchedItems)
|
||||
}
|
||||
|
||||
$itemsToRemove = @($itemsToRemove | Sort-Object FullName -Unique)
|
||||
|
||||
if ($itemsToRemove.Count -eq 0) {
|
||||
Write-Log -Level "INFO" -Message " No artifacts matched cleanup rules."
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($item in $itemsToRemove) {
|
||||
Remove-Item -Path $item.FullName -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Write-Log -Level "OK" -Message " Removed: $($item.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin
|
||||
@ -1,102 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
.NET release archive plugin — zip from NuGet pack/publish artifacts.
|
||||
|
||||
.DESCRIPTION
|
||||
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)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||
$version = $sharedSettings.version
|
||||
$archiveInputs = @()
|
||||
|
||||
$fromFact = Get-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -LegacyProperty @('releaseArchiveInputs')
|
||||
if ($null -ne $fromFact) {
|
||||
$archiveInputs = @($fromFact)
|
||||
}
|
||||
else {
|
||||
$packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile')
|
||||
if ($null -ne $packageFile) {
|
||||
$archiveInputs = @($packageFile.FullName)
|
||||
$symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile')
|
||||
if ($null -ne $symbolsPackageFile) {
|
||||
$archiveInputs += $symbolsPackageFile.FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($archiveInputs.Count -eq 0) {
|
||||
throw "DotNetCreateArchive plugin requires prepared artifacts. Run DotNetPack or DotNetPublish first."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||
throw "DotNetCreateArchive plugin requires an artifacts directory in the shared context."
|
||||
}
|
||||
|
||||
if (-not (Test-Path $artifactsDirectory -PathType Container)) {
|
||||
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
||||
}
|
||||
|
||||
$zipNamePattern = if ($pluginSettings.PSObject.Properties['zipNamePattern'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.zipNamePattern)) {
|
||||
[string]$pluginSettings.zipNamePattern
|
||||
}
|
||||
else {
|
||||
"release-{version}.zip"
|
||||
}
|
||||
|
||||
$zipFileName = $zipNamePattern -replace '\{version\}', $version
|
||||
$zipPath = Join-Path $artifactsDirectory $zipFileName
|
||||
|
||||
if (Test-Path $zipPath) {
|
||||
Remove-Item -Path $zipPath -Force
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Creating release archive..."
|
||||
Compress-Archive -Path $archiveInputs -DestinationPath $zipPath -CompressionLevel Optimal -Force
|
||||
|
||||
if (-not (Test-Path $zipPath -PathType Leaf)) {
|
||||
throw "Failed to create release archive at: $zipPath"
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " Release archive ready: $zipPath"
|
||||
|
||||
$releaseAssetPaths = @($zipPath)
|
||||
$packageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -LegacyProperty @('packageFile')
|
||||
if ($null -ne $packageFile) {
|
||||
$releaseAssetPaths += $packageFile.FullName
|
||||
}
|
||||
$symbolsPackageFile = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -LegacyProperty @('symbolsPackageFile')
|
||||
if ($null -ne $symbolsPackageFile) {
|
||||
$releaseAssetPaths += $symbolsPackageFile.FullName
|
||||
}
|
||||
|
||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archivePath' -Value $zipPath -Overwrite Replace -LegacyProperty 'releaseArchivePath'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $releaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin
|
||||
@ -1,90 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
.NET NuGet publish plugin.
|
||||
|
||||
.DESCRIPTION
|
||||
This plugin publishes the package artifact from shared runtime
|
||||
context to the configured NuGet feed using the configured API key.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret'
|
||||
$packageFile = $sharedSettings.packageFile
|
||||
|
||||
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
||||
|
||||
Assert-Command dotnet
|
||||
|
||||
if (-not $packageFile) {
|
||||
throw "DotNetNuGet plugin requires a NuGet package artifact. Ensure DotNetPack produced a .nupkg before running DotNetNuGet."
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
|
||||
"https://api.nuget.org/v3/index.json"
|
||||
}
|
||||
else {
|
||||
$pluginSettings.source
|
||||
}
|
||||
|
||||
Write-Log -Level "INFO" -Message "Dry run: would push $($packageFile.FullName) to $nugetSource"
|
||||
return
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($nugetSecret)) {
|
||||
throw "DotNetNuGet plugin requires 'nugetSecret' in scriptSettings.json (logical secret name, e.g. NuGet)."
|
||||
}
|
||||
|
||||
$nugetKey = Get-SecretEnvironmentValue -Name $nugetSecret
|
||||
if ([string]::IsNullOrWhiteSpace($nugetKey)) {
|
||||
throw "NuGet API key is not set. Set environment variable '$nugetSecret'."
|
||||
}
|
||||
|
||||
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
|
||||
"https://api.nuget.org/v3/index.json"
|
||||
}
|
||||
else {
|
||||
$pluginSettings.source
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Pushing package to NuGet feed..."
|
||||
dotnet nuget push $packageFile.FullName -k $nugetKey -s $nugetSource --skip-duplicate
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to push the package to NuGet feed."
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " NuGet push completed."
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-EnginePublishCompletion"
|
||||
Add-EnginePublishCompletion -Context $sharedSettings -Publisher 'DotNetNuGet'
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $true }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
|
||||
|
||||
|
||||
@ -1,98 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
.NET pack plugin for producing package artifacts.
|
||||
|
||||
.DESCRIPTION
|
||||
This plugin creates package output for the release pipeline.
|
||||
It packs the configured .NET project, resolves the generated
|
||||
package artifacts, and publishes them into shared runtime context
|
||||
for later plugins.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
# Load this globally only as a fallback. Re-importing PluginSupport in its own execution path
|
||||
# can invalidate commands already resolved by the release engine.
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
|
||||
Import-PluginDependency -ModuleName "DotNetArtifactSupport" -RequiredCommand "Resolve-DotNetPackageArtifacts"
|
||||
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
$version = $sharedSettings.version
|
||||
|
||||
if ($Settings.PSObject.Properties['projectFiles'] -and $null -ne $Settings.projectFiles) {
|
||||
$projectFiles = @(Resolve-RelativePaths -Value $Settings.projectFiles -BasePath $scriptDir)
|
||||
}
|
||||
elseif ($sharedSettings.PSObject.Properties['projectFiles'] -and $null -ne $sharedSettings.projectFiles) {
|
||||
$projectFiles = @($sharedSettings.projectFiles)
|
||||
}
|
||||
else {
|
||||
$projectFiles = @()
|
||||
}
|
||||
|
||||
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
|
||||
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir)))
|
||||
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
|
||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||
}
|
||||
else {
|
||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||
}
|
||||
|
||||
Assert-Command dotnet
|
||||
|
||||
if ($projectFiles.Count -eq 0) {
|
||||
throw "DotNetPack plugin requires projectFiles in plugin settings or projectFiles on shared context."
|
||||
}
|
||||
|
||||
$outputDir = $artifactsDirectory
|
||||
|
||||
if (!(Test-Path $outputDir)) {
|
||||
New-Item -ItemType Directory -Path $outputDir | Out-Null
|
||||
}
|
||||
|
||||
# First path in the configured project list is the pack target.
|
||||
$packageProjectPath = (@($projectFiles))[0]
|
||||
Write-Log -Level "STEP" -Message "Packing NuGet package..."
|
||||
$dotnetPackArguments = @(
|
||||
'pack', $packageProjectPath, '-c', 'Release', '-o', $outputDir, '--nologo',
|
||||
'-p:IncludeSymbols=true', '-p:SymbolPackageFormat=snupkg'
|
||||
)
|
||||
& dotnet @dotnetPackArguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet pack failed for $packageProjectPath."
|
||||
}
|
||||
|
||||
$resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $outputDir -Version $version
|
||||
|
||||
Write-Log -Level "OK" -Message " Package ready: $($resolved.PackageFile.FullName)"
|
||||
if ($resolved.SymbolsPackageFile) {
|
||||
Write-Log -Level "OK" -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "WARN" -Message " Symbols package (.snupkg) not found for version $version."
|
||||
}
|
||||
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin
|
||||
@ -1,75 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
.NET publish plugin for producing application release artifacts.
|
||||
|
||||
.DESCRIPTION
|
||||
This plugin publishes the configured .NET project into a release output
|
||||
directory and exposes that published directory to the shared release
|
||||
context so later release-stage plugins can archive and publish it.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
|
||||
|
||||
$sharedSettings = $Settings.context
|
||||
$projectFiles = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles')
|
||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||
$publishProjectPath = $null
|
||||
|
||||
Assert-Command dotnet
|
||||
|
||||
if ($null -eq $projectFiles -or @($projectFiles).Count -eq 0) {
|
||||
throw "DotNetPublish plugin requires project files in the shared context."
|
||||
}
|
||||
|
||||
$projectFiles = @($projectFiles)
|
||||
|
||||
if (!(Test-Path $artifactsDirectory)) {
|
||||
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
|
||||
}
|
||||
|
||||
# The first configured project remains the canonical release artifact source.
|
||||
$publishProjectPath = $projectFiles[0]
|
||||
$publishDir = Join-Path $artifactsDirectory ([System.IO.Path]::GetFileNameWithoutExtension($publishProjectPath))
|
||||
|
||||
if (Test-Path $publishDir) {
|
||||
Remove-Item -Path $publishDir -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Publishing release artifact..."
|
||||
dotnet publish $publishProjectPath -c Release -o $publishDir --nologo
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet publish failed for $publishProjectPath."
|
||||
}
|
||||
|
||||
$publishedItems = @(Get-ChildItem -Path $publishDir -Force -ErrorAction SilentlyContinue)
|
||||
if ($publishedItems.Count -eq 0) {
|
||||
throw "dotnet publish completed, but no files were produced in: $publishDir"
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir"
|
||||
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $null -Overwrite Replace -LegacyProperty 'packageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $null -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($publishDir) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin
|
||||
@ -1,96 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Loads release version from an SDK-style .csproj into shared context.
|
||||
|
||||
.DESCRIPTION
|
||||
Dedicated version-loading plugin. Reads <Version> from the first configured
|
||||
projectFiles entry and writes it (plus the resolved projectFiles) to the
|
||||
shared runtime context. Declares providesVersion = $true so the engine can
|
||||
discover it as the single release version source.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Get-CsprojPropertyValueInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[xml]$Csproj,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PropertyName
|
||||
)
|
||||
|
||||
# SDK-style .csproj files can have multiple PropertyGroup nodes.
|
||||
# Use the first group that defines the requested property.
|
||||
$propNode = $Csproj.Project.PropertyGroup |
|
||||
Where-Object { $_.$PropertyName } |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($propNode) {
|
||||
return $propNode.$PropertyName
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-CsprojVersionInternal {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ProjectPath
|
||||
)
|
||||
|
||||
if (-not (Test-Path $ProjectPath -PathType Leaf)) {
|
||||
throw "DotNetReleaseVersion: project file not found at '$ProjectPath'."
|
||||
}
|
||||
|
||||
if ([System.IO.Path]::GetExtension($ProjectPath) -ne ".csproj") {
|
||||
throw "DotNetReleaseVersion: configured project file is not a .csproj file: '$ProjectPath'."
|
||||
}
|
||||
|
||||
[xml]$csproj = Get-Content $ProjectPath
|
||||
$version = Get-CsprojPropertyValueInternal -Csproj $csproj -PropertyName "Version"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace([string]$version)) {
|
||||
throw "DotNetReleaseVersion: <Version> not found in '$ProjectPath'."
|
||||
}
|
||||
|
||||
return [string]$version
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
return [pscustomobject]@{ providesVersion = $true }
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
|
||||
|
||||
$shared = $Settings.context
|
||||
$projectFiles = @(Resolve-RelativePaths -Value $Settings.projectFiles -BasePath $shared.scriptDir)
|
||||
if ($projectFiles.Count -eq 0) {
|
||||
throw "DotNetReleaseVersion plugin requires 'projectFiles' (first .csproj with <Version>) in scriptSettings.json."
|
||||
}
|
||||
|
||||
Write-Log -Level "INFO" -Message "Reading version from SDK-style project file (projectFiles)..."
|
||||
$version = Get-CsprojVersionInternal -ProjectPath $projectFiles[0]
|
||||
|
||||
Set-EngineState -Context $shared -Name 'version' -Value $version
|
||||
Set-EngineFact -Context $shared -Namespace 'dotnet' -Name 'projectFiles' -Value $projectFiles -Overwrite Replace -LegacyProperty 'projectFiles'
|
||||
Write-Log -Level "OK" -Message " Release version loaded by DotNetReleaseVersion plugin: $version"
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
@ -1,90 +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). When `resultsDir` (or the
|
||||
multi-project default TestResults folder) is used, Cobertura output is kept on disk
|
||||
via TestRunner `-KeepResults` so repo-root `test-results/` persists after the run.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
# 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
|
||||
$invokeTestParams.KeepResults = $true
|
||||
}
|
||||
|
||||
$testResult = Invoke-TestsWithCoverage @invokeTestParams
|
||||
|
||||
if (-not $testResult.Success) {
|
||||
throw "Tests failed. $($testResult.Error)"
|
||||
}
|
||||
|
||||
Import-PluginDependency -ModuleName "TestRunner" -RequiredCommand "Publish-CoverageMetricsToSharedContext"
|
||||
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
|
||||
|
||||
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
|
||||
@ -6,9 +6,9 @@
|
||||
Publishes npm workspace packages to the npm registry.
|
||||
|
||||
.DESCRIPTION
|
||||
Publishes packages in configured order using npmSecret (logical secret name).
|
||||
Pass the token via an environment variable named like the configured npm secret (e.g. Npm).
|
||||
Uses a temporary .npmrc in the workspace root.
|
||||
Stages packages in configured order using RepoUtilsSecrets slot Npm.
|
||||
Uses npm stage publish so a maintainer approves the version with 2FA.
|
||||
Requires npm CLI 11.15.0 or newer. Uses a temporary .npmrc in the workspace root.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
@ -28,6 +28,7 @@ function Invoke-Plugin {
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Resolve-RelativePaths"
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Get-ReleaseSemverPrereleaseLabel"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$shared = $Settings.context
|
||||
@ -62,6 +63,14 @@ function Invoke-Plugin {
|
||||
[string]$pluginSettings.access
|
||||
}
|
||||
|
||||
$npmDistTag = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.npmDistTag)) {
|
||||
$npmDistTag = [string]$pluginSettings.npmDistTag
|
||||
}
|
||||
else {
|
||||
$npmDistTag = Get-ReleaseSemverPrereleaseLabel -Version ([string]$shared.version)
|
||||
}
|
||||
|
||||
$publishOrder = @()
|
||||
if ($pluginSettings.publishOrder) {
|
||||
if ($pluginSettings.publishOrder -is [System.Collections.IEnumerable] -and -not ($pluginSettings.publishOrder -is [string])) {
|
||||
@ -76,6 +85,8 @@ function Invoke-Plugin {
|
||||
throw "NpmPublish plugin requires non-empty 'publishOrder' (workspace package names)."
|
||||
}
|
||||
|
||||
$npmSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'npmSecret' -PluginDisplayName 'NpmPublish' -Required
|
||||
|
||||
Import-Module (Join-Path $PSScriptRoot 'NpmPackageSupport.psm1') -Force
|
||||
$useWorkspaces = Test-NpmWorkspacesConfigured -WorkspaceRoot $workspaceRoot
|
||||
if (-not $useWorkspaces -and $publishOrder.Count -gt 1) {
|
||||
@ -84,19 +95,24 @@ function Invoke-Plugin {
|
||||
|
||||
if ($dryRun) {
|
||||
foreach ($packageName in $publishOrder) {
|
||||
Write-Log -Level "INFO" -Message "Dry run: would publish npm package '$packageName' to $registry"
|
||||
$tagNote = if ([string]::IsNullOrWhiteSpace($npmDistTag)) { 'latest' } else { $npmDistTag }
|
||||
Write-Log -Level "INFO" -Message "Dry run: would stage npm package '$packageName' to $registry (dist-tag $tagNote). Approval with 2FA is separate."
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
$npmSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'npmSecret'
|
||||
if ([string]::IsNullOrWhiteSpace($npmSecret)) {
|
||||
throw "NpmPublish plugin requires 'npmSecret' in scriptSettings.json (logical secret name, e.g. Npm)."
|
||||
$npmCliVersion = [string](& npm --version)
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($npmCliVersion)) {
|
||||
throw "NpmPublish could not read the npm CLI version."
|
||||
}
|
||||
$npmCliVersion = $npmCliVersion.Trim()
|
||||
if (([version]$npmCliVersion) -lt [version]'11.15.0') {
|
||||
throw "NpmPublish requires npm CLI 11.15.0 or newer for 'npm stage publish' (installed: $npmCliVersion). Direct publish cannot defer 2FA."
|
||||
}
|
||||
|
||||
$npmToken = Get-SecretEnvironmentValue -Name $npmSecret
|
||||
$npmToken = Get-RepoUtilsSecretSlot -Name $npmSecret -Settings $shared
|
||||
if ([string]::IsNullOrWhiteSpace($npmToken)) {
|
||||
throw "npm API key is not set. Set environment variable '$npmSecret'."
|
||||
throw "npm API key is not set. Set RepoUtilsSecrets slot '$npmSecret' (npmSecret)."
|
||||
}
|
||||
|
||||
$registryHost = ([uri]$registry).Host
|
||||
@ -111,22 +127,29 @@ registry=$registry
|
||||
Set-Content -Path $tempNpmRcPath -Value $npmRcContent -Encoding UTF8 -NoNewline
|
||||
|
||||
foreach ($packageName in $publishOrder) {
|
||||
Write-Log -Level "STEP" -Message "Publishing npm package '$packageName'..."
|
||||
Write-Log -Level "STEP" -Message "Staging npm package '$packageName' (approve later with 2FA)..."
|
||||
$publishArgs = @('stage', 'publish')
|
||||
if ($useWorkspaces) {
|
||||
npm publish -w $packageName --access $access --userconfig $tempNpmRcPath
|
||||
$publishArgs += @('-w', $packageName)
|
||||
}
|
||||
else {
|
||||
Assert-NpmRootPackageName -WorkspaceRoot $workspaceRoot -ExpectedPackageName $packageName
|
||||
npm publish --access $access --userconfig $tempNpmRcPath
|
||||
}
|
||||
$publishArgs += @('--access', $access, '--userconfig', $tempNpmRcPath)
|
||||
if (-not [string]::IsNullOrWhiteSpace($npmDistTag)) {
|
||||
$publishArgs += @('--tag', $npmDistTag)
|
||||
Write-Log -Level "INFO" -Message " Using npm dist-tag '$npmDistTag' (prerelease)."
|
||||
}
|
||||
|
||||
npm @publishArgs
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to publish npm package '$packageName'."
|
||||
throw "Failed to stage npm package '$packageName'."
|
||||
}
|
||||
Write-Log -Level "OK" -Message " Published $packageName."
|
||||
Write-Log -Level "OK" -Message " Staged $packageName. It is not installable until a maintainer runs 'npm stage approve' with 2FA."
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " npm publish completed."
|
||||
Write-Log -Level "OK" -Message " npm stage publish completed."
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-EnginePublishCompletion"
|
||||
Add-EnginePublishCompletion -Context $shared -Publisher 'NpmPublish'
|
||||
}
|
||||
|
||||
@ -35,8 +35,9 @@ function Get-PackageJsonVersionInternal {
|
||||
throw "NpmReleaseVersion: 'version' is missing in '$PackageJsonPath'."
|
||||
}
|
||||
|
||||
if ($version -notmatch '^\d+\.\d+\.\d+') {
|
||||
throw "NpmReleaseVersion: version '$version' in '$PackageJsonPath' is not a valid semver."
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||
if (-not (Test-ReleaseSemver -Version $version)) {
|
||||
throw "NpmReleaseVersion: version '$version' in '$PackageJsonPath' is not a valid semver (X.Y.Z or X.Y.Z-prerelease)."
|
||||
}
|
||||
|
||||
return $version
|
||||
@ -69,6 +70,7 @@ function Invoke-Plugin {
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$shared = $Settings.context
|
||||
|
||||
78
utils/plugins/Platform/CollectCoverage.psm1
Normal file
78
utils/plugins/Platform/CollectCoverage.psm1
Normal file
@ -0,0 +1,78 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Collects coverage metrics from an existing test results directory.
|
||||
|
||||
.DESCRIPTION
|
||||
Stack-agnostic parser for recovered container or host test output. Auto-detects .NET
|
||||
Cobertura (coverage.cobertura.xml) or Jest (coverage-summary.json) and publishes the
|
||||
same shared-context keys as DotNetTest / NpmJestTest for QualityGate.
|
||||
Use after DockerContainerBuilder / PodmanContainerBuilder when tests run inside a container image.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Get-CoverageFromResultsDirectory'
|
||||
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Publish-CoverageMetricsToSharedContext'
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
|
||||
$resultsDirSetting = $pluginSettings.resultsDir
|
||||
if ([string]::IsNullOrWhiteSpace($resultsDirSetting)) {
|
||||
if ($sharedSettings.PSObject.Properties.Name -contains 'testResultsDirectory' -and $sharedSettings.testResultsDirectory) {
|
||||
$resultsDirSetting = $sharedSettings.testResultsDirectory
|
||||
}
|
||||
else {
|
||||
throw "CollectCoverage requires 'resultsDir' in plugin settings or testResultsDirectory on shared context (from ContainerBuilder)."
|
||||
}
|
||||
}
|
||||
|
||||
$resultsDirectory = if ([System.IO.Path]::IsPathRooted([string]$resultsDirSetting)) {
|
||||
[string]$resultsDirSetting
|
||||
}
|
||||
else {
|
||||
[System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$resultsDirSetting)))
|
||||
}
|
||||
|
||||
$format = 'auto'
|
||||
if ($pluginSettings.PSObject.Properties.Name -contains 'format' -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.format)) {
|
||||
$format = [string]$pluginSettings.format
|
||||
}
|
||||
|
||||
Write-Log -Level 'STEP' -Message "Collecting coverage from $resultsDirectory ..."
|
||||
$testResult = Get-CoverageFromResultsDirectory -ResultsDirectory $resultsDirectory -Format $format -Silent:$true
|
||||
if (-not $testResult.Success) {
|
||||
throw $testResult.Error
|
||||
}
|
||||
|
||||
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
|
||||
|
||||
$formatLabel = if ($testResult.CoverageFormat) { $testResult.CoverageFormat } else { 'unknown' }
|
||||
Write-Log -Level 'OK' -Message " Coverage collected ($formatLabel)."
|
||||
Write-Log -Level 'INFO' -Message " Line Coverage: $($testResult.LineRate)%"
|
||||
Write-Log -Level 'INFO' -Message " Branch Coverage: $($testResult.BranchRate)%"
|
||||
Write-Log -Level 'INFO' -Message " Method Coverage: $($testResult.MethodRate)%"
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $false }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
52
utils/plugins/Platform/ContainerEngineProbe.psm1
Normal file
52
utils/plugins/Platform/ContainerEngineProbe.psm1
Normal file
@ -0,0 +1,52 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Probes docker/podman availability and sets mandatory shared engine state for the pipeline.
|
||||
|
||||
.DESCRIPTION
|
||||
Must be the first enabled plugin. Sets containerEngineProbeCompleted, activeContainerEngine,
|
||||
dockerEngineAvailable, podmanEngineAvailable, and compose CLI fields on shared context.
|
||||
Fails the pipeline when no usable engine is found.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||
Import-PluginDependency -ModuleName 'ContainerEngineSupport' -RequiredCommand 'Resolve-ActiveContainerEngineState'
|
||||
|
||||
$shared = $Settings.context
|
||||
|
||||
Write-Log -Level 'STEP' -Message 'Probing container engine availability...'
|
||||
$state = Resolve-ActiveContainerEngineState
|
||||
Set-ContainerEngineSharedState -SharedSettings $shared -State $state
|
||||
|
||||
$display = Get-ContainerEngineDisplayName -Engine (Get-SharedContainerEngineInvocation -SharedSettings $shared)
|
||||
Write-Log -Level 'OK' -Message " Active engine: $display"
|
||||
Write-Log -Level 'INFO' -Message " docker CLI available: $($state.DockerEngineAvailable); podman CLI available: $($state.PodmanEngineAvailable)"
|
||||
if ([string]::IsNullOrWhiteSpace([string]$state.ComposeExecutable)) {
|
||||
Write-Log -Level 'INFO' -Message ' Compose CLI: not available (Compose plugins will fail if enabled).'
|
||||
}
|
||||
else {
|
||||
Write-Log -Level 'INFO' -Message " Compose CLI: $($state.ComposeExecutable) $($state.ComposeBaseArgs -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $false }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
124
utils/plugins/Platform/DiscoverPackageArtifacts.psm1
Normal file
124
utils/plugins/Platform/DiscoverPackageArtifacts.psm1
Normal file
@ -0,0 +1,124 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Discovers package artifacts produced by pack plugins or container build pipelines.
|
||||
|
||||
.DESCRIPTION
|
||||
Stack-agnostic scanner for artifactsDir. Detects .NET .nupkg/.snupkg or npm .tgz files
|
||||
and populates shared context (packageFile, releaseAssetPaths, releaseArchiveInputs).
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
|
||||
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
|
||||
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Settings
|
||||
)
|
||||
|
||||
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
|
||||
Import-PluginDependency -ModuleName 'DotNetArtifactSupport' -RequiredCommand 'Resolve-DotNetPackageArtifacts'
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$scriptDir = $sharedSettings.scriptDir
|
||||
$version = $sharedSettings.version
|
||||
|
||||
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
|
||||
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir)))
|
||||
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
|
||||
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
|
||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||
}
|
||||
else {
|
||||
$sharedSettings | Add-Member -NotePropertyName artifactsDirectory -NotePropertyValue $artifactsDirectory -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
|
||||
}
|
||||
}
|
||||
else {
|
||||
$artifactsDirectory = $sharedSettings.artifactsDirectory
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
|
||||
throw 'DiscoverPackageArtifacts requires artifactsDir in plugin settings or artifactsDirectory on shared context.'
|
||||
}
|
||||
|
||||
$nupkgCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.nupkg' -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Name -notlike '*.symbols.nupkg' -and $_.Name -notlike '*.snupkg'
|
||||
})
|
||||
$tgzCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.tgz' -ErrorAction SilentlyContinue)
|
||||
|
||||
if ($nupkgCandidates.Count -gt 0 -and $tgzCandidates.Count -gt 0) {
|
||||
throw "DiscoverPackageArtifacts found both NuGet and npm artifacts in $artifactsDirectory. Use one package stack per repository."
|
||||
}
|
||||
|
||||
if ($nupkgCandidates.Count -gt 0) {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$version)) {
|
||||
throw 'DiscoverPackageArtifacts requires release version on shared context when discovering .nupkg artifacts.'
|
||||
}
|
||||
|
||||
Write-Log -Level 'STEP' -Message "Discovering NuGet package artifacts in $artifactsDirectory ..."
|
||||
$resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
|
||||
|
||||
Write-Log -Level 'OK' -Message " Package ready: $($resolved.PackageFile.FullName)"
|
||||
if ($resolved.SymbolsPackageFile) {
|
||||
Write-Log -Level 'OK' -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level 'WARN' -Message " Symbols package (.snupkg) not found for version $version."
|
||||
}
|
||||
|
||||
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName symbolsPackageFile -NotePropertyValue $resolved.SymbolsPackageFile -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
|
||||
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ($tgzCandidates.Count -gt 0) {
|
||||
Import-PluginDependency -ModuleName 'NpmArtifactSupport' -RequiredCommand 'Resolve-NpmPackageArtifacts'
|
||||
|
||||
Write-Log -Level 'STEP' -Message "Discovering npm package artifacts in $artifactsDirectory ..."
|
||||
$resolved = Resolve-NpmPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
|
||||
|
||||
foreach ($tarball in $resolved.PackageFiles) {
|
||||
Write-Log -Level 'OK' -Message " Package ready: $($tarball.FullName)"
|
||||
}
|
||||
|
||||
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseAssetPaths -Force
|
||||
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
|
||||
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
|
||||
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
|
||||
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
|
||||
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
throw "DiscoverPackageArtifacts found no .nupkg or .tgz files in: $artifactsDirectory"
|
||||
}
|
||||
|
||||
function Get-PluginMetadata {
|
||||
[pscustomobject]@{ mutatesRemote = $false }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata
|
||||
@ -7,9 +7,10 @@
|
||||
|
||||
.DESCRIPTION
|
||||
Reads a single-line semver from the configured versionFilePath (default
|
||||
repo-root VERSION). Useful for repositories without .csproj or package.json
|
||||
version metadata. Declares providesVersion = $true so the engine can
|
||||
discover it as the single release version source.
|
||||
repo-root VERSION), including optional prerelease (0.1.0-alpha.1). Useful for
|
||||
repositories without .csproj or package.json version metadata. Declares
|
||||
providesVersion = $true so the engine can discover it as the single release
|
||||
version source.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
@ -36,8 +37,9 @@ function Get-VersionFileSemverInternal {
|
||||
}
|
||||
|
||||
$version = $version -replace '^[vV]', ''
|
||||
if ($version -notmatch '^\d+\.\d+\.\d+') {
|
||||
throw "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver."
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||
if (-not (Test-ReleaseSemver -Version $version)) {
|
||||
throw "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver (X.Y.Z or X.Y.Z-prerelease)."
|
||||
}
|
||||
|
||||
return $version
|
||||
@ -55,6 +57,7 @@ function Invoke-Plugin {
|
||||
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
|
||||
|
||||
$shared = $Settings.context
|
||||
$versionFileSetting = if ($Settings.versionFilePath) {
|
||||
|
||||
@ -10,7 +10,8 @@
|
||||
repository, and creates the configured GitHub release using the
|
||||
shared release artifacts and release notes from CHANGELOG.md.
|
||||
Release notes must use Keep a Changelog headers: ## [semver] - YYYY-MM-DD
|
||||
(see ChangelogSupport.psm1).
|
||||
(including optional SemVer prerelease, e.g. ## [0.1.0-alpha.1] / [0.1.0-beta.1] / [0.1.0-rc.1];
|
||||
see ChangelogSupport.psm1). Hyphenated versions are created with gh --prerelease.
|
||||
#>
|
||||
|
||||
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
|
||||
@ -95,11 +96,11 @@ function Invoke-Plugin {
|
||||
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
|
||||
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Get-LatestChangelogVersion"
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemverPrerelease"
|
||||
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Get-EngineFact"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$sharedSettings = $Settings.context
|
||||
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret'
|
||||
$configuredRepository = $pluginSettings.repository
|
||||
$releaseNotesFileSetting = $pluginSettings.releaseNotesFile
|
||||
$releaseTitlePatternSetting = $pluginSettings.releaseTitlePattern
|
||||
@ -110,6 +111,7 @@ function Invoke-Plugin {
|
||||
$releaseAssetPaths = @()
|
||||
|
||||
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
|
||||
$githubSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'githubSecret' -PluginDisplayName 'GitHub' -Required
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||
throw "GitHub plugin requires 'releaseNotesFile' in scriptSettings.json."
|
||||
@ -128,18 +130,17 @@ function Invoke-Plugin {
|
||||
}
|
||||
$releaseName = $releaseTitlePattern -replace '\{version\}', $version
|
||||
Write-Log -Level "INFO" -Message "Dry run: would create GitHub release '$releaseName' ($tag) on $repo"
|
||||
if (Test-ReleaseSemverPrerelease -Version ([string]$version)) {
|
||||
Write-Log -Level "INFO" -Message "Dry run: release would be marked prerelease."
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Assert-Command gh
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($githubSecret)) {
|
||||
throw "GitHub plugin requires 'githubSecret' in scriptSettings.json (logical secret name, e.g. GitHub)."
|
||||
}
|
||||
|
||||
$ghToken = Get-SecretEnvironmentValue -Name $githubSecret
|
||||
$ghToken = Get-RepoUtilsSecretSlot -Name $githubSecret -Settings $sharedSettings
|
||||
if ([string]::IsNullOrWhiteSpace($ghToken)) {
|
||||
throw "GitHub token is not set. Set environment variable '$githubSecret'."
|
||||
throw "GitHub token is not set. Set RepoUtilsSecrets slot '$githubSecret' (githubSecret)."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($releaseNotesFileSetting)) {
|
||||
@ -230,7 +231,7 @@ function Invoke-Plugin {
|
||||
$authStatus | ForEach-Object { Write-Log -Level "WARN" -Message " $_" }
|
||||
}
|
||||
|
||||
throw "GitHub CLI authentication failed for repository '$repo'. Ensure secret '$githubSecret' is valid and has access to this repository."
|
||||
throw "GitHub CLI authentication failed for repository '$repo'. Ensure RepoUtilsSecrets slot '$githubSecret' is valid and has access to this repository."
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message " GitHub token validated for repository: $($authOutput | Select-Object -First 1)"
|
||||
@ -261,6 +262,10 @@ function Invoke-Plugin {
|
||||
"--title", $releaseName,
|
||||
"--notes-file", $notesFilePath
|
||||
)
|
||||
if (Test-ReleaseSemverPrerelease -Version ([string]$version)) {
|
||||
$createReleaseArgs += '--prerelease'
|
||||
}
|
||||
|
||||
& gh @createReleaseArgs
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
@ -11,8 +11,9 @@
|
||||
when they do not (whenRequirementsNotMet: skip). Publish plugins no longer use per-plugin
|
||||
branch lists; put allowed branches here instead.
|
||||
|
||||
Typical checks: allowed branches, optional clean working tree, exact semver tag on HEAD,
|
||||
tag version vs DotNetReleaseVersion, optional push tag to remote.
|
||||
Typical checks: allowed branches, optional clean working tree, exact semver tag on HEAD
|
||||
(vX.Y.Z or vX.Y.Z-prerelease such as v0.1.0-alpha.1 / v0.1.0-beta.1 / v0.1.0-rc.1),
|
||||
tag version vs release version, optional push tag to remote.
|
||||
|
||||
The engine preflight no longer reads git tags; this plugin sets context.tag from the
|
||||
git tag on HEAD when required. Shared context version always remains from DotNetReleaseVersion.
|
||||
@ -77,6 +78,7 @@ function Invoke-Plugin {
|
||||
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Get-GitStatusShort"
|
||||
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Test-RemoteTagExists"
|
||||
Import-PluginDependency -ModuleName "GitTools" -RequiredCommand "Push-TagToRemote"
|
||||
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Get-ChangelogSemverPattern"
|
||||
|
||||
$pluginSettings = $Settings
|
||||
$shared = $Settings.context
|
||||
@ -123,8 +125,9 @@ function Invoke-Plugin {
|
||||
return
|
||||
}
|
||||
|
||||
if ($tag -notmatch '^v(\d+\.\d+\.\d+)$') {
|
||||
Invoke-NotMetInternal -Shared $shared -When $when -Reason "tag '$tag' must match vX.Y.Z."
|
||||
$tagPattern = '^v(' + (Get-ChangelogSemverPattern) + ')$'
|
||||
if ($tag -notmatch $tagPattern) {
|
||||
Invoke-NotMetInternal -Shared $shared -When $when -Reason "tag '$tag' must match vX.Y.Z or vX.Y.Z-prerelease (e.g. v0.1.0-alpha.1, v0.1.0-beta.1, v0.1.0-rc.1)."
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
# CI builder image for .NET NuGet libraries: test (Coverlet) + pack in one ENTRYPOINT.
|
||||
#
|
||||
# Setup in a product repo:
|
||||
# 1. Copy to utils/engines/containerbuilder/{repo}-containerbuilder.Dockerfile
|
||||
# 2. Customize test/pack .csproj paths below
|
||||
# 3. Reference the same dockerfile from engines/test and engines/release scriptSettings
|
||||
# 4. Disable host DotNetTest and DotNetPack; enable CollectCoverage + DiscoverPackageArtifacts (release)
|
||||
#
|
||||
# Source injection: ContainerBuilder copies sourceContextPath into WORKDIR (/src).
|
||||
# - Repo root (sourceContextPath ..\..\..): use paths like src/YourProject/YourProject.csproj
|
||||
# - src folder (sourceContextPath ..\..\..\src): use paths like YourProject/YourProject.csproj
|
||||
#
|
||||
# Reference: maksit-nats (utils/engines/containerbuilder/maksit-nats-containerbuilder.Dockerfile)
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0
|
||||
WORKDIR /src
|
||||
ENV ARTIFACTS_DIR=/artifacts
|
||||
RUN mkdir -p /artifacts /testResults
|
||||
|
||||
# Option B: COPY build/ci-dotnet-pack.sh /usr/local/bin/ci-pack.sh && chmod +x ... && ENTRYPOINT ["/usr/local/bin/ci-pack.sh"]
|
||||
ENTRYPOINT ["sh", "-c", "dotnet test path/to/YourProject.Tests/YourProject.Tests.csproj -c Release --nologo --collect:\"XPlat Code Coverage\" --results-directory /testResults && dotnet pack path/to/YourProject/YourProject.csproj -c Release -o /artifacts --nologo -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg"]
|
||||
@ -1,22 +0,0 @@
|
||||
# CI builder image for npm libraries: ci, Jest coverage, build, and pack (.tgz).
|
||||
#
|
||||
# Setup in a product repo:
|
||||
# 1. Copy to utils/engines/containerbuilder/{repo}-containerbuilder.Dockerfile
|
||||
# 2. Customize WORKDIR (package root), testScript, and build script names
|
||||
# 3. Reference the same dockerfile from engines/test and engines/release scriptSettings
|
||||
# 4. Disable host NpmJestTest / NpmBuild / NpmPack when the container owns the pipeline
|
||||
#
|
||||
# Source injection: set sourceContextPath to the folder that contains package.json (often repo src/).
|
||||
# Recovered paths: /artifacts (.tgz), /testResults (coverage/ for Jest json-summary).
|
||||
#
|
||||
# CollectCoverage reads recovered /testResults for QualityGate; DiscoverPackageArtifacts finds .tgz in /artifacts.
|
||||
|
||||
FROM node:24-bookworm-slim
|
||||
WORKDIR /src
|
||||
ENV ARTIFACTS_DIR=/artifacts
|
||||
ENV NPM_CONFIG_FUND=false
|
||||
ENV NPM_CONFIG_AUDIT=false
|
||||
RUN mkdir -p /artifacts /testResults
|
||||
|
||||
# Option B: COPY build/ci-npm-pack.sh /usr/local/bin/ci-npm.sh && chmod +x ... && ENTRYPOINT ["/usr/local/bin/ci-npm.sh"]
|
||||
ENTRYPOINT ["sh", "-c", "npm ci && npm run test:coverage -- --coverage --coverageReporters=json-summary --coverageReporters=text && cp -r coverage /testResults/ && npm run build && npm pack --pack-destination /artifacts"]
|
||||
@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${ARTIFACTS_DIR:=/artifacts}"
|
||||
mkdir -p "$ARTIFACTS_DIR" /testResults
|
||||
|
||||
# Customize test/pack .csproj paths (see templates/Dockerfile.ci.dotnet).
|
||||
dotnet test path/to/YourProject.Tests/YourProject.Tests.csproj -c Release --nologo \
|
||||
--collect:"XPlat Code Coverage" --results-directory /testResults
|
||||
dotnet pack path/to/YourProject/YourProject.csproj -c Release -o "$ARTIFACTS_DIR" --nologo \
|
||||
-p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
|
||||
@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${ARTIFACTS_DIR:=/artifacts}"
|
||||
mkdir -p "$ARTIFACTS_DIR" /testResults
|
||||
|
||||
# Customize testScript / build script names for your package.json.
|
||||
npm ci
|
||||
npm run test:coverage -- --coverage --coverageReporters=json-summary --coverageReporters=text
|
||||
cp -r coverage /testResults/
|
||||
npm run build
|
||||
npm pack --pack-destination "$ARTIFACTS_DIR"
|
||||
Loading…
Reference in New Issue
Block a user