Skip to content

Create CIHelpers.psm1 shared module for CI platform detection and output #287

@WilliamBerryiii

Description

@WilliamBerryiii

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

  • CIHelpers.psm1 created at scripts/lib/Modules/CIHelpers.psm1
  • All exported functions have comment-based help
  • Functions support GitHub Actions, Azure DevOps, and local execution
  • Module passes PSScriptAnalyzer
  • Unit tests provide coverage for all exported functions

Metadata

Metadata

Assignees

No one assigned

    Labels

    ciContinuous integrationenhancementNew feature or requestgood first issueGood for newcomers

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions