mirror of
https://github.com/MAKS-IT-COM/maksit-webui.git
synced 2026-08-15 09:58:09 +02:00
(feature): Breadcrumb and RepoUtils update
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
eb9e8b9af0
commit
ecce33e42f
10
CHANGELOG.md
10
CHANGELOG.md
@ -4,6 +4,16 @@ 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.3] - 2026-08-06
|
||||
|
||||
### Added
|
||||
|
||||
- `Breadcrumb` — presentational page trail (`nav` + `ol`); injectable `linkComponent` (e.g. react-router `Link`); last item is current page (`aria-current`). Export: `Breadcrumb`, `BreadcrumbProps`, `BreadcrumbItem`. Storybook coverage included.
|
||||
|
||||
### Changed
|
||||
|
||||
- RepoUtils: removed `Update-RepoUtils` tool (local-copy sync only); engine/plugin/module refinements; added `utils/templates/` CI Docker/build helpers.
|
||||
|
||||
## [0.4.2] - 2026-07-28
|
||||
|
||||
### Added
|
||||
|
||||
@ -44,7 +44,7 @@ Configured plugins (see `utils\engines\release\scriptSettings.json`):
|
||||
| `GitHub` | GitHub release (optional; set `GitHub` env var) |
|
||||
| `NpmPublish` | Publish `@maks-it.com/webui` |
|
||||
|
||||
Refresh shared utils from repoutils: **`utils\Update-RepoUtils.bat`**.
|
||||
Refresh shared utils from **maksit-repoutils** via local-copy sync (no Update-RepoUtils in product repos).
|
||||
|
||||
## Consume in product repos
|
||||
|
||||
|
||||
119
src/components/components/Breadcrumb/Breadcrumb.tsx
Normal file
119
src/components/components/Breadcrumb/Breadcrumb.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import {
|
||||
type ComponentType,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
label: ReactNode
|
||||
/** Used by default `<a>` and as fallback for router links. */
|
||||
href?: string
|
||||
/** Preferred when injecting react-router `Link`. */
|
||||
to?: string
|
||||
/** Extra props passed to the injected link component. */
|
||||
linkProps?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type BreadcrumbLinkComponent = ComponentType<{
|
||||
href?: string
|
||||
to?: string
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
[key: string]: unknown
|
||||
}>
|
||||
|
||||
export interface BreadcrumbProps {
|
||||
items: BreadcrumbItem[]
|
||||
/** Defaults to `"/"`. */
|
||||
separator?: ReactNode
|
||||
className?: string
|
||||
linkClassName?: string
|
||||
currentClassName?: string
|
||||
separatorClassName?: string
|
||||
/** Host injects `Link` from react-router (or any anchor-like component). Defaults to `<a>`. */
|
||||
linkComponent?: BreadcrumbLinkComponent
|
||||
/** Accessible name for the nav landmark. Defaults to `"Breadcrumb"`. */
|
||||
label?: string
|
||||
}
|
||||
|
||||
const DefaultLink: BreadcrumbLinkComponent = ({
|
||||
href,
|
||||
to,
|
||||
children,
|
||||
...rest
|
||||
}) => (
|
||||
<a href={href ?? to} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
|
||||
/**
|
||||
* Presentational page trail. Use links/`span` only — never headings.
|
||||
* Keep a single page `h1` on `FormHeader` or the page title.
|
||||
*/
|
||||
const Breadcrumb: FC<BreadcrumbProps> = ({
|
||||
items,
|
||||
separator = '/',
|
||||
className = '',
|
||||
linkClassName = 'text-sky-700 hover:text-sky-900 hover:underline',
|
||||
currentClassName = 'text-gray-700',
|
||||
separatorClassName = 'text-gray-400',
|
||||
linkComponent: LinkComponent = DefaultLink,
|
||||
label = 'Breadcrumb',
|
||||
}) => {
|
||||
if (items.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label={label}
|
||||
className={['text-sm', className].filter(Boolean).join(' ')}
|
||||
>
|
||||
<ol className={'flex flex-wrap items-center gap-x-2 gap-y-1'}>
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1
|
||||
const target = item.to ?? item.href
|
||||
const isLink = !isLast && Boolean(target)
|
||||
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className={'inline-flex items-center gap-x-2'}
|
||||
>
|
||||
{index > 0 ? (
|
||||
<span
|
||||
className={separatorClassName}
|
||||
aria-hidden={'true'}
|
||||
>
|
||||
{separator}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{isLink ? (
|
||||
<LinkComponent
|
||||
href={item.href ?? (typeof item.to === 'string' ? item.to : undefined)}
|
||||
to={item.to ?? item.href}
|
||||
className={linkClassName}
|
||||
{...(item.linkProps ?? {})}
|
||||
>
|
||||
{item.label}
|
||||
</LinkComponent>
|
||||
) : (
|
||||
<span
|
||||
className={currentClassName}
|
||||
{...(isLast ? { 'aria-current': 'page' as const } : {})}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb
|
||||
}
|
||||
2
src/components/components/Breadcrumb/index.ts
Normal file
2
src/components/components/Breadcrumb/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Breadcrumb } from './Breadcrumb'
|
||||
export type { BreadcrumbProps, BreadcrumbItem } from './Breadcrumb'
|
||||
@ -3,6 +3,8 @@ export { FieldContainer } from './components/editors/FieldContainer'
|
||||
export { SecretComponent } from './components/editors/SecretComponent'
|
||||
export type { SecretDataSource, SecretComponentProps } from './components/editors/SecretComponent'
|
||||
export { FormContainer, FormContent, FormFooter, FormHeader } from './components/FormLayout'
|
||||
export { Breadcrumb } from './components/Breadcrumb'
|
||||
export type { BreadcrumbProps, BreadcrumbItem } from './components/Breadcrumb'
|
||||
export { Offcanvas } from './components/Offcanvas'
|
||||
export { Modal, ConfirmDialog } from './components/Modal'
|
||||
export type { ModalProps, ModalSize, ConfirmDialogProps } from './components/Modal'
|
||||
|
||||
4
src/package-lock.json
generated
4
src/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@maks-it.com/webui",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@maks-it.com/webui",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"date-fns": "^4.4.0"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@maks-it.com/webui",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.3",
|
||||
"description": "Shared contracts, utilities, and React components for MaksIT WebUI apps",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
|
||||
79
src/stories/components/Breadcrumb.stories.tsx
Normal file
79
src/stories/components/Breadcrumb.stories.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { expect, within } from 'storybook/test'
|
||||
import { Breadcrumb } from '@webui/components/components/Breadcrumb'
|
||||
import { FormHeader } from '@webui/components/components/FormLayout/FormHeader'
|
||||
|
||||
const meta = {
|
||||
title: 'components/Breadcrumb',
|
||||
component: Breadcrumb,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Presentational page trail (`nav` + `ol`). Never uses headings — keep a single page `h1` on `FormHeader` or the page title. Inject `linkComponent` (e.g. react-router `Link`) for SPA navigation.',
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Breadcrumb>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{ label: 'Admin', to: '/admin' },
|
||||
{ label: 'Shop', to: '/admin/shop' },
|
||||
{ label: 'Edit item' },
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const nav = canvas.getByRole('navigation', { name: /breadcrumb/i })
|
||||
await expect(nav).toBeVisible()
|
||||
await expect(within(nav).getByRole('link', { name: 'Admin' })).toHaveAttribute('href', '/admin')
|
||||
await expect(within(nav).getByRole('link', { name: 'Shop' })).toHaveAttribute('href', '/admin/shop')
|
||||
await expect(within(nav).getByText('Edit item')).toHaveAttribute('aria-current', 'page')
|
||||
},
|
||||
}
|
||||
|
||||
export const SingleItem: Story = {
|
||||
args: {
|
||||
items: [{ label: 'Dashboard' }],
|
||||
},
|
||||
}
|
||||
|
||||
export const WithFormHeader: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-0 border border-gray-200 bg-white">
|
||||
<div className="bg-gray-50 px-4 py-2">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: 'Admin', to: '/admin' },
|
||||
{ label: 'Shop', to: '/admin/shop' },
|
||||
{ label: 'Edit item' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<FormHeader>Edit shop item</FormHeader>
|
||||
<div className="p-4 text-sm text-gray-600">Form content</div>
|
||||
</div>
|
||||
),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
await expect(canvas.getByRole('navigation', { name: /breadcrumb/i })).toBeVisible()
|
||||
await expect(canvas.getByRole('heading', { level: 1, name: 'Edit shop item' })).toBeVisible()
|
||||
},
|
||||
}
|
||||
|
||||
export const CustomSeparator: Story = {
|
||||
args: {
|
||||
separator: '›',
|
||||
items: [
|
||||
{ label: 'Shop', to: '/shop' },
|
||||
{ label: 'Parent product', to: '/shop/parent' },
|
||||
{ label: 'Gallery image' },
|
||||
],
|
||||
},
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\Update-RepoUtils\Update-RepoUtils.ps1" %*
|
||||
pause
|
||||
@ -80,7 +80,7 @@ else {
|
||||
}
|
||||
}
|
||||
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -ContinueOnError:$false
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot
|
||||
if (-not $pluginSucceeded) {
|
||||
$releaseHadPluginFailures = $true
|
||||
break
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
Plugin-driven test and coverage engine entry script.
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$srcDir = (Resolve-Path (Join-Path $scriptDir '..\..')).Path
|
||||
|
||||
@ -29,7 +31,7 @@ if ($configuredPlugins.Count -eq 0) {
|
||||
$testHadPluginFailures = $false
|
||||
|
||||
foreach ($plugin in $configuredPlugins) {
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -EngineDirectory $scriptDir -ContinueOnError:$false
|
||||
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $engineContext -EngineDirectory $scriptDir
|
||||
if (-not $pluginSucceeded) {
|
||||
$testHadPluginFailures = $true
|
||||
break
|
||||
|
||||
@ -22,7 +22,7 @@ function Test-IsEngineRuntimeModuleName {
|
||||
[string]$ModuleName
|
||||
)
|
||||
|
||||
# Engine runtime under modules/ only — never dual-homed under plugins/.
|
||||
# Host engine runtime under modules/ (and optional modules/Extensions/) — never dual-homed under plugins/.
|
||||
$engineNames = [System.Collections.Generic.HashSet[string]]::new(
|
||||
[string[]]@(
|
||||
'ChangelogSupport',
|
||||
@ -34,11 +34,7 @@ function Test-IsEngineRuntimeModuleName {
|
||||
'EngineContext',
|
||||
'PluginSupport',
|
||||
'ReleaseSupport',
|
||||
'TestSupport',
|
||||
'DeployConfig',
|
||||
'EngineContextSupport',
|
||||
'OrchestratorSupport',
|
||||
'PluginPathSupport'
|
||||
'TestSupport'
|
||||
),
|
||||
[System.StringComparer]::OrdinalIgnoreCase
|
||||
)
|
||||
@ -46,6 +42,34 @@ function Test-IsEngineRuntimeModuleName {
|
||||
return $engineNames.Contains($ModuleName)
|
||||
}
|
||||
|
||||
function Get-PluginDependencyGroupDirectories {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PluginsRoot
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $PluginsRoot -PathType Container)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
# Prefer Shared (helpers), then stock host groups; any other plugins/{Group}/ is discovered.
|
||||
$preferred = @('Shared', 'Platform', 'DotNet', 'Npm')
|
||||
$dirs = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($name in $preferred) {
|
||||
$path = Join-Path $PluginsRoot $name
|
||||
if (Test-Path -LiteralPath $path -PathType Container) {
|
||||
$dirs.Add($path)
|
||||
}
|
||||
}
|
||||
|
||||
Get-ChildItem -LiteralPath $PluginsRoot -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -notin $preferred } |
|
||||
Sort-Object Name |
|
||||
ForEach-Object { $dirs.Add($_.FullName) }
|
||||
|
||||
return @($dirs)
|
||||
}
|
||||
|
||||
function Import-PluginDependency {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@ -66,16 +90,16 @@ function Import-PluginDependency {
|
||||
$candidatePaths = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
if (Test-IsEngineRuntimeModuleName -ModuleName $ModuleName) {
|
||||
# Engine runtime: modules/ only (no plugins/ fallback).
|
||||
# Engine runtime: modules/ only (no plugins/ fallback). Optional Extensions/ for layered hosts.
|
||||
$candidatePaths.Add((Join-Path $modulesDir "$ModuleName.psm1"))
|
||||
$candidatePaths.Add((Join-Path $engineModuleDir "$ModuleName.psm1"))
|
||||
$extensionsDir = Join-Path $modulesDir 'Extensions'
|
||||
$candidatePaths.Add((Join-Path $extensionsDir "$ModuleName.psm1"))
|
||||
}
|
||||
else {
|
||||
# Plugin helpers: plugins/ only (no modules/ legacy shadow).
|
||||
foreach ($group in @('Shared', 'Platform', 'DotNet', 'Npm', 'Helm', 'Docker', 'Podman')) {
|
||||
$candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $group) "$ModuleName.psm1"))
|
||||
# Plugin helpers: plugins/{Group}/ only (no modules/ legacy shadow). Groups are discovered.
|
||||
foreach ($groupDir in Get-PluginDependencyGroupDirectories -PluginsRoot $pluginsRoot) {
|
||||
$candidatePaths.Add((Join-Path $groupDir "$ModuleName.psm1"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -344,8 +368,8 @@ function Get-RegistryCredentialsFromRuntime {
|
||||
|
||||
.DESCRIPTION
|
||||
Looks up the environment variable named by SecretName. The value must be
|
||||
Base64(UTF8('username:password')). Used by Docker/Podman/Helm registry login
|
||||
and image-pull secret creation — never pass the password itself as a parameter.
|
||||
Base64(UTF8('username:password')). Used by registry login and image-pull
|
||||
secret creation — never pass the password itself as a parameter.
|
||||
|
||||
.PARAMETER SecretName
|
||||
Logical secret name (environment variable name), not a password or token.
|
||||
@ -671,10 +695,7 @@ function Invoke-ConfiguredPlugin {
|
||||
[psobject]$SharedSettings,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$EngineDirectory,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[bool]$ContinueOnError = $false
|
||||
[string]$EngineDirectory
|
||||
)
|
||||
|
||||
if (-not (Test-PluginRunnable -Plugin $Plugin -SharedSettings $SharedSettings -EngineDirectory $EngineDirectory -WriteLogs:$true)) {
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
Runs native CLIs (dotnet, git, helm, …) and keeps $LASTEXITCODE intact.
|
||||
By default throws on non-zero exit so callers cannot forget to check.
|
||||
Pass -ThrowOnError:$false when you need the exit code / output yourself
|
||||
(e.g. TestRunner Success objects, logging full container build output first).
|
||||
|
||||
Test hooks: Set-ExternalCommandTestHandler / Set-ExternalCommandAvailability
|
||||
let Pester stub CLIs without touching PATH.
|
||||
#>
|
||||
|
||||
$script:ExternalCommandTestHandler = $null
|
||||
$script:ExternalCommandAvailability = @{}
|
||||
|
||||
@ -40,7 +50,10 @@ function Invoke-ExternalCommand {
|
||||
|
||||
[string]$InputObject,
|
||||
|
||||
[switch]$MergeErrorOutput
|
||||
[switch]$MergeErrorOutput,
|
||||
|
||||
# Default true: fail fast. Soft callers (tests, nested loggers) pass $false.
|
||||
[bool]$ThrowOnError = $true
|
||||
)
|
||||
|
||||
$previousLocation = $null
|
||||
@ -51,6 +64,7 @@ function Invoke-ExternalCommand {
|
||||
|
||||
try {
|
||||
$effectiveWorkingDirectory = (Get-Location).Path
|
||||
$output = @()
|
||||
|
||||
if ($null -ne $script:ExternalCommandTestHandler) {
|
||||
$handlerResult = & $script:ExternalCommandTestHandler `
|
||||
@ -62,31 +76,46 @@ function Invoke-ExternalCommand {
|
||||
|
||||
$global:LASTEXITCODE = [int]$handlerResult.ExitCode
|
||||
if ($null -eq $handlerResult.Output) {
|
||||
return @()
|
||||
$output = @()
|
||||
}
|
||||
|
||||
if ($handlerResult.Output -is [System.Collections.IEnumerable] -and -not ($handlerResult.Output -is [string])) {
|
||||
return @($handlerResult.Output)
|
||||
elseif ($handlerResult.Output -is [System.Collections.IEnumerable] -and -not ($handlerResult.Output -is [string])) {
|
||||
$output = @($handlerResult.Output)
|
||||
}
|
||||
else {
|
||||
$output = @([string]$handlerResult.Output)
|
||||
}
|
||||
|
||||
return @([string]$handlerResult.Output)
|
||||
}
|
||||
|
||||
if ($script:ExternalCommandAvailability.ContainsKey($Name) -and -not $script:ExternalCommandAvailability[$Name]) {
|
||||
throw "External command '$Name' is marked unavailable."
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($InputObject)) {
|
||||
$output = $InputObject | & $Name @ArgumentList 2>&1
|
||||
}
|
||||
elseif ($MergeErrorOutput) {
|
||||
$output = & $Name @ArgumentList 2>&1
|
||||
}
|
||||
else {
|
||||
$output = & $Name @ArgumentList
|
||||
if ($script:ExternalCommandAvailability.ContainsKey($Name) -and -not $script:ExternalCommandAvailability[$Name]) {
|
||||
throw "External command '$Name' is marked unavailable."
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($InputObject)) {
|
||||
$raw = $InputObject | & $Name @ArgumentList 2>&1
|
||||
}
|
||||
elseif ($MergeErrorOutput) {
|
||||
$raw = & $Name @ArgumentList 2>&1
|
||||
}
|
||||
else {
|
||||
$raw = & $Name @ArgumentList
|
||||
}
|
||||
|
||||
$output = @($raw)
|
||||
}
|
||||
|
||||
return @($output)
|
||||
$exitCode = [int]$global:LASTEXITCODE
|
||||
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 ' '
|
||||
if ([string]::IsNullOrWhiteSpace($preview)) {
|
||||
throw "External command '$Name' failed with exit code $exitCode."
|
||||
}
|
||||
|
||||
throw "External command '$Name' failed with exit code $exitCode. $preview"
|
||||
}
|
||||
|
||||
return $output
|
||||
}
|
||||
finally {
|
||||
if ($null -ne $previousLocation) {
|
||||
|
||||
@ -53,21 +53,18 @@ function Invoke-GitInternal {
|
||||
|
||||
if (-not (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue)) {
|
||||
$srcDir = Split-Path $PSScriptRoot -Parent
|
||||
$externalCandidates = @(
|
||||
(Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1'),
|
||||
(Join-Path $srcDir 'plugins' 'Shared' 'ExternalCommandSupport.psm1')
|
||||
)
|
||||
foreach ($externalModule in $externalCandidates) {
|
||||
if (Test-Path -LiteralPath $externalModule -PathType Leaf) {
|
||||
Import-Module $externalModule -Global
|
||||
break
|
||||
}
|
||||
$externalModule = Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1'
|
||||
if (Test-Path -LiteralPath $externalModule -PathType Leaf) {
|
||||
Import-Module $externalModule -Global
|
||||
}
|
||||
elseif (Test-Path -LiteralPath (Join-Path $srcDir 'modules' 'ExternalCommandSupport.psm1') -PathType Leaf) {
|
||||
Import-Module (Join-Path $srcDir 'modules' 'ExternalCommandSupport.psm1') -Global
|
||||
}
|
||||
}
|
||||
|
||||
if ($CaptureOutput) {
|
||||
if (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue) {
|
||||
$output = Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput
|
||||
$output = Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput -ThrowOnError:$false
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
Write-Error "$ErrorMessage (exit code: $exitCode)"
|
||||
@ -96,7 +93,7 @@ function Invoke-GitInternal {
|
||||
}
|
||||
|
||||
if (Get-Command Invoke-ExternalCommand -ErrorAction SilentlyContinue) {
|
||||
Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput | Out-Null
|
||||
Invoke-ExternalCommand -Name git -ArgumentList $Arguments -MergeErrorOutput -ThrowOnError:$false | Out-Null
|
||||
}
|
||||
else {
|
||||
& git @Arguments
|
||||
|
||||
@ -19,10 +19,8 @@ function Import-ExternalCommandSupportInternal {
|
||||
return
|
||||
}
|
||||
|
||||
$srcDir = Split-Path $PSScriptRoot -Parent
|
||||
$candidates = @(
|
||||
(Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1'),
|
||||
(Join-Path $srcDir 'plugins' 'Shared' 'ExternalCommandSupport.psm1')
|
||||
(Join-Path $PSScriptRoot 'ExternalCommandSupport.psm1')
|
||||
)
|
||||
foreach ($modulePath in $candidates) {
|
||||
if (Test-Path -LiteralPath $modulePath -PathType Leaf) {
|
||||
@ -175,10 +173,10 @@ function Invoke-TestsWithCoverage {
|
||||
|
||||
Import-ExternalCommandSupportInternal
|
||||
if ($Silent) {
|
||||
$null = Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs -MergeErrorOutput
|
||||
$null = Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs -MergeErrorOutput -ThrowOnError:$false
|
||||
}
|
||||
else {
|
||||
Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs | Out-Default
|
||||
Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetArgs -ThrowOnError:$false | Out-Default
|
||||
}
|
||||
|
||||
$testExitCode = $LASTEXITCODE
|
||||
@ -356,10 +354,10 @@ function Invoke-NpmJestTestsWithCoverage {
|
||||
$npmArgs = @('run', $TestScript, '--', '--coverage', '--coverageReporters=json-summary', '--coverageReporters=text')
|
||||
Import-ExternalCommandSupportInternal
|
||||
if ($Silent) {
|
||||
$null = Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs -MergeErrorOutput
|
||||
$null = Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs -MergeErrorOutput -ThrowOnError:$false
|
||||
}
|
||||
else {
|
||||
Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs | Out-Default
|
||||
Invoke-ExternalCommand -Name npm -ArgumentList $npmArgs -ThrowOnError:$false | Out-Default
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
Pester test plugin for the RepoUtils test engine.
|
||||
|
||||
.DESCRIPTION
|
||||
Runs the community Pester suite and publishes normalized coverage metrics on the
|
||||
Runs the RepoUtils Pester suite and publishes normalized coverage metrics on the
|
||||
shared engine context for QualityGate.
|
||||
#>
|
||||
|
||||
|
||||
21
utils/templates/Dockerfile.ci.dotnet
Normal file
21
utils/templates/Dockerfile.ci.dotnet
Normal file
@ -0,0 +1,21 @@
|
||||
# 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"]
|
||||
22
utils/templates/Dockerfile.ci.npm
Normal file
22
utils/templates/Dockerfile.ci.npm
Normal file
@ -0,0 +1,22 @@
|
||||
# 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"]
|
||||
11
utils/templates/build/ci-dotnet-pack.sh
Normal file
11
utils/templates/build/ci-dotnet-pack.sh
Normal file
@ -0,0 +1,11 @@
|
||||
#!/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
|
||||
12
utils/templates/build/ci-npm-pack.sh
Normal file
12
utils/templates/build/ci-npm-pack.sh
Normal file
@ -0,0 +1,12 @@
|
||||
#!/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"
|
||||
@ -1,439 +0,0 @@
|
||||
#requires -Version 7.0
|
||||
#requires -PSEdition Core
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Refreshes a local maksit-repoutils copy from GitHub.
|
||||
|
||||
.DESCRIPTION
|
||||
This script clones the configured repository into a temporary directory,
|
||||
refreshes the parent directory of this script, preserves existing
|
||||
scriptSettings.json files in subfolders, and copies the cloned source contents into that parent directory.
|
||||
|
||||
All configuration is stored in scriptSettings.json.
|
||||
|
||||
.EXAMPLE
|
||||
pwsh -File .\Update-RepoUtils.ps1
|
||||
|
||||
.NOTES
|
||||
CONFIGURATION (scriptSettings.json):
|
||||
- dryRun: If true, logs the planned update without modifying files
|
||||
- repository.url: Git repository to clone
|
||||
- repository.sourceSubdirectory: Folder copied into the target directory
|
||||
- repository.preserveFileName: Existing file name to preserve in subfolders
|
||||
- repository.cloneDepth: Depth used for git clone
|
||||
- repository.skippedRelativeDirectories: Relative directories to exclude from phase-two refresh (preserve dest)
|
||||
- repository.omittedRelativeDirectories: Relative directories to delete from dest and never copy from source
|
||||
(product repos: ["tests"] — RepoUtils self-tests stay only in maksit-repoutils / enterprise)
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$ContinueAfterSelfUpdate,
|
||||
[string]$TargetDirectoryOverride,
|
||||
[string]$ClonedSourceDirectoryOverride,
|
||||
[string]$TemporaryRootOverride
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$srcDir = Split-Path (Split-Path $scriptDir -Parent) -Parent
|
||||
$modulesDir = Join-Path $srcDir 'modules'
|
||||
|
||||
# Refresh the src directory that contains modules, engines, plugins, and tools.
|
||||
$targetDirectory = if ([string]::IsNullOrWhiteSpace($TargetDirectoryOverride)) {
|
||||
$srcDir
|
||||
}
|
||||
else {
|
||||
[System.IO.Path]::GetFullPath($TargetDirectoryOverride)
|
||||
}
|
||||
$currentScriptPath = [System.IO.Path]::GetFullPath($MyInvocation.MyCommand.Path)
|
||||
$selfUpdateDirectory = [System.IO.Path]::Combine('tools', 'Update-RepoUtils')
|
||||
|
||||
function ConvertTo-NormalizedRelativePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$normalizedPath = $Path.Replace('/', [System.IO.Path]::DirectorySeparatorChar).Replace('\', [System.IO.Path]::DirectorySeparatorChar)
|
||||
return $normalizedPath.TrimStart('.', [System.IO.Path]::DirectorySeparatorChar).TrimEnd([System.IO.Path]::DirectorySeparatorChar)
|
||||
}
|
||||
|
||||
function Test-IsInRelativeDirectory {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RelativePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Directories
|
||||
)
|
||||
|
||||
$normalizedRelativePath = ConvertTo-NormalizedRelativePath -Path $RelativePath
|
||||
foreach ($directory in $Directories) {
|
||||
$normalizedDirectory = ConvertTo-NormalizedRelativePath -Path $directory
|
||||
if ([string]::IsNullOrWhiteSpace($normalizedDirectory)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
$normalizedRelativePath.Equals($normalizedDirectory, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
$normalizedRelativePath.StartsWith($normalizedDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-ReleaseDeployPreserveFiles {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TargetDirectory
|
||||
)
|
||||
|
||||
return @()
|
||||
}
|
||||
|
||||
function Add-PreservedFileBackup {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$PreservedFiles,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.IO.FileInfo]$File,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TargetDirectory,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$TemporaryRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[bool]$DryRun
|
||||
)
|
||||
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($TargetDirectory, $File.FullName)
|
||||
foreach ($existing in $PreservedFiles) {
|
||||
if ($existing.RelativePath.Equals($relativePath, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$backupPath = Join-Path $TemporaryRoot ("preserved-" + ($relativePath -replace '[\\/:*?""<>|]', '_'))
|
||||
$PreservedFiles.Add([pscustomobject]@{
|
||||
RelativePath = $relativePath
|
||||
BackupPath = $backupPath
|
||||
}) | Out-Null
|
||||
|
||||
if (-not $DryRun) {
|
||||
$backupDirectory = Split-Path -Parent $backupPath
|
||||
if (-not (Test-Path -Path $backupDirectory -PathType Container)) {
|
||||
New-Item -ItemType Directory -Path $backupDirectory -Force | Out-Null
|
||||
}
|
||||
|
||||
Copy-Item -Path $File.FullName -Destination $backupPath -Force
|
||||
}
|
||||
}
|
||||
|
||||
#region Import Modules
|
||||
|
||||
$scriptConfigModulePath = Join-Path $modulesDir "ScriptConfig.psm1"
|
||||
if (-not (Test-Path $scriptConfigModulePath)) {
|
||||
Write-Error "ScriptConfig module not found at: $scriptConfigModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$loggingModulePath = Join-Path $modulesDir "Logging.psm1"
|
||||
if (-not (Test-Path $loggingModulePath)) {
|
||||
Write-Error "Logging module not found at: $loggingModulePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Import-Module $scriptConfigModulePath -Force
|
||||
Import-Module $loggingModulePath -Force
|
||||
|
||||
#endregion
|
||||
|
||||
#region Load Settings
|
||||
|
||||
$settings = Get-ScriptSettings -ScriptDir $scriptDir
|
||||
|
||||
#endregion
|
||||
|
||||
#region Configuration
|
||||
|
||||
$repositoryUrl = $settings.repository.url
|
||||
$dryRun = if ($null -ne $settings.dryRun) { [bool]$settings.dryRun } else { $false }
|
||||
$sourceSubdirectory = if ($settings.repository.sourceSubdirectory) { $settings.repository.sourceSubdirectory } else { 'src' }
|
||||
$preserveFileName = if ($settings.repository.preserveFileName) { $settings.repository.preserveFileName } else { 'scriptSettings.json' }
|
||||
$cloneDepth = if ($settings.repository.cloneDepth) { [int]$settings.repository.cloneDepth } else { 1 }
|
||||
[string[]]$skippedRelativeDirectories = if ($settings.repository.skippedRelativeDirectories) {
|
||||
@(
|
||||
$settings.repository.skippedRelativeDirectories |
|
||||
ForEach-Object {
|
||||
ConvertTo-NormalizedRelativePath -Path ([string]$_)
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
@(
|
||||
[System.IO.Path]::Combine('engines', 'release', 'custom'),
|
||||
[System.IO.Path]::Combine('engines', 'test', 'custom')
|
||||
)
|
||||
}
|
||||
[string[]]$omittedRelativeDirectories = if ($settings.repository.omittedRelativeDirectories) {
|
||||
@(
|
||||
$settings.repository.omittedRelativeDirectories |
|
||||
ForEach-Object {
|
||||
ConvertTo-NormalizedRelativePath -Path ([string]$_)
|
||||
} |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
)
|
||||
}
|
||||
else {
|
||||
@()
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate CLI Dependencies
|
||||
|
||||
Assert-Command git
|
||||
Assert-Command pwsh
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($repositoryUrl)) {
|
||||
Write-Error "repository.url is required in scriptSettings.json."
|
||||
exit 1
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Main
|
||||
|
||||
Write-Log -Level "INFO" -Message "========================================"
|
||||
Write-Log -Level "INFO" -Message "Update RepoUtils Script"
|
||||
Write-Log -Level "INFO" -Message "========================================"
|
||||
Write-Log -Level "INFO" -Message "Target directory: $targetDirectory"
|
||||
Write-Log -Level "INFO" -Message "Dry run: $dryRun"
|
||||
|
||||
$ownsTemporaryRoot = [string]::IsNullOrWhiteSpace($TemporaryRootOverride)
|
||||
$temporaryRoot = if ($ownsTemporaryRoot) {
|
||||
Join-Path ([System.IO.Path]::GetTempPath()) ("maksit-repoutils-update-" + [System.Guid]::NewGuid().ToString('N'))
|
||||
}
|
||||
else {
|
||||
[System.IO.Path]::GetFullPath($TemporaryRootOverride)
|
||||
}
|
||||
|
||||
try {
|
||||
$clonedSourceDirectory = if ([string]::IsNullOrWhiteSpace($ClonedSourceDirectoryOverride)) {
|
||||
Write-LogStep "Cloning latest repository snapshot..."
|
||||
& git clone --depth $cloneDepth $repositoryUrl $temporaryRoot
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git clone failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Repository cloned"
|
||||
|
||||
Join-Path $temporaryRoot $sourceSubdirectory
|
||||
}
|
||||
else {
|
||||
[System.IO.Path]::GetFullPath($ClonedSourceDirectoryOverride)
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $clonedSourceDirectory -PathType Container)) {
|
||||
throw "The cloned repository does not contain the expected source directory: $clonedSourceDirectory"
|
||||
}
|
||||
|
||||
if (-not $ContinueAfterSelfUpdate) {
|
||||
if ($dryRun) {
|
||||
Write-LogStep "Dry run self-update summary"
|
||||
Write-Log -Level "INFO" -Message "Would refresh shared modules and $selfUpdateDirectory before relaunching the updater"
|
||||
}
|
||||
else {
|
||||
Write-LogStep "Refreshing updater files..."
|
||||
$selfUpdateFiles = Get-ChildItem -Path $clonedSourceDirectory -Recurse -Force -File |
|
||||
Where-Object {
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $_.FullName)
|
||||
$isRootFile = -not $relativePath.Contains([System.IO.Path]::DirectorySeparatorChar)
|
||||
$isUpdaterFile = $relativePath.StartsWith($selfUpdateDirectory + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
|
||||
$_.Name -ne $preserveFileName -and
|
||||
($isRootFile -or $isUpdaterFile)
|
||||
}
|
||||
|
||||
foreach ($sourceFile in $selfUpdateFiles) {
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $sourceFile.FullName)
|
||||
$destinationPath = Join-Path $targetDirectory $relativePath
|
||||
$destinationDirectory = Split-Path -Parent $destinationPath
|
||||
if (-not (Test-Path -Path $destinationDirectory -PathType Container)) {
|
||||
New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null
|
||||
}
|
||||
|
||||
Copy-Item -Path $sourceFile.FullName -Destination $destinationPath -Force
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message "Updater files refreshed"
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
Write-LogStep "Dry run bootstrap completed"
|
||||
Write-Log -Level "INFO" -Message "Continuing with phase two in the current process because no files were changed"
|
||||
}
|
||||
else {
|
||||
Write-LogStep "Relaunching the updated updater..."
|
||||
& pwsh -File $currentScriptPath `
|
||||
-ContinueAfterSelfUpdate `
|
||||
-TargetDirectoryOverride $targetDirectory `
|
||||
-ClonedSourceDirectoryOverride $clonedSourceDirectory `
|
||||
-TemporaryRootOverride $temporaryRoot
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Relaunched updater failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message "Bootstrap phase completed"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$preservedFiles = [System.Collections.ArrayList]@()
|
||||
[string[]]$updatePhaseSkippedDirectories = @($skippedRelativeDirectories) + $selfUpdateDirectory
|
||||
$existingPreservedFiles = Get-ChildItem -Path $targetDirectory -Recurse -File -Filter $preserveFileName -ErrorAction SilentlyContinue
|
||||
if ($existingPreservedFiles) {
|
||||
foreach ($file in $existingPreservedFiles) {
|
||||
Add-PreservedFileBackup -PreservedFiles $preservedFiles -File $file -TargetDirectory $targetDirectory -TemporaryRoot $temporaryRoot -DryRun $dryRun
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Preserved $($existingPreservedFiles.Count) existing $preserveFileName file(s)"
|
||||
}
|
||||
else {
|
||||
Write-Log -Level "WARN" -Message "No existing $preserveFileName files found in subfolders"
|
||||
}
|
||||
|
||||
$releaseDeployPreserveFiles = @(Get-ReleaseDeployPreserveFiles -TargetDirectory $targetDirectory)
|
||||
if ($releaseDeployPreserveFiles.Count -gt 0) {
|
||||
foreach ($file in $releaseDeployPreserveFiles) {
|
||||
Add-PreservedFileBackup -PreservedFiles $preservedFiles -File $file -TargetDirectory $targetDirectory -TemporaryRoot $temporaryRoot -DryRun $dryRun
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Preserved $($releaseDeployPreserveFiles.Count) release deploy file(s)"
|
||||
}
|
||||
|
||||
$preservedRelativePaths = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($preservedFile in $preservedFiles) {
|
||||
[void]$preservedRelativePaths.Add($preservedFile.RelativePath)
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
Write-LogStep "Dry run summary"
|
||||
Write-Log -Level "INFO" -Message "Would remove all files under target except preserved $preserveFileName and release deploy files"
|
||||
Write-Log -Level "INFO" -Message "Would skip phase-two refresh for: $($updatePhaseSkippedDirectories -join ', ')"
|
||||
if ($omittedRelativeDirectories.Count -gt 0) {
|
||||
Write-Log -Level "INFO" -Message "Would omit (delete dest + skip copy): $($omittedRelativeDirectories -join ', ')"
|
||||
}
|
||||
Write-Log -Level "INFO" -Message "Would copy refreshed files from: $clonedSourceDirectory"
|
||||
if ($preservedFiles.Count -gt 0) {
|
||||
$preservedList = ($preservedFiles | ForEach-Object { $_.RelativePath }) -join ", "
|
||||
Write-Log -Level "INFO" -Message "Would restore preserved files: $preservedList"
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Dry run completed. No files were modified."
|
||||
return
|
||||
}
|
||||
|
||||
Write-LogStep "Cleaning target directory..."
|
||||
$filesToRemove = Get-ChildItem -Path $targetDirectory -Recurse -Force -File |
|
||||
Where-Object {
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $_.FullName)
|
||||
$isInSkippedDirectory = Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $updatePhaseSkippedDirectories
|
||||
$isInOmittedDirectory = ($omittedRelativeDirectories.Count -gt 0) -and
|
||||
(Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $omittedRelativeDirectories)
|
||||
|
||||
$_.Name -ne $preserveFileName -and
|
||||
-not $preservedRelativePaths.Contains($relativePath) -and
|
||||
(-not $isInSkippedDirectory -or $isInOmittedDirectory)
|
||||
}
|
||||
|
||||
foreach ($file in $filesToRemove) {
|
||||
Remove-Item -Path $file.FullName -Force
|
||||
}
|
||||
|
||||
$directoriesToRemove = Get-ChildItem -Path $targetDirectory -Recurse -Force -Directory |
|
||||
Sort-Object { $_.FullName.Length } -Descending
|
||||
|
||||
foreach ($directory in $directoriesToRemove) {
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($targetDirectory, $directory.FullName)
|
||||
$isInOmittedDirectory = ($omittedRelativeDirectories.Count -gt 0) -and
|
||||
(Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $omittedRelativeDirectories)
|
||||
if ((Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $updatePhaseSkippedDirectories) -and
|
||||
-not $isInOmittedDirectory) {
|
||||
continue
|
||||
}
|
||||
|
||||
$remainingItems = Get-ChildItem -Path $directory.FullName -Force -ErrorAction SilentlyContinue
|
||||
if (-not $remainingItems) {
|
||||
Remove-Item -Path $directory.FullName -Force
|
||||
}
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Target directory cleaned"
|
||||
|
||||
Write-LogStep "Copying refreshed source files..."
|
||||
$sourceFilesToCopy = Get-ChildItem -Path $clonedSourceDirectory -Recurse -Force -File |
|
||||
Where-Object {
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $_.FullName)
|
||||
$isInSkippedDirectory = Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $updatePhaseSkippedDirectories
|
||||
$isInOmittedDirectory = ($omittedRelativeDirectories.Count -gt 0) -and
|
||||
(Test-IsInRelativeDirectory -RelativePath $relativePath -Directories $omittedRelativeDirectories)
|
||||
|
||||
-not $isInSkippedDirectory -and -not $isInOmittedDirectory
|
||||
}
|
||||
|
||||
foreach ($sourceFile in $sourceFilesToCopy) {
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($clonedSourceDirectory, $sourceFile.FullName)
|
||||
$destinationPath = Join-Path $targetDirectory $relativePath
|
||||
$destinationDirectory = Split-Path -Parent $destinationPath
|
||||
if (-not (Test-Path -Path $destinationDirectory -PathType Container)) {
|
||||
New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null
|
||||
}
|
||||
|
||||
Copy-Item -Path $sourceFile.FullName -Destination $destinationPath -Force
|
||||
}
|
||||
|
||||
foreach ($skippedDirectory in $updatePhaseSkippedDirectories) {
|
||||
$skippedSourcePath = Join-Path $clonedSourceDirectory $skippedDirectory
|
||||
if (Test-Path -Path $skippedSourcePath) {
|
||||
Write-Log -Level "INFO" -Message "Skipped refresh for $skippedDirectory"
|
||||
}
|
||||
}
|
||||
foreach ($omittedDirectory in $omittedRelativeDirectories) {
|
||||
Write-Log -Level "INFO" -Message "Omitted (not shipped): $omittedDirectory"
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Source files copied"
|
||||
|
||||
if ($preservedFiles.Count -gt 0) {
|
||||
foreach ($preservedFile in $preservedFiles) {
|
||||
if (-not (Test-Path -Path $preservedFile.BackupPath -PathType Leaf)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$restorePath = Join-Path $targetDirectory $preservedFile.RelativePath
|
||||
$restoreDirectory = Split-Path -Parent $restorePath
|
||||
if (-not (Test-Path -Path $restoreDirectory -PathType Container)) {
|
||||
New-Item -ItemType Directory -Path $restoreDirectory -Force | Out-Null
|
||||
}
|
||||
|
||||
Copy-Item -Path $preservedFile.BackupPath -Destination $restorePath -Force
|
||||
}
|
||||
Write-Log -Level "OK" -Message "Preserved files restored ($($preservedFiles.Count))"
|
||||
}
|
||||
|
||||
Write-Log -Level "OK" -Message "========================================"
|
||||
Write-Log -Level "OK" -Message "Update completed successfully!"
|
||||
Write-Log -Level "OK" -Message "========================================"
|
||||
}
|
||||
finally {
|
||||
if ($ownsTemporaryRoot -and (Test-Path -Path $temporaryRoot)) {
|
||||
Remove-Item -Path $temporaryRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft-07/schema",
|
||||
"title": "Update RepoUtils Script Settings",
|
||||
"description": "Configuration for the Update-RepoUtils utility.",
|
||||
"dryRun": false,
|
||||
"repository": {
|
||||
"url": "https://github.com/MAKS-IT-COM/maksit-repoutils.git",
|
||||
"sourceSubdirectory": "src",
|
||||
"preserveFileName": "scriptSettings.json",
|
||||
"cloneDepth": 1,
|
||||
"skippedRelativeDirectories": [
|
||||
"engines/release/custom",
|
||||
"engines/test/custom"
|
||||
],
|
||||
"omittedRelativeDirectories": [
|
||||
"tests"
|
||||
]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user