microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fix/dependabot-uuid-postcss-overrides

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/plugins/Validate-Marketplace.ps1

392lines · modecode

1#!/usr/bin/env pwsh
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4#Requires -Version 7.0
5
6<#
7.SYNOPSIS
8 Validates the marketplace.json manifest for Copilot CLI plugins.
9
10.DESCRIPTION
11 Reads .github/plugin/marketplace.json and validates JSON schema compliance,
12 plugin source directory existence, name-source consistency, version
13 consistency with the root package.json, and absence of path separators
14 in source values.
15
16.EXAMPLE
17 ./Validate-Marketplace.ps1 -OutputPath 'logs/marketplace-validation-results.json'
18#>
19
20[CmdletBinding()]
21param(
22 [Parameter(Mandatory = $false)]
23 [string]$OutputPath = 'logs/marketplace-validation-results.json'
24)
25
26$ErrorActionPreference = 'Stop'
27
28Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force
29
30#region Validation Helpers
31
32function Write-MarketplaceValidationReport {
33 <#
34 .SYNOPSIS
35 Writes marketplace validation results to a JSON report.
36
37 .PARAMETER RepoRoot
38 Absolute path to the repository root directory.
39
40 .PARAMETER OutputPath
41 Output report path, absolute or relative to RepoRoot.
42
43 .PARAMETER ErrorCount
44 Total number of validation errors.
45
46 .PARAMETER Results
47 Validation results grouped by plugin or manifest scope.
48
49 .OUTPUTS
50 [void]
51
52 .EXAMPLE
53 Write-MarketplaceValidationReport -RepoRoot $RepoRoot -OutputPath 'logs/marketplace-validation-results.json' -ErrorCount 0 -Results @()
54 #>
55 [CmdletBinding()]
56 param(
57 [Parameter(Mandatory = $true)]
58 [ValidateNotNullOrEmpty()]
59 [string]$RepoRoot,
60
61 [Parameter(Mandatory = $false)]
62 [string]$OutputPath = 'logs/marketplace-validation-results.json',
63
64 [Parameter(Mandatory = $true)]
65 [int]$ErrorCount,
66
67 [Parameter(Mandatory = $false)]
68 [array]$Results = @()
69 )
70
71 if ([string]::IsNullOrWhiteSpace($OutputPath)) {
72 return
73 }
74
75 $resolvedOutputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) {
76 $OutputPath
77 }
78 else {
79 Join-Path -Path $RepoRoot -ChildPath $OutputPath
80 }
81
82 $outputDirectory = Split-Path -Path $resolvedOutputPath -Parent
83 if (-not [string]::IsNullOrWhiteSpace($outputDirectory) -and -not (Test-Path -Path $outputDirectory -PathType Container)) {
84 New-Item -Path $outputDirectory -ItemType Directory -Force | Out-Null
85 }
86
87 $report = [ordered]@{
88 Timestamp = (Get-Date).ToUniversalTime().ToString('o')
89 ErrorCount = $ErrorCount
90 Results = @($Results)
91 }
92
93 $report | ConvertTo-Json -Depth 10 | Set-Content -Path $resolvedOutputPath -Encoding UTF8
94}
95
96function Test-PluginSourceDirectory {
97 <#
98 .SYNOPSIS
99 Validates that a plugin source directory exists under the plugins root.
100
101 .PARAMETER Source
102 Plugin source value from marketplace.json.
103
104 .PARAMETER PluginsRoot
105 Absolute path to the plugins directory.
106
107 .OUTPUTS
108 [string] Error message if directory not found, empty string if valid.
109 #>
110 [CmdletBinding()]
111 [OutputType([string])]
112 param(
113 [Parameter(Mandatory = $true)]
114 [string]$Source,
115
116 [Parameter(Mandatory = $true)]
117 [string]$PluginsRoot
118 )
119
120 $pluginDir = Join-Path -Path $PluginsRoot -ChildPath $Source
121 if (-not (Test-Path -Path $pluginDir -PathType Container)) {
122 return "plugin source directory not found: plugins/$Source"
123 }
124
125 return ''
126}
127
128function Test-PluginSourceFormat {
129 <#
130 .SYNOPSIS
131 Validates that a plugin source contains no path separators.
132
133 .PARAMETER Source
134 Plugin source value from marketplace.json.
135
136 .OUTPUTS
137 [string] Error message if source contains path separators, empty string if valid.
138 #>
139 [CmdletBinding()]
140 [OutputType([string])]
141 param(
142 [Parameter(Mandatory = $true)]
143 [string]$Source
144 )
145
146 if ($Source -match '[/\\]') {
147 return "plugin source '$Source' must not contain path separators"
148 }
149
150 if ($Source -match '^\./') {
151 return "plugin source '$Source' must not contain relative path prefix"
152 }
153
154 return ''
155}
156
157#endregion Validation Helpers
158
159#region Orchestration
160
161function Invoke-MarketplaceValidation {
162 <#
163 .SYNOPSIS
164 Validates the marketplace.json manifest.
165
166 .DESCRIPTION
167 Validates the marketplace manifest against its JSON schema and performs
168 cross-validation checks including source directory existence,
169 name-source consistency, version consistency, and source format.
170
171 .PARAMETER RepoRoot
172 Absolute path to the repository root directory.
173
174 .OUTPUTS
175 Hashtable with Success bool and ErrorCount int.
176 #>
177 [CmdletBinding()]
178 [OutputType([hashtable])]
179 param(
180 [Parameter(Mandatory = $true)]
181 [ValidateNotNullOrEmpty()]
182 [string]$RepoRoot,
183
184 [Parameter(Mandatory = $false)]
185 [string]$OutputPath = 'logs/marketplace-validation-results.json'
186 )
187
188 $manifestPath = Join-Path -Path $RepoRoot -ChildPath '.github' -AdditionalChildPath 'plugin', 'marketplace.json'
189
190 if (-not (Test-Path -Path $manifestPath)) {
191 Write-Host ' FAIL marketplace.json not found' -ForegroundColor Red
192 $results = @(
193 @{
194 PluginName = 'marketplace'
195 IsValid = $false
196 Errors = @('marketplace.json not found')
197 Warnings = @()
198 }
199 )
200 Write-MarketplaceValidationReport -RepoRoot $RepoRoot -OutputPath $OutputPath -ErrorCount 1 -Results $results
201 return @{ Success = $false; ErrorCount = 1 }
202 }
203
204 Write-Host 'Validating marketplace.json...'
205
206 $errors = @()
207 $results = @()
208
209 # Parse JSON
210 try {
211 $manifestContent = Get-Content -Path $manifestPath -Raw
212 $manifest = $manifestContent | ConvertFrom-Json -AsHashtable
213 }
214 catch {
215 $errors += "invalid JSON: $($_.Exception.Message)"
216 foreach ($err in $errors) {
217 Write-Host " x $err" -ForegroundColor Red
218 }
219 $results += @{
220 PluginName = 'marketplace'
221 IsValid = $false
222 Errors = @($errors)
223 Warnings = @()
224 }
225 Write-MarketplaceValidationReport -RepoRoot $RepoRoot -OutputPath $OutputPath -ErrorCount 1 -Results $results
226 return @{ Success = $false; ErrorCount = 1 }
227 }
228
229 # Required top-level fields
230 $requiredFields = @('name', 'metadata', 'owner', 'plugins')
231 foreach ($field in $requiredFields) {
232 if (-not $manifest.ContainsKey($field) -or $null -eq $manifest[$field]) {
233 $errors += "missing required field '$field'"
234 }
235 }
236
237 if ($errors.Count -gt 0) {
238 foreach ($err in $errors) {
239 Write-Host " x $err" -ForegroundColor Red
240 }
241 $results += @{
242 PluginName = 'marketplace'
243 IsValid = $false
244 Errors = @($errors)
245 Warnings = @()
246 }
247 Write-MarketplaceValidationReport -RepoRoot $RepoRoot -OutputPath $OutputPath -ErrorCount $errors.Count -Results $results
248 return @{ Success = $false; ErrorCount = $errors.Count }
249 }
250
251 # Metadata validation
252 $metadataRequired = @('description', 'version', 'pluginRoot')
253 foreach ($field in $metadataRequired) {
254 if (-not $manifest.metadata.ContainsKey($field) -or [string]::IsNullOrWhiteSpace([string]$manifest.metadata[$field])) {
255 $errors += "missing required metadata field '$field'"
256 }
257 }
258
259 # Owner validation
260 if (-not $manifest.owner.ContainsKey('name') -or [string]::IsNullOrWhiteSpace([string]$manifest.owner.name)) {
261 $errors += "missing required owner field 'name'"
262 }
263
264 # Version consistency with package.json
265 $packageJsonPath = Join-Path -Path $RepoRoot -ChildPath 'package.json'
266 $expectedVersion = $null
267 if (Test-Path -Path $packageJsonPath) {
268 $packageJson = Get-Content -Path $packageJsonPath -Raw | ConvertFrom-Json
269 $expectedVersion = $packageJson.version
270 if ($manifest.metadata.version -ne $expectedVersion) {
271 $errors += "metadata.version '$($manifest.metadata.version)' does not match package.json version '$expectedVersion'"
272 }
273 }
274
275 # Plugins validation
276 if ($manifest.plugins -isnot [array] -or $manifest.plugins.Count -eq 0) {
277 $errors += 'plugins array is empty or missing'
278 }
279 else {
280 $pluginsRoot = Join-Path -Path $RepoRoot -ChildPath 'plugins'
281 $seenNames = @{}
282
283 foreach ($plugin in $manifest.plugins) {
284 $pluginName = $plugin.name
285 $pluginErrors = @()
286 $pluginWarnings = @()
287
288 # Required plugin fields
289 $pluginRequired = @('name', 'source', 'description', 'version')
290 foreach ($field in $pluginRequired) {
291 if (-not $plugin.ContainsKey($field) -or [string]::IsNullOrWhiteSpace([string]$plugin[$field])) {
292 $pluginErrors += "missing required field '$field'"
293 }
294 }
295
296 # Duplicate name check
297 if ($seenNames.ContainsKey($pluginName)) {
298 $pluginErrors += "duplicate plugin name '$pluginName'"
299 }
300 else {
301 $seenNames[$pluginName] = $true
302 }
303
304 # Source format (no path separators)
305 if (-not [string]::IsNullOrWhiteSpace($plugin.source)) {
306 $formatError = Test-PluginSourceFormat -Source $plugin.source
307 if ($formatError) {
308 $pluginErrors += $formatError
309 }
310 }
311
312 # Source directory existence
313 if (-not [string]::IsNullOrWhiteSpace($plugin.source)) {
314 $dirError = Test-PluginSourceDirectory -Source $plugin.source -PluginsRoot $pluginsRoot
315 if ($dirError) {
316 $pluginErrors += $dirError
317 }
318 }
319
320 # Name-source consistency
321 if ($pluginName -ne $plugin.source) {
322 $pluginErrors += "name does not match source '$($plugin.source)'"
323 }
324
325 # Plugin version consistency
326 if ($expectedVersion -and $plugin.version -ne $expectedVersion) {
327 $pluginErrors += "version '$($plugin.version)' does not match package.json version '$expectedVersion'"
328 }
329
330 $results += @{
331 PluginName = $pluginName
332 IsValid = ($pluginErrors.Count -eq 0)
333 Errors = @($pluginErrors)
334 Warnings = @($pluginWarnings)
335 }
336
337 foreach ($pluginError in $pluginErrors) {
338 $errors += "plugin '$pluginName': $pluginError"
339 }
340 }
341 }
342
343 if ($errors.Count -gt 0 -and $results.Count -eq 0) {
344 $results += @{
345 PluginName = 'marketplace'
346 IsValid = $false
347 Errors = @($errors)
348 Warnings = @()
349 }
350 }
351
352 if ($errors.Count -gt 0) {
353 Write-Host " FAIL marketplace.json - $($errors.Count) error(s)" -ForegroundColor Red
354 foreach ($err in $errors) {
355 Write-Host " $err" -ForegroundColor Red
356 }
357 }
358 else {
359 Write-Host " OK marketplace.json ($($manifest.plugins.Count) plugins)"
360 }
361
362 Write-MarketplaceValidationReport -RepoRoot $RepoRoot -OutputPath $OutputPath -ErrorCount $errors.Count -Results $results
363
364 return @{
365 Success = ($errors.Count -eq 0)
366 ErrorCount = $errors.Count
367 }
368}
369
370#endregion Orchestration
371
372#region Main Execution
373if ($MyInvocation.InvocationName -ne '.') {
374 try {
375 $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
376 $RepoRoot = (Get-Item "$ScriptDir/../..").FullName
377
378 $result = Invoke-MarketplaceValidation -RepoRoot $RepoRoot -OutputPath $OutputPath
379
380 if (-not $result.Success) {
381 throw "Marketplace validation failed with $($result.ErrorCount) error(s)."
382 }
383
384 exit 0
385 }
386 catch {
387 Write-Error "Marketplace validation failed: $($_.Exception.Message)"
388 Write-CIAnnotation -Message $_.Exception.Message -Level Error
389 exit 1
390 }
391}
392#endregion
393