microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1d56d25494d03b3ff5b9bf68c8ec3e7e38d351d5

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/plugins/Validate-Marketplace.ps1

277lines · 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
18#>
19
20[CmdletBinding()]
21param()
22
23$ErrorActionPreference = 'Stop'
24
25Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force
26
27#region Validation Helpers
28
29function Test-PluginSourceDirectory {
30 <#
31 .SYNOPSIS
32 Validates that a plugin source directory exists under the plugins root.
33
34 .PARAMETER Source
35 Plugin source value from marketplace.json.
36
37 .PARAMETER PluginsRoot
38 Absolute path to the plugins directory.
39
40 .OUTPUTS
41 [string] Error message if directory not found, empty string if valid.
42 #>
43 [CmdletBinding()]
44 [OutputType([string])]
45 param(
46 [Parameter(Mandatory = $true)]
47 [string]$Source,
48
49 [Parameter(Mandatory = $true)]
50 [string]$PluginsRoot
51 )
52
53 $pluginDir = Join-Path -Path $PluginsRoot -ChildPath $Source
54 if (-not (Test-Path -Path $pluginDir -PathType Container)) {
55 return "plugin source directory not found: plugins/$Source"
56 }
57
58 return ''
59}
60
61function Test-PluginSourceFormat {
62 <#
63 .SYNOPSIS
64 Validates that a plugin source contains no path separators.
65
66 .PARAMETER Source
67 Plugin source value from marketplace.json.
68
69 .OUTPUTS
70 [string] Error message if source contains path separators, empty string if valid.
71 #>
72 [CmdletBinding()]
73 [OutputType([string])]
74 param(
75 [Parameter(Mandatory = $true)]
76 [string]$Source
77 )
78
79 if ($Source -match '[/\\]') {
80 return "plugin source '$Source' must not contain path separators"
81 }
82
83 if ($Source -match '^\./') {
84 return "plugin source '$Source' must not contain relative path prefix"
85 }
86
87 return ''
88}
89
90#endregion Validation Helpers
91
92#region Orchestration
93
94function Invoke-MarketplaceValidation {
95 <#
96 .SYNOPSIS
97 Validates the marketplace.json manifest.
98
99 .DESCRIPTION
100 Validates the marketplace manifest against its JSON schema and performs
101 cross-validation checks including source directory existence,
102 name-source consistency, version consistency, and source format.
103
104 .PARAMETER RepoRoot
105 Absolute path to the repository root directory.
106
107 .OUTPUTS
108 Hashtable with Success bool and ErrorCount int.
109 #>
110 [CmdletBinding()]
111 [OutputType([hashtable])]
112 param(
113 [Parameter(Mandatory = $true)]
114 [ValidateNotNullOrEmpty()]
115 [string]$RepoRoot
116 )
117
118 $manifestPath = Join-Path -Path $RepoRoot -ChildPath '.github' -AdditionalChildPath 'plugin', 'marketplace.json'
119
120 if (-not (Test-Path -Path $manifestPath)) {
121 Write-Host ' FAIL marketplace.json not found' -ForegroundColor Red
122 return @{ Success = $false; ErrorCount = 1 }
123 }
124
125 Write-Host 'Validating marketplace.json...'
126
127 $errorCount = 0
128 $errors = @()
129
130 # Parse JSON
131 try {
132 $manifestContent = Get-Content -Path $manifestPath -Raw
133 $manifest = $manifestContent | ConvertFrom-Json -AsHashtable
134 }
135 catch {
136 $errors += "invalid JSON: $($_.Exception.Message)"
137 foreach ($err in $errors) {
138 Write-Host " x $err" -ForegroundColor Red
139 }
140 return @{ Success = $false; ErrorCount = 1 }
141 }
142
143 # Required top-level fields
144 $requiredFields = @('name', 'metadata', 'owner', 'plugins')
145 foreach ($field in $requiredFields) {
146 if (-not $manifest.ContainsKey($field) -or $null -eq $manifest[$field]) {
147 $errors += "missing required field '$field'"
148 }
149 }
150
151 if ($errors.Count -gt 0) {
152 foreach ($err in $errors) {
153 Write-Host " x $err" -ForegroundColor Red
154 }
155 return @{ Success = $false; ErrorCount = $errors.Count }
156 }
157
158 # Metadata validation
159 $metadataRequired = @('description', 'version', 'pluginRoot')
160 foreach ($field in $metadataRequired) {
161 if (-not $manifest.metadata.ContainsKey($field) -or [string]::IsNullOrWhiteSpace([string]$manifest.metadata[$field])) {
162 $errors += "missing required metadata field '$field'"
163 }
164 }
165
166 # Owner validation
167 if (-not $manifest.owner.ContainsKey('name') -or [string]::IsNullOrWhiteSpace([string]$manifest.owner.name)) {
168 $errors += "missing required owner field 'name'"
169 }
170
171 # Version consistency with package.json
172 $packageJsonPath = Join-Path -Path $RepoRoot -ChildPath 'package.json'
173 $expectedVersion = $null
174 if (Test-Path -Path $packageJsonPath) {
175 $packageJson = Get-Content -Path $packageJsonPath -Raw | ConvertFrom-Json
176 $expectedVersion = $packageJson.version
177 if ($manifest.metadata.version -ne $expectedVersion) {
178 $errors += "metadata.version '$($manifest.metadata.version)' does not match package.json version '$expectedVersion'"
179 }
180 }
181
182 # Plugins validation
183 if ($manifest.plugins -isnot [array] -or $manifest.plugins.Count -eq 0) {
184 $errors += 'plugins array is empty or missing'
185 }
186 else {
187 $pluginsRoot = Join-Path -Path $RepoRoot -ChildPath 'plugins'
188 $seenNames = @{}
189
190 foreach ($plugin in $manifest.plugins) {
191 $pluginName = $plugin.name
192
193 # Required plugin fields
194 $pluginRequired = @('name', 'source', 'description', 'version')
195 foreach ($field in $pluginRequired) {
196 if (-not $plugin.ContainsKey($field) -or [string]::IsNullOrWhiteSpace([string]$plugin[$field])) {
197 $errors += "plugin '$pluginName': missing required field '$field'"
198 }
199 }
200
201 # Duplicate name check
202 if ($seenNames.ContainsKey($pluginName)) {
203 $errors += "duplicate plugin name '$pluginName'"
204 }
205 else {
206 $seenNames[$pluginName] = $true
207 }
208
209 # Source format (no path separators)
210 if (-not [string]::IsNullOrWhiteSpace($plugin.source)) {
211 $formatError = Test-PluginSourceFormat -Source $plugin.source
212 if ($formatError) {
213 $errors += "plugin '$pluginName': $formatError"
214 }
215 }
216
217 # Source directory existence
218 if (-not [string]::IsNullOrWhiteSpace($plugin.source)) {
219 $dirError = Test-PluginSourceDirectory -Source $plugin.source -PluginsRoot $pluginsRoot
220 if ($dirError) {
221 $errors += "plugin '$pluginName': $dirError"
222 }
223 }
224
225 # Name-source consistency
226 if ($pluginName -ne $plugin.source) {
227 $errors += "plugin '$pluginName': name does not match source '$($plugin.source)'"
228 }
229
230 # Plugin version consistency
231 if ($expectedVersion -and $plugin.version -ne $expectedVersion) {
232 $errors += "plugin '$pluginName': version '$($plugin.version)' does not match package.json version '$expectedVersion'"
233 }
234 }
235 }
236
237 $errorCount = $errors.Count
238
239 if ($errorCount -gt 0) {
240 Write-Host " FAIL marketplace.json - $errorCount error(s)" -ForegroundColor Red
241 foreach ($err in $errors) {
242 Write-Host " $err" -ForegroundColor Red
243 }
244 }
245 else {
246 Write-Host " OK marketplace.json ($($manifest.plugins.Count) plugins)"
247 }
248
249 return @{
250 Success = ($errorCount -eq 0)
251 ErrorCount = $errorCount
252 }
253}
254
255#endregion Orchestration
256
257#region Main Execution
258if ($MyInvocation.InvocationName -ne '.') {
259 try {
260 $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
261 $RepoRoot = (Get-Item "$ScriptDir/../..").FullName
262
263 $result = Invoke-MarketplaceValidation -RepoRoot $RepoRoot
264
265 if (-not $result.Success) {
266 throw "Marketplace validation failed with $($result.ErrorCount) error(s)."
267 }
268
269 exit 0
270 }
271 catch {
272 Write-Error "Marketplace validation failed: $($_.Exception.Message)"
273 Write-CIAnnotation -Message $_.Exception.Message -Level Error
274 exit 1
275 }
276}
277#endregion
278