microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/1873-devcontainer

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/devcontainer/Test-DevcontainerLockfile.ps1

237lines · 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 devcontainer lockfile integrity and feature coverage.
9
10.DESCRIPTION
11 Checks that devcontainer-lock.json exists, all features have SHA-256 integrity
12 hashes and resolved references, and that every feature declared in devcontainer.json
13 is present in the lockfile. Outputs results as JSON and emits CI annotations for
14 any violations found.
15
16.PARAMETER RepoRoot
17 Root directory of the repository. Defaults to the git working tree root or the
18 script directory when not inside a git repository.
19
20.PARAMETER OutputPath
21 Path where validation results JSON should be saved. Defaults to
22 'logs/devcontainer-lockfile-results.json'.
23
24.PARAMETER FailOnViolation
25 Exit with code 1 when any validation check fails.
26
27.EXAMPLE
28 ./Test-DevcontainerLockfile.ps1
29 Validate lockfile in the current repository with default settings.
30
31.EXAMPLE
32 ./Test-DevcontainerLockfile.ps1 -FailOnViolation
33 Validate lockfile and exit with error code on failures.
34
35.NOTES
36 Runs via: npm run validate:devcontainer-lockfile
37#>
38
39[CmdletBinding()]
40param(
41 [Parameter(Mandatory = $false)]
42 [string]$RepoRoot = (git rev-parse --show-toplevel 2>$null),
43
44 [Parameter(Mandatory = $false)]
45 [string]$OutputPath = 'logs/devcontainer-lockfile-results.json',
46
47 [Parameter(Mandatory = $false)]
48 [switch]$FailOnViolation
49)
50
51if ([string]::IsNullOrWhiteSpace($RepoRoot)) { $RepoRoot = $PSScriptRoot }
52
53$ErrorActionPreference = 'Stop'
54
55Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force
56
57#region Functions
58
59function Test-LockfileExists {
60 [CmdletBinding()]
61 [OutputType([hashtable])]
62 param(
63 [Parameter(Mandatory = $true)]
64 [string]$RepoRoot
65 )
66
67 $lockfilePath = Join-Path $RepoRoot '.devcontainer/devcontainer-lock.json'
68 if (Test-Path $lockfilePath) {
69 return @{
70 Passed = $true
71 Message = "Lockfile exists at $lockfilePath"
72 }
73 }
74
75 return @{
76 Passed = $false
77 Message = "Lockfile not found at $lockfilePath"
78 }
79}
80
81function Test-FeatureIntegrity {
82 [CmdletBinding()]
83 [OutputType([hashtable])]
84 param(
85 [Parameter(Mandatory = $true)]
86 [string]$LockfilePath
87 )
88
89 $lockData = Get-Content -Path $LockfilePath -Raw | ConvertFrom-Json
90 $violations = @()
91
92 foreach ($feature in $lockData.features.PSObject.Properties) {
93 $name = $feature.Name
94 $value = $feature.Value
95
96 if (-not $value.resolved) {
97 $violations += "Feature '$name' is missing a resolved reference"
98 }
99 if (-not $value.integrity) {
100 $violations += "Feature '$name' is missing an integrity hash"
101 }
102 elseif (-not $value.integrity.StartsWith('sha256:')) {
103 $violations += "Feature '$name' has non-SHA-256 integrity: $($value.integrity)"
104 }
105 }
106
107 return @{
108 Passed = ($violations.Count -eq 0)
109 Violations = $violations
110 }
111}
112
113function Test-FeatureCoverage {
114 [CmdletBinding()]
115 [OutputType([hashtable])]
116 param(
117 [Parameter(Mandatory = $true)]
118 [string]$LockfilePath,
119
120 [Parameter(Mandatory = $true)]
121 [string]$ConfigPath
122 )
123
124 $lockData = Get-Content -Path $LockfilePath -Raw | ConvertFrom-Json
125 $configData = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
126
127 $lockKeys = @($lockData.features.PSObject.Properties |
128 ForEach-Object { $_.Name.ToLowerInvariant() })
129 $configKeys = @($configData.features.PSObject.Properties |
130 ForEach-Object { $_.Name.ToLowerInvariant() })
131
132 $missingKeys = @($configKeys | Where-Object { $_ -notin $lockKeys })
133
134 return @{
135 Passed = ($missingKeys.Count -eq 0)
136 MissingKeys = $missingKeys
137 }
138}
139
140function Invoke-LockfileValidation {
141 [CmdletBinding()]
142 [OutputType([hashtable])]
143 param(
144 [Parameter(Mandatory = $true)]
145 [string]$RepoRoot
146 )
147
148 $lockfilePath = Join-Path $RepoRoot '.devcontainer/devcontainer-lock.json'
149 $configPath = Join-Path $RepoRoot '.devcontainer/devcontainer.json'
150
151 $details = @()
152
153 # Check 1: Lockfile exists
154 $existsResult = Test-LockfileExists -RepoRoot $RepoRoot
155 $details += @{
156 CheckName = 'LockfileExists'
157 Passed = $existsResult.Passed
158 Message = $existsResult.Message
159 }
160
161 if (-not $existsResult.Passed) {
162 Write-CIAnnotation -Level Error -Message $existsResult.Message
163 return @{
164 TotalChecks = 1
165 PassedChecks = 0
166 FailedChecks = 1
167 Details = $details
168 }
169 }
170
171 # Check 2: Feature integrity
172 $integrityResult = Test-FeatureIntegrity -LockfilePath $lockfilePath
173 $details += @{
174 CheckName = 'FeatureIntegrity'
175 Passed = $integrityResult.Passed
176 Violations = $integrityResult.Violations
177 }
178
179 if (-not $integrityResult.Passed) {
180 foreach ($violation in $integrityResult.Violations) {
181 Write-CIAnnotation -Level Error -Message $violation
182 }
183 }
184
185 # Check 3: Feature coverage
186 $coverageResult = Test-FeatureCoverage -LockfilePath $lockfilePath -ConfigPath $configPath
187 $details += @{
188 CheckName = 'FeatureCoverage'
189 Passed = $coverageResult.Passed
190 MissingKeys = $coverageResult.MissingKeys
191 }
192
193 if (-not $coverageResult.Passed) {
194 foreach ($key in $coverageResult.MissingKeys) {
195 Write-CIAnnotation -Level Error -Message "Feature '$key' declared in devcontainer.json but missing from lockfile"
196 }
197 }
198
199 $passedCount = ($details | Where-Object { $_.Passed }).Count
200 $failedCount = ($details | Where-Object { -not $_.Passed }).Count
201
202 return @{
203 TotalChecks = $details.Count
204 PassedChecks = $passedCount
205 FailedChecks = $failedCount
206 Details = $details
207 }
208}
209
210#endregion Functions
211
212#region Main Execution
213
214if ($MyInvocation.InvocationName -ne '.') {
215 try {
216 New-Item -ItemType Directory -Force -Path (Split-Path $OutputPath -Parent) | Out-Null
217 $result = Invoke-LockfileValidation -RepoRoot $RepoRoot
218 $result | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath -Encoding UTF8
219
220 if ($result.FailedChecks -gt 0) {
221 Write-CIAnnotation -Level Error -Message "Devcontainer lockfile integrity check failed with $($result.FailedChecks) error(s)"
222 if ($FailOnViolation) {
223 exit 1
224 }
225 }
226 else {
227 Write-Host "[PASS] Lockfile covers all features with SHA-256 integrity"
228 }
229 exit 0
230 }
231 catch {
232 Write-Error -ErrorAction Continue "Test-DevcontainerLockfile failed: $($_.Exception.Message)"
233 exit 1
234 }
235}
236
237#endregion Main Execution
238