Summary
Create a new CIHelpers.psm1 module to centralize CI platform detection and output formatting, eliminating duplicate code across security and linting scripts.
Problem
Multiple scripts duplicate CI platform detection and output logic:
GitHub Actions output patterns (inline in scripts):
scripts/security/Test-DependencyPinning.ps1 lines 784-808:
if ($env:GITHUB_OUTPUT) {
"dependency-report=$ReportPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
"compliance-score=$($Report.ComplianceScore)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
}
if ($env:GITHUB_STEP_SUMMARY) {
Copy-Item -Path $summaryPath -Destination $env:GITHUB_STEP_SUMMARY -Force
}
scripts/extension/Package-Extension.ps1 lines 555-561:
if ($env:GITHUB_OUTPUT) {
"version=$packageVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
"vsix-file=$($vsixFile.Name)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
}
Azure DevOps output patterns:
scripts/security/Test-DependencyPinning.ps1 lines 813-822:
if ($env:TF_BUILD -eq 'True' -or $env:AZURE_PIPELINES -eq 'True') {
Write-Output "##vso[task.setvariable variable=dependencyReport;isOutput=true]$ReportPath"
Write-Output "##vso[task.setvariable variable=complianceScore;isOutput=true]$($Report.ComplianceScore)"
Write-Output "##vso[artifact.upload containerfolder=dependency-pinning;artifactname=dependency-pinning-report]$ReportPath"
}
scripts/security/Test-SHAStaleness.ps1 lines 709-718:
$Message = "##vso[task.logissue type=warning;sourcepath=$($Dep.File);][$($Dep.Severity)] $($Dep.Message)"
Write-Output "##vso[task.complete result=SucceededWithIssues]"
scripts/security/Update-ActionSHAPinning.ps1 lines 424-438:
Write-Output "##vso[task.logissue type=warning;sourcepath=$sourcePath]$message"
Write-Output "##vso[task.complete result=SucceededWithIssues]"
Existing pattern to follow:
scripts/linting/Modules/LintingHelpers.psm1 already provides GitHub-specific helpers:
Write-GitHubStepSummary
Set-GitHubOutput
Set-GitHubEnv
Write-GitHubAnnotation
Solution
Create scripts/lib/Modules/CIHelpers.psm1 with unified CI platform support.
Module Structure
# CIHelpers.psm1
#
# Purpose: Unified CI platform detection and output formatting
# Supports: GitHub Actions, Azure DevOps, local development
#region Platform Detection
function Get-CIPlatform {
<#
.SYNOPSIS
Detects the current CI platform.
.OUTPUTS
[string] 'GitHub', 'AzureDevOps', or 'Local'
#>
if ($env:GITHUB_ACTIONS -eq 'true') { return 'GitHub' }
if ($env:TF_BUILD -eq 'True' -or $env:AZURE_PIPELINES -eq 'True') { return 'AzureDevOps' }
return 'Local'
}
function Test-CIEnvironment {
<#
.SYNOPSIS
Returns true if running in any CI environment.
#>
return (Get-CIPlatform) -ne 'Local'
}
#endregion
#region Output Variables
function Set-CIOutput {
<#
.SYNOPSIS
Sets an output variable for the current CI platform.
.PARAMETER Name
Variable name.
.PARAMETER Value
Variable value.
.PARAMETER IsOutput
For Azure DevOps, marks as output variable (default: true).
#>
param(
[Parameter(Mandatory)]
[string]$Name,
[Parameter(Mandatory)]
[string]$Value,
[switch]$IsOutput
)
switch (Get-CIPlatform) {
'GitHub' {
if ($env:GITHUB_OUTPUT) {
"$Name=$Value" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding UTF8
}
}
'AzureDevOps' {
$outputFlag = if ($IsOutput) { ';isOutput=true' } else { '' }
Write-Output "##vso[task.setvariable variable=$Name$outputFlag]$Value"
}
'Local' {
Write-Verbose "CI Output: $Name=$Value"
}
}
}
#endregion
#region Step Summary
function Write-CIStepSummary {
<#
.SYNOPSIS
Writes content to the CI step summary.
.PARAMETER Content
Markdown content to append.
.PARAMETER Path
Path to a file containing summary content.
#>
param(
[Parameter(ParameterSetName='Content')]
[string]$Content,
[Parameter(ParameterSetName='File')]
[string]$Path
)
$summaryContent = if ($Path) { Get-Content -Path $Path -Raw } else { $Content }
switch (Get-CIPlatform) {
'GitHub' {
if ($env:GITHUB_STEP_SUMMARY) {
$summaryContent | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding UTF8
}
}
'AzureDevOps' {
Write-Output "##[section]$summaryContent"
}
'Local' {
Write-Host $summaryContent
}
}
}
#endregion
#region Logging and Annotations
function Write-CIAnnotation {
<#
.SYNOPSIS
Writes a CI annotation (warning/error) with optional file location.
#>
param(
[Parameter(Mandatory)]
[ValidateSet('warning', 'error', 'notice')]
[string]$Type,
[Parameter(Mandatory)]
[string]$Message,
[string]$File,
[int]$Line
)
switch (Get-CIPlatform) {
'GitHub' {
$location = if ($File) { "file=$File" + $(if ($Line) { ",line=$Line" }) } else { '' }
Write-Output "::$Type $location::$Message"
}
'AzureDevOps' {
$vsoType = if ($Type -eq 'notice') { 'info' } else { $Type }
$sourcePath = if ($File) { ";sourcepath=$File" } else { '' }
Write-Output "##vso[task.logissue type=$vsoType$sourcePath]$Message"
}
'Local' {
$color = switch ($Type) { 'error' { 'Red' } 'warning' { 'Yellow' } default { 'Cyan' } }
Write-Host "[$Type] $Message" -ForegroundColor $color
}
}
}
function Set-CITaskResult {
<#
.SYNOPSIS
Sets the CI task result status.
#>
param(
[Parameter(Mandatory)]
[ValidateSet('Succeeded', 'SucceededWithIssues', 'Failed')]
[string]$Result
)
switch (Get-CIPlatform) {
'GitHub' {
if ($Result -eq 'Failed') { exit 1 }
}
'AzureDevOps' {
Write-Output "##vso[task.complete result=$Result]"
}
}
}
#endregion
#region Artifact Upload
function Publish-CIArtifact {
<#
.SYNOPSIS
Uploads an artifact to the CI system.
#>
param(
[Parameter(Mandatory)]
[string]$Path,
[Parameter(Mandatory)]
[string]$Name,
[string]$ContainerFolder
)
switch (Get-CIPlatform) {
'GitHub' {
# GitHub Actions artifact upload requires actions/upload-artifact
Write-Verbose "GitHub artifact: $Name at $Path (use actions/upload-artifact)"
}
'AzureDevOps' {
$folder = if ($ContainerFolder) { "containerfolder=$ContainerFolder;" } else { '' }
Write-Output "##vso[artifact.upload ${folder}artifactname=$Name]$Path"
}
'Local' {
Write-Verbose "Artifact: $Name at $Path"
}
}
}
#endregion
Export-ModuleMember -Function @(
'Get-CIPlatform'
'Test-CIEnvironment'
'Set-CIOutput'
'Write-CIStepSummary'
'Write-CIAnnotation'
'Set-CITaskResult'
'Publish-CIArtifact'
)
File Location
Create at: scripts/lib/Modules/CIHelpers.psm1
This location:
- Places shared modules in
lib/Modules/ following existing convention
- Keeps CI helpers separate from linting-specific helpers
- Allows import via
Import-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1")
Validation
Create corresponding tests at scripts/tests/lib/CIHelpers.Tests.ps1 covering:
- Platform detection with mocked environment variables
- Output formatting for each platform
- Annotation formatting
Acceptance Criteria
Summary
Create a new
CIHelpers.psm1module to centralize CI platform detection and output formatting, eliminating duplicate code across security and linting scripts.Problem
Multiple scripts duplicate CI platform detection and output logic:
GitHub Actions output patterns (inline in scripts):
scripts/security/Test-DependencyPinning.ps1lines 784-808:scripts/extension/Package-Extension.ps1lines 555-561:Azure DevOps output patterns:
scripts/security/Test-DependencyPinning.ps1lines 813-822:scripts/security/Test-SHAStaleness.ps1lines 709-718:scripts/security/Update-ActionSHAPinning.ps1lines 424-438:Existing pattern to follow:
scripts/linting/Modules/LintingHelpers.psm1already provides GitHub-specific helpers:Write-GitHubStepSummarySet-GitHubOutputSet-GitHubEnvWrite-GitHubAnnotationSolution
Create
scripts/lib/Modules/CIHelpers.psm1with unified CI platform support.Module Structure
File Location
Create at:
scripts/lib/Modules/CIHelpers.psm1This location:
lib/Modules/following existing conventionImport-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1")Validation
Create corresponding tests at
scripts/tests/lib/CIHelpers.Tests.ps1covering:Acceptance Criteria
CIHelpers.psm1created atscripts/lib/Modules/CIHelpers.psm1