From 3f7cbe3e0f91f6db3b5a694bd1872bd6e280da2a Mon Sep 17 00:00:00 2001 From: katriendg Date: Wed, 25 Mar 2026 11:06:07 +0000 Subject: [PATCH 1/3] docs(contributing): update prerequisites and contribution workflow dates and add Go testing instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - update ms.date to 2026-03-25 in prerequisites and contribution workflow - add Go and golangci-lint installation instructions - include Go testing command in contribution workflow 🔧 - Generated by Copilot --- .cspell/general-technical.txt | 1 + .github/copilot-instructions.md | 1 + docs/contributing/contribution-workflow.md | 3 ++- docs/contributing/prerequisites.md | 17 ++++++++++++++++- 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.cspell/general-technical.txt b/.cspell/general-technical.txt index b0753db0..539c3296 100644 --- a/.cspell/general-technical.txt +++ b/.cspell/general-technical.txt @@ -483,6 +483,7 @@ gitops gmail GMSH golang +golangci google googlecloud gpu diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3ff04595..e6aac16c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -179,6 +179,7 @@ Run `npm install` (or `npm ci`) before any `npm run` lint commands. `shellcheck` | `*.md` | `npm run lint:md`, `npm run spell-check`, `npm run format:tables` | | `*.tf`, `*.tfvars` | `npm run lint:tf`, `npm run lint:tf:validate`, `terraform plan` | | `*.tftest.hcl` | `npm run test:tf`, `cd infrastructure/terraform/modules/ && terraform test` or `cd infrastructure/terraform && terraform test` | +| `*.go` | `npm run test:go` (golangci-lint + `go test`) | | `*.sh` | `shellcheck ` | | `*.ps1` | `npm run lint:ps` | | `*.yml` (GitHub Actions) | `npm run lint:yaml` | diff --git a/docs/contributing/contribution-workflow.md b/docs/contributing/contribution-workflow.md index eaa3d116..a18ac16c 100644 --- a/docs/contributing/contribution-workflow.md +++ b/docs/contributing/contribution-workflow.md @@ -3,7 +3,7 @@ sidebar_position: 4 title: Contribution Workflow description: How to contribute including legal requirements, bug reports, enhancement suggestions, and documentation improvements author: Microsoft Robotics-AI Team -ms.date: 2026-03-18 +ms.date: 2026-03-25 ms.topic: how-to keywords: - contributing @@ -170,6 +170,7 @@ This reference architecture validates through deployment rather than automated t | Full infrastructure changes | Deployment testing in dev subscription with cost estimate and teardown confirmation | | Training scripts | AzureML job submission in test workspace with logs | | Workflow templates | Workflow execution validation with job outputs | +| Go modules | `npm run test:go` (golangci-lint + `go test`) | | Configuration manifests | Syntax validation, test deployment in non-production cluster | ### Testing Documentation diff --git a/docs/contributing/prerequisites.md b/docs/contributing/prerequisites.md index 299919b7..eae46574 100644 --- a/docs/contributing/prerequisites.md +++ b/docs/contributing/prerequisites.md @@ -3,7 +3,7 @@ sidebar_position: 6 title: Prerequisites and Build Validation description: Required tools, Azure access, NGC credentials, and build validation commands for contributing author: Microsoft Robotics-AI Team -ms.date: 2026-02-08 +ms.date: 2026-03-25 ms.topic: how-to keywords: - prerequisites @@ -33,6 +33,8 @@ Install these tools before contributing: | Python | 3.11+ | | | shellcheck | 0.10+ | | | uv | latest | | +| Go | 1.24+ | | +| golangci-lint | 2.11+ | | | Docker | latest | (with NVIDIA Container Toolkit) | | OSMO CLI | latest | | | hve-core | latest | | @@ -130,6 +132,12 @@ shellcheck --version # >= 0.10 # uv (Python package manager) uv --version +# Go +go version # >= 1.24 + +# golangci-lint +golangci-lint version # >= 2.11 + # Docker with NVIDIA Container Toolkit docker --version nvidia-ctk --version @@ -167,6 +175,13 @@ tflint --recursive infrastructure/terraform/ shellcheck deploy/**/*.sh scripts/**/*.sh ``` +**Go:** + +```bash +# Lint and test all Go modules (required for Go changes) +npm run test:go +``` + **Documentation:** ```bash From 6e465e8e816e986ae29fcef38edc8c13f4ad670a Mon Sep 17 00:00:00 2001 From: katriendg Date: Wed, 25 Mar 2026 12:57:05 +0100 Subject: [PATCH 2/3] feat(build): split Go CI into separate lint and test scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add Invoke-GoLint.ps1 with golangci-lint wrapper and SHA256-verified install - refactor Invoke-GoTest.ps1 to test-only, remove all lint logic - add go-lint.yml reusable workflow and wire into main.yml and pr-validation.yml - add lint:go npm script, update *.go quick reference in docs and copilot-instructions - add golangci to cspell dictionary 🔧 - Generated by Copilot --- .github/copilot-instructions.md | 2 +- .github/workflows/go-lint.yml | 62 +++++ .github/workflows/go-tests.yml | 2 +- .github/workflows/main.yml | 12 +- .github/workflows/pr-validation.yml | 12 +- docs/contributing/contribution-workflow.md | 2 +- docs/contributing/prerequisites.md | 5 +- package.json | 1 + shared/ci/linting/Invoke-GoLint.ps1 | 186 +++++++++++++++ shared/ci/linting/Invoke-GoTest.ps1 | 51 +--- .../ci/tests/linting/Invoke-GoLint.Tests.ps1 | 217 ++++++++++++++++++ .../ci/tests/linting/Invoke-GoTest.Tests.ps1 | 102 +------- 12 files changed, 505 insertions(+), 149 deletions(-) create mode 100644 .github/workflows/go-lint.yml create mode 100644 shared/ci/linting/Invoke-GoLint.ps1 create mode 100644 shared/ci/tests/linting/Invoke-GoLint.Tests.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e6aac16c..cc4753ed 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -179,7 +179,7 @@ Run `npm install` (or `npm ci`) before any `npm run` lint commands. `shellcheck` | `*.md` | `npm run lint:md`, `npm run spell-check`, `npm run format:tables` | | `*.tf`, `*.tfvars` | `npm run lint:tf`, `npm run lint:tf:validate`, `terraform plan` | | `*.tftest.hcl` | `npm run test:tf`, `cd infrastructure/terraform/modules/ && terraform test` or `cd infrastructure/terraform && terraform test` | -| `*.go` | `npm run test:go` (golangci-lint + `go test`) | +| `*.go` | `npm run lint:go` (golangci-lint), `npm run test:go` (`go test`) | | `*.sh` | `shellcheck ` | | `*.ps1` | `npm run lint:ps` | | `*.yml` (GitHub Actions) | `npm run lint:yaml` | diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml new file mode 100644 index 00000000..944bf620 --- /dev/null +++ b/.github/workflows/go-lint.yml @@ -0,0 +1,62 @@ +name: Go Lint + +on: + workflow_call: + inputs: + soft-fail: + description: 'Whether to continue on Go lint failures' + required: false + type: boolean + default: false + changed-files-only: + description: 'Only lint when Go files changed' + required: false + type: boolean + default: false + +permissions: + contents: read + +defaults: + run: + shell: pwsh + +jobs: + go-lint: + name: Go Lint + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: ${{ inputs.changed-files-only && '0' || '1' }} + + - name: Create logs directory + run: New-Item -ItemType Directory -Force -Path logs | Out-Null + + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version-file: infrastructure/terraform/e2e/go.mod + cache-dependency-path: infrastructure/terraform/e2e/go.mod + + - name: Run Go lint + id: go-lint + continue-on-error: ${{ inputs.soft-fail }} + run: | + $params = @{} + if ('${{ inputs.changed-files-only }}' -eq 'true') { + $params['ChangedFilesOnly'] = $true + } + ./shared/ci/linting/Invoke-GoLint.ps1 @params + + - name: Upload Go lint results + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: go-lint-results + path: logs/go-lint-results.json + if-no-files-found: ignore diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml index fd817f64..a6b8bc28 100644 --- a/.github/workflows/go-tests.yml +++ b/.github/workflows/go-tests.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: soft-fail: - description: 'Whether to continue on Go test or lint failures' + description: 'Whether to continue on Go test failures' required: false type: boolean default: false diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08bb17d3..6e8f43a2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -162,7 +162,16 @@ jobs: contents: read id-token: write - # Go tests (golangci-lint + go test) + # Go linting using golangci-lint + go-lint: + name: Go Lint + uses: ./.github/workflows/go-lint.yml + with: + soft-fail: true + permissions: + contents: read + + # Go tests go-tests: name: Go Tests uses: ./.github/workflows/go-tests.yml @@ -202,6 +211,7 @@ jobs: - terraform-lint - terraform-validation - terraform-tests + - go-lint - go-tests - codeql-analysis name: Release Please diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index af0f8014..73bcdc3b 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -184,7 +184,17 @@ jobs: contents: read id-token: write - # Go tests (golangci-lint + go test) + # Go linting using golangci-lint + go-lint: + name: Go Lint + uses: ./.github/workflows/go-lint.yml + with: + soft-fail: true + changed-files-only: true + permissions: + contents: read + + # Go tests go-tests: name: Go Tests uses: ./.github/workflows/go-tests.yml diff --git a/docs/contributing/contribution-workflow.md b/docs/contributing/contribution-workflow.md index a18ac16c..f2c58874 100644 --- a/docs/contributing/contribution-workflow.md +++ b/docs/contributing/contribution-workflow.md @@ -170,7 +170,7 @@ This reference architecture validates through deployment rather than automated t | Full infrastructure changes | Deployment testing in dev subscription with cost estimate and teardown confirmation | | Training scripts | AzureML job submission in test workspace with logs | | Workflow templates | Workflow execution validation with job outputs | -| Go modules | `npm run test:go` (golangci-lint + `go test`) | +| Go modules | `npm run lint:go` (golangci-lint), `npm run test:go` (`go test`) | | Configuration manifests | Syntax validation, test deployment in non-production cluster | ### Testing Documentation diff --git a/docs/contributing/prerequisites.md b/docs/contributing/prerequisites.md index eae46574..e1752c76 100644 --- a/docs/contributing/prerequisites.md +++ b/docs/contributing/prerequisites.md @@ -178,7 +178,10 @@ shellcheck deploy/**/*.sh scripts/**/*.sh **Go:** ```bash -# Lint and test all Go modules (required for Go changes) +# Lint Go modules (required for Go changes) +npm run lint:go + +# Test Go modules (required for Go changes) npm run test:go ``` diff --git a/package.json b/package.json index 863b1343..47c51c7f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "format:tables": "markdown-table-formatter \"**/*.md\"", "test:ps": "pwsh -File ./shared/ci/tests/Invoke-PesterTests.ps1", "test:tf": "pwsh -File shared/ci/linting/Invoke-TerraformTest.ps1", + "lint:go": "pwsh -File shared/ci/linting/Invoke-GoLint.ps1", "test:go": "pwsh -File shared/ci/linting/Invoke-GoTest.ps1", "prepare": "husky", "docs:build": "cd docs/docusaurus && npm run build", diff --git a/shared/ci/linting/Invoke-GoLint.ps1 b/shared/ci/linting/Invoke-GoLint.ps1 new file mode 100644 index 00000000..c7ebfbd4 --- /dev/null +++ b/shared/ci/linting/Invoke-GoLint.ps1 @@ -0,0 +1,186 @@ +#!/usr/bin/env pwsh +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT + +#Requires -Version 7.0 + +<# +.SYNOPSIS + Runs golangci-lint across Go modules in the repository. +.DESCRIPTION + Verifies golangci-lint is available (installing if needed via SHA256-verified binary), + runs golangci-lint run, and writes JSON output to logs/. Reports violations via CI + annotations and generates a GitHub step summary. +.PARAMETER OutputPath + Path for JSON results. Defaults to logs/go-lint-results.json. +.PARAMETER GoModuleDir + Directory containing the Go module to lint. Defaults to infrastructure/terraform/e2e. +.PARAMETER ChangedFilesOnly + When set, only run lint if Go-related files have changed. +#> + +[CmdletBinding()] +param( + [string]$OutputPath, + [string]$GoModuleDir, + [switch]$ChangedFilesOnly +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Import-Module (Join-Path $PSScriptRoot "Modules/LintingHelpers.psm1") -Force +Import-Module (Join-Path $PSScriptRoot "../../../scripts/lib/Modules/CIHelpers.psm1") -Force + +function Write-EmptyLintResults { + [CmdletBinding()] + param( + [string]$OutputPath, + [string]$SummaryMessage + ) + + $results = @{ + timestamp = (Get-Date -Format 'o') + golangci_lint_version = '' + lint_passed = $true + violation_count = 0 + summary = @{ + overall_passed = $true + } + } + + $results | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding utf8 + Write-Host "Results written to $OutputPath" + + $summaryContent = @( + '### Go Lint Results' + '' + $SummaryMessage + ) -join "`n" + Write-CIStepSummary -Content $summaryContent + Write-Host $summaryContent +} + +function Invoke-GoLintCore { + [CmdletBinding()] + param( + [string]$OutputPath, + [string]$GoModuleDir, + [switch]$ChangedFilesOnly + ) + + $repoRoot = & git rev-parse --show-toplevel 2>$null + if (-not $repoRoot) { + $repoRoot = (Get-Item $PSScriptRoot).Parent.Parent.Parent.FullName + } + + if (-not $OutputPath) { $OutputPath = Join-Path $repoRoot 'logs/go-lint-results.json' } + if (-not $GoModuleDir) { $GoModuleDir = Join-Path $repoRoot 'infrastructure/terraform/e2e' } + + $outputDir = Split-Path $OutputPath -Parent + if (-not (Test-Path $outputDir)) { + New-Item -ItemType Directory -Force -Path $outputDir | Out-Null + } + + # Guard: go.mod must exist + $goModPath = Join-Path $GoModuleDir 'go.mod' + if (-not (Test-Path $goModPath)) { + Write-Host "No go.mod found in $GoModuleDir — skipping lint" + Write-EmptyLintResults -OutputPath $OutputPath -SummaryMessage 'No `go.mod` found — nothing to lint.' + return 0 + } + + # Guard: ChangedFilesOnly + if ($ChangedFilesOnly) { + $changedFiles = @(Get-ChangedFilesFromGit -FileExtensions @('*.go', 'go.mod', 'go.sum')) + if ($changedFiles.Count -eq 0) { + Write-Host 'No Go files changed — skipping lint' + Write-EmptyLintResults -OutputPath $OutputPath -SummaryMessage 'No Go files changed — skipping lint.' + return 0 + } + } + + # Check golangci-lint on PATH; install if missing via SHA256-verified binary download + if (-not (Get-Command golangci-lint -ErrorAction SilentlyContinue)) { + Write-Host 'golangci-lint not found — installing via SHA256-verified binary...' + $lintInstallVersion = '2.11.4' + $lintExpectedSHA256 = '200c5b7503f67b59a6743ccf32133026c174e272b930ee79aa2aa6f37aca7ef1' + $lintUrl = "https://github.com/golangci/golangci-lint/releases/download/v${lintInstallVersion}/golangci-lint-${lintInstallVersion}-linux-amd64.tar.gz" + $lintTarball = '/tmp/golangci-lint.tar.gz' + $goPathBin = (& go env GOPATH) + '/bin' + + & bash -c "set -euo pipefail && curl -fsSL -o '${lintTarball}' '${lintUrl}' && echo '${lintExpectedSHA256} ${lintTarball}' | sha256sum -c --quiet - && mkdir -p '${goPathBin}' && tar -xzf '${lintTarball}' -C '${goPathBin}' --strip-components=1 'golangci-lint-${lintInstallVersion}-linux-amd64/golangci-lint' && rm -f '${lintTarball}'" + if ($LASTEXITCODE -ne 0) { + Write-CIAnnotation -Level Error -Message 'Failed to install golangci-lint' + return 1 + } + $env:PATH = $goPathBin + [IO.Path]::PathSeparator + $env:PATH + if (-not (Get-Command golangci-lint -ErrorAction SilentlyContinue)) { + Write-CIAnnotation -Level Error -Message 'golangci-lint not available after install attempt' + return 1 + } + } + + # Capture version + $lintVersionOutput = & golangci-lint version 2>$null + $lintVersion = if ($lintVersionOutput -match 'v([\d.]+)') { $Matches[0] } else { 'unknown' } + + # Run golangci-lint + $lintPassed = $true + Push-Location $GoModuleDir + try { + $lintOutput = & golangci-lint run './...' 2>&1 + if ($LASTEXITCODE -ne 0) { + $lintPassed = $false + Write-CIAnnotation -Level Error -Message "golangci-lint failed in $GoModuleDir" + } + + $violationCount = 0 + if (-not $lintPassed -and $lintOutput) { + $outputLines = @($lintOutput | ForEach-Object { $_.ToString() } | Where-Object { $_ -match '^\S+:\d+' }) + $violationCount = $outputLines.Count + } + + $results = @{ + timestamp = (Get-Date -Format 'o') + golangci_lint_version = $lintVersion + lint_passed = $lintPassed + violation_count = $violationCount + summary = @{ + overall_passed = $lintPassed + } + } + + $results | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding utf8 + Write-Host "Results written to $OutputPath" + + # Step summary + $status = if ($lintPassed) { '✅ Passed' } else { "❌ Failed ($violationCount violation(s))" } + $summaryContent = @( + '### Go Lint Results' + '' + "**golangci-lint:** $status" + ) -join "`n" + Write-CIStepSummary -Content $summaryContent + Write-Host $summaryContent + } + finally { + Pop-Location + } + + if ($lintPassed) { return 0 } else { return 1 } +} + +#region Main Execution +if ($MyInvocation.InvocationName -ne '.') { + try { + $exitCode = Invoke-GoLintCore @PSBoundParameters + exit $exitCode + } + catch { + Write-Error -ErrorAction Continue "Invoke-GoLint failed: $($_.Exception.Message)" + Write-CIAnnotation -Level Error -Message $_.Exception.Message + exit 1 + } +} +#endregion Main Execution diff --git a/shared/ci/linting/Invoke-GoTest.ps1 b/shared/ci/linting/Invoke-GoTest.ps1 index 55cb231a..f2871975 100644 --- a/shared/ci/linting/Invoke-GoTest.ps1 +++ b/shared/ci/linting/Invoke-GoTest.ps1 @@ -6,11 +6,11 @@ <# .SYNOPSIS - Runs Go tests and golangci-lint for Go modules in the repository. + Runs Go tests for Go modules in the repository. .DESCRIPTION - Verifies Go and golangci-lint are available, runs golangci-lint run and go test -json, - parses results, and writes JSON output to logs/. Reports failures via CI annotations - and generates a GitHub step summary. + Verifies Go is available, runs go test -json, parses results, and writes JSON + output to logs/. Reports failures via CI annotations and generates a GitHub + step summary. .PARAMETER OutputPath Path for JSON results. Defaults to logs/go-test-results.json. .PARAMETER CoverageOutput @@ -45,8 +45,6 @@ function Write-EmptyResults { $results = @{ timestamp = (Get-Date -Format 'o') go_version = '' - golangci_lint_version = '' - lint_passed = $true packages = @() summary = @{ packages_tested = 0 @@ -119,44 +117,12 @@ function Invoke-GoTestCore { return 1 } - # Check golangci-lint on PATH; install if missing via SHA256-verified binary download - if (-not (Get-Command golangci-lint -ErrorAction SilentlyContinue)) { - Write-Host 'golangci-lint not found — installing via SHA256-verified binary...' - $lintInstallVersion = '2.11.4' - $lintExpectedSHA256 = '200c5b7503f67b59a6743ccf32133026c174e272b930ee79aa2aa6f37aca7ef1' - $lintUrl = "https://github.com/golangci/golangci-lint/releases/download/v${lintInstallVersion}/golangci-lint-${lintInstallVersion}-linux-amd64.tar.gz" - $lintTarball = '/tmp/golangci-lint.tar.gz' - $goPathBin = (& go env GOPATH) + '/bin' - - & bash -c "set -euo pipefail && curl -fsSL -o '${lintTarball}' '${lintUrl}' && echo '${lintExpectedSHA256} ${lintTarball}' | sha256sum -c --quiet - && mkdir -p '${goPathBin}' && tar -xzf '${lintTarball}' -C '${goPathBin}' --strip-components=1 'golangci-lint-${lintInstallVersion}-linux-amd64/golangci-lint' && rm -f '${lintTarball}'" - if ($LASTEXITCODE -ne 0) { - Write-CIAnnotation -Level Error -Message 'Failed to install golangci-lint' - return 1 - } - $env:PATH = $goPathBin + [IO.Path]::PathSeparator + $env:PATH - if (-not (Get-Command golangci-lint -ErrorAction SilentlyContinue)) { - Write-CIAnnotation -Level Error -Message 'golangci-lint not available after install attempt' - return 1 - } - } - - # Capture versions + # Capture version $goVersionOutput = & go version 2>$null $goVersion = if ($goVersionOutput -match 'go([\d.]+)') { $Matches[0] } else { 'unknown' } - $lintVersionOutput = & golangci-lint version 2>$null - $lintVersion = if ($lintVersionOutput -match 'v([\d.]+)') { $Matches[0] } else { 'unknown' } - - # Run golangci-lint - $lintPassed = $true Push-Location $GoTestDir try { - $null = & golangci-lint run './...' 2>&1 - if ($LASTEXITCODE -ne 0) { - $lintPassed = $false - Write-CIAnnotation -Level Error -Message "golangci-lint failed in $GoTestDir" - } - # Run go test $testOutput = & go test -race "-coverprofile=$CoverageOutput" -covermode=atomic -v -json './...' 2>&1 @@ -239,13 +205,11 @@ function Invoke-GoTestCore { $packagesPassed = @($packages | Where-Object { $_.failed -eq 0 }).Count $packagesSkipped = @($packages | Where-Object { $_.passed -eq 0 -and $_.failed -eq 0 -and $_.skipped -gt 0 }).Count - $overallPassed = $lintPassed -and ($totalFailed -eq 0) + $overallPassed = ($totalFailed -eq 0) $results = @{ timestamp = (Get-Date -Format 'o') go_version = $goVersion - golangci_lint_version = $lintVersion - lint_passed = $lintPassed packages = @($packages | ForEach-Object { @{ path = $_.path @@ -297,8 +261,7 @@ function Invoke-GoTestCore { } $summaryLines += '' - $lintStatus = if ($lintPassed) { '✅ Passed' } else { '❌ Failed' } - $summaryLines += "**Lint:** $lintStatus | **Total:** $totalPassed passed, $totalFailed failed, $totalSkipped skipped" + $summaryLines += "**Total:** $totalPassed passed, $totalFailed failed, $totalSkipped skipped" $summaryContent = $summaryLines -join "`n" Write-CIStepSummary -Content $summaryContent diff --git a/shared/ci/tests/linting/Invoke-GoLint.Tests.ps1 b/shared/ci/tests/linting/Invoke-GoLint.Tests.ps1 new file mode 100644 index 00000000..773860fb --- /dev/null +++ b/shared/ci/tests/linting/Invoke-GoLint.Tests.ps1 @@ -0,0 +1,217 @@ +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: MIT + +#Requires -Version 7.0 +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0' } + +# Stub functions for external tools trigger PSUseApprovedVerbs +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] +param() + +BeforeAll { + . $PSScriptRoot/../../linting/Invoke-GoLint.ps1 + $ErrorActionPreference = 'Continue' + Import-Module (Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1') -Force + function golangci-lint { } + function go { } +} + +Describe 'Invoke-GoLintCore' -Tag 'Unit' { + BeforeAll { Save-CIEnvironment } + AfterAll { Restore-CIEnvironment } + + BeforeEach { + $script:MockFiles = Initialize-MockCIEnvironment -Workspace $TestDrive + $script:TestOutputPath = Join-Path $TestDrive 'logs/go-lint-results.json' + $script:TestModuleDir = Join-Path $TestDrive 'infrastructure/terraform/e2e' + New-Item -ItemType Directory -Force -Path $script:TestModuleDir | Out-Null + New-Item -ItemType File -Force -Path (Join-Path $script:TestModuleDir 'go.mod') | Out-Null + + Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' } + Mock Write-CIAnnotation {} + Mock Write-CIStepSummary {} + + Mock golangci-lint { + $global:LASTEXITCODE = 0 + return 'golangci-lint has version v2.11.4' + } -ParameterFilter { $args[0] -eq 'version' } + + Mock golangci-lint { + $global:LASTEXITCODE = 0 + return '' + } -ParameterFilter { $args[0] -eq 'run' } + + Mock go { + $global:LASTEXITCODE = 0 + return '/usr/local/go/bin' + } -ParameterFilter { $args[0] -eq 'env' } + } + + AfterEach { + Restore-CIEnvironment + Remove-MockCIFiles -MockFiles $script:MockFiles + } + + Context 'tool availability' { + It 'Installs golangci-lint when not in PATH' { + $script:getLintCallCount = 0 + Mock Get-Command { + $script:getLintCallCount++ + if ($script:getLintCallCount -le 1) { return $null } + return @{ Source = '/usr/local/bin/golangci-lint' } + } -ParameterFilter { $Name -eq 'golangci-lint' } + Mock bash { $global:LASTEXITCODE = 0 } + + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + Should -Invoke bash -Times 1 + } + + It 'Returns 0 when lint passes after install' { + $script:getLintCallCount = 0 + Mock Get-Command { + $script:getLintCallCount++ + if ($script:getLintCallCount -le 1) { return $null } + return @{ Source = '/usr/local/bin/golangci-lint' } + } -ParameterFilter { $Name -eq 'golangci-lint' } + Mock bash { $global:LASTEXITCODE = 0 } + + $result = Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + $result | Should -Be 0 + } + + It 'Returns 1 when golangci-lint install fails' { + Mock Get-Command { return $null } -ParameterFilter { $Name -eq 'golangci-lint' } + Mock bash { $global:LASTEXITCODE = 1 } + $result = Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + $result | Should -Be 1 + } + + It 'Writes error annotation when install fails' { + Mock Get-Command { return $null } -ParameterFilter { $Name -eq 'golangci-lint' } + Mock bash { $global:LASTEXITCODE = 1 } + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + Should -Invoke Write-CIAnnotation -ParameterFilter { + $Level -eq 'Error' -and $Message -like '*golangci-lint*' + } + } + } + + Context 'go.mod guard' { + BeforeEach { + Remove-Item -Path (Join-Path $script:TestModuleDir 'go.mod') -Force + } + + It 'Returns 0 when go.mod does not exist' { + $result = Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + $result | Should -Be 0 + } + + It 'Writes empty results JSON when go.mod missing' { + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + $script:TestOutputPath | Should -Exist + $json = Get-Content $script:TestOutputPath -Raw | ConvertFrom-Json + $json.lint_passed | Should -BeTrue + $json.violation_count | Should -Be 0 + } + + It 'Writes step summary when go.mod missing' { + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + Should -Invoke Write-CIStepSummary -Times 1 + } + } + + Context 'ChangedFilesOnly' { + It 'Returns 0 early when no Go files changed' { + Mock Get-ChangedFilesFromGit { return @() } + $result = Invoke-GoLintCore -OutputPath $script:TestOutputPath ` + -GoModuleDir $script:TestModuleDir -ChangedFilesOnly + $result | Should -Be 0 + $json = Get-Content $script:TestOutputPath -Raw | ConvertFrom-Json + $json.summary.overall_passed | Should -BeTrue + } + + It 'Calls Get-ChangedFilesFromGit with correct extensions' { + Mock Get-ChangedFilesFromGit { return @() } + Invoke-GoLintCore -OutputPath $script:TestOutputPath ` + -GoModuleDir $script:TestModuleDir -ChangedFilesOnly + Should -Invoke Get-ChangedFilesFromGit -Times 1 -ParameterFilter { + ($FileExtensions -contains '*.go') -and ($FileExtensions -contains 'go.mod') + } + } + + It 'Runs lint when Go files have changed' { + Mock Get-ChangedFilesFromGit { + return @('infrastructure/terraform/e2e/main.go') + } + $result = Invoke-GoLintCore -OutputPath $script:TestOutputPath ` + -GoModuleDir $script:TestModuleDir -ChangedFilesOnly + $result | Should -Be 0 + Should -Invoke golangci-lint -ParameterFilter { $args[0] -eq 'run' } + } + } + + Context 'lint success' { + It 'Returns 0 when lint passes' { + $result = Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + $result | Should -Be 0 + } + + It 'JSON reports lint_passed as true' { + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + $json = Get-Content $script:TestOutputPath -Raw | ConvertFrom-Json + $json.lint_passed | Should -BeTrue + $json.violation_count | Should -Be 0 + } + + It 'JSON includes golangci_lint_version field' { + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + $json = Get-Content $script:TestOutputPath -Raw | ConvertFrom-Json + $json.golangci_lint_version | Should -Not -BeNullOrEmpty + } + + It 'Writes step summary' { + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + Should -Invoke Write-CIStepSummary -Times 1 + } + } + + Context 'lint failure' { + BeforeEach { + Mock golangci-lint { + $global:LASTEXITCODE = 1 + return 'main.go:10:5: unused variable (deadcode)' + } -ParameterFilter { $args[0] -eq 'run' } + } + + It 'Returns 1 when golangci-lint fails' { + $result = Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + $result | Should -Be 1 + } + + It 'Reports lint_passed false in JSON' { + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + $json = Get-Content $script:TestOutputPath -Raw | ConvertFrom-Json + $json.lint_passed | Should -BeFalse + } + + It 'Writes error annotation for lint failure' { + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + Should -Invoke Write-CIAnnotation -ParameterFilter { + $Level -eq 'Error' -and $Message -like '*golangci-lint*failed*' + } + } + } + + Context 'output file creation' { + It 'Creates output directory if it does not exist' { + $nestedOutput = Join-Path $TestDrive 'deep/nested/output.json' + Invoke-GoLintCore -OutputPath $nestedOutput -GoModuleDir $script:TestModuleDir + Split-Path $nestedOutput -Parent | Should -Exist + } + + It 'Writes valid JSON to output path' { + Invoke-GoLintCore -OutputPath $script:TestOutputPath -GoModuleDir $script:TestModuleDir + { Get-Content $script:TestOutputPath -Raw | ConvertFrom-Json } | Should -Not -Throw + } + } +} diff --git a/shared/ci/tests/linting/Invoke-GoTest.Tests.ps1 b/shared/ci/tests/linting/Invoke-GoTest.Tests.ps1 index 0e60a6cf..cb9acce1 100644 --- a/shared/ci/tests/linting/Invoke-GoTest.Tests.ps1 +++ b/shared/ci/tests/linting/Invoke-GoTest.Tests.ps1 @@ -13,7 +13,6 @@ BeforeAll { $ErrorActionPreference = 'Continue' Import-Module (Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1') -Force function go { } - function golangci-lint { } } Describe 'Invoke-GoTestCore' -Tag 'Unit' { @@ -37,25 +36,10 @@ Describe 'Invoke-GoTestCore' -Tag 'Unit' { return 'go version go1.26 linux/amd64' } -ParameterFilter { $args[0] -eq 'version' } - Mock golangci-lint { - $global:LASTEXITCODE = 0 - return 'golangci-lint has version v2.11.4' - } -ParameterFilter { $args[0] -eq 'version' } - - Mock golangci-lint { - $global:LASTEXITCODE = 0 - return '' - } -ParameterFilter { $args[0] -eq 'run' } - Mock go { $global:LASTEXITCODE = 0 return '' } -ParameterFilter { $args[0] -eq 'test' } - - Mock go { - $global:LASTEXITCODE = 0 - return '/usr/local/go/bin' - } -ParameterFilter { $args[0] -eq 'env' } } AfterEach { @@ -79,24 +63,6 @@ Describe 'Invoke-GoTestCore' -Tag 'Unit' { $Level -eq 'Error' -and $Message -like '*go*not*' } } - - It 'Returns 1 when golangci-lint not found and install fails' { - Mock Get-Command { return $null } -ParameterFilter { $Name -eq 'golangci-lint' } - Mock bash { $global:LASTEXITCODE = 1 } - $result = Invoke-GoTestCore -OutputPath $script:TestOutputPath ` - -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir - $result | Should -Be 1 - } - - It 'Writes error annotation when golangci-lint install fails' { - Mock Get-Command { return $null } -ParameterFilter { $Name -eq 'golangci-lint' } - Mock bash { $global:LASTEXITCODE = 1 } - Invoke-GoTestCore -OutputPath $script:TestOutputPath ` - -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir - Should -Invoke Write-CIAnnotation -ParameterFilter { - $Level -eq 'Error' -and $Message -like '*golangci-lint*' - } - } } Context 'go.mod guard' { @@ -141,15 +107,15 @@ Describe 'Invoke-GoTestCore' -Tag 'Unit' { $json.summary.overall_passed | Should -BeTrue } - It 'Reports lint passed with no tests' { + It 'Reports overall_passed true with no tests' { Invoke-GoTestCore -OutputPath $script:TestOutputPath ` -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir $json = Get-Content $script:TestOutputPath -Raw | ConvertFrom-Json - $json.lint_passed | Should -BeTrue + $json.summary.overall_passed | Should -BeTrue } } - Context 'lint success and test pass' { + Context 'test pass' { BeforeEach { Mock go { $global:LASTEXITCODE = 0 @@ -177,13 +143,6 @@ Describe 'Invoke-GoTestCore' -Tag 'Unit' { $json.summary.overall_passed | Should -BeTrue } - It 'Reports lint passed in JSON' { - Invoke-GoTestCore -OutputPath $script:TestOutputPath ` - -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir - $json = Get-Content $script:TestOutputPath -Raw | ConvertFrom-Json - $json.lint_passed | Should -BeTrue - } - It 'Captures Go version in JSON' { Invoke-GoTestCore -OutputPath $script:TestOutputPath ` -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir @@ -198,36 +157,6 @@ Describe 'Invoke-GoTestCore' -Tag 'Unit' { } } - Context 'lint failure' { - BeforeEach { - Mock golangci-lint { - $global:LASTEXITCODE = 1 - return 'some lint error' - } -ParameterFilter { $args[0] -eq 'run' } - } - - It 'Returns 1 when golangci-lint fails' { - $result = Invoke-GoTestCore -OutputPath $script:TestOutputPath ` - -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir - $result | Should -Be 1 - } - - It 'Reports lint_passed false in JSON' { - Invoke-GoTestCore -OutputPath $script:TestOutputPath ` - -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir - $json = Get-Content $script:TestOutputPath -Raw | ConvertFrom-Json - $json.lint_passed | Should -BeFalse - } - - It 'Writes error annotation for lint failure' { - Invoke-GoTestCore -OutputPath $script:TestOutputPath ` - -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir - Should -Invoke Write-CIAnnotation -ParameterFilter { - $Level -eq 'Error' -and $Message -like '*golangci-lint*failed*' - } - } - } - Context 'test failure' { BeforeEach { Mock go { @@ -350,29 +279,4 @@ Describe 'Invoke-GoTestCore' -Tag 'Unit' { $result | Should -Be 1 } } - - Context 'golangci-lint installation' { - It 'Calls install script via bash when lint not found' { - $script:getLintCallCount = 0 - Mock Get-Command { - $script:getLintCallCount++ - if ($script:getLintCallCount -le 1) { return $null } - return @{ Source = '/usr/local/bin/golangci-lint' } - } -ParameterFilter { $Name -eq 'golangci-lint' } - Mock bash { $global:LASTEXITCODE = 0 } - - Invoke-GoTestCore -OutputPath $script:TestOutputPath ` - -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir - Should -Invoke bash -Times 1 - } - - It 'Returns 1 when install fails' { - Mock Get-Command { return $null } -ParameterFilter { $Name -eq 'golangci-lint' } - Mock bash { $global:LASTEXITCODE = 1 } - - $exitCode = Invoke-GoTestCore -OutputPath $script:TestOutputPath ` - -CoverageOutput $script:TestCoveragePath -GoTestDir $script:TestGoDir - $exitCode | Should -Be 1 - } - } } From 1ac7c706fa268906a10ed803c6e65c98b957582a Mon Sep 17 00:00:00 2001 From: katriendg Date: Wed, 25 Mar 2026 13:09:19 +0100 Subject: [PATCH 3/3] feat(linting): add Go linting to npm linting scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - include lint:go in lint:all command - update Invoke-GoLint.ps1 shebang for consistency - refactor Invoke-GoTest.ps1 for improved readability 🔧 - Generated by Copilot --- .github/copilot-instructions.md | 2 +- package.json | 4 ++-- shared/ci/linting/Invoke-GoLint.ps1 | 2 +- shared/ci/linting/Invoke-GoTest.ps1 | 16 ++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cc4753ed..67c7913e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -189,7 +189,7 @@ Run `npm install` (or `npm ci`) before any `npm run` lint commands. `shellcheck` ### Linting -* `npm run lint:all` runs `lint:md` + `lint:ps` + `lint:links` + `lint:yaml` + `lint:tf` in sequence +* `npm run lint:all` runs `lint:md` + `lint:ps` + `lint:links` + `lint:yaml` + `lint:tf` + `lint:go` in sequence * `npm run spell-check` and `npm run format:tables` are NOT included in `lint:all` — run them separately * `npm run lint:md:fix` and `npm run format:tables` auto-fix markdown issues * `.copilot-tracking/` is excluded from markdown linting via `.markdownlint-cli2.jsonc` diff --git a/package.json b/package.json index 47c51c7f..9253b705 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,14 @@ "lint:md:fix": "markdownlint-cli2 \"**/*.md\" --fix", "lint:ps": "pwsh -File shared/ci/linting/Invoke-PSScriptAnalyzer.ps1", "lint:links": "pwsh -File shared/ci/linting/Invoke-LinkLanguageCheck.ps1", + "lint:go": "pwsh -File shared/ci/linting/Invoke-GoLint.ps1", "lint:yaml": "pwsh -File shared/ci/linting/Invoke-YamlLint.ps1", "lint:tf": "pwsh -File shared/ci/linting/Invoke-TFLint.ps1", "lint:tf:validate": "pwsh -File shared/ci/linting/Invoke-TerraformValidation.ps1", - "lint:all": "npm run lint:md && npm run lint:ps && npm run lint:links && npm run lint:yaml && npm run lint:tf", + "lint:all": "npm run lint:md && npm run lint:ps && npm run lint:links && npm run lint:yaml && npm run lint:tf && npm run lint:go", "format:tables": "markdown-table-formatter \"**/*.md\"", "test:ps": "pwsh -File ./shared/ci/tests/Invoke-PesterTests.ps1", "test:tf": "pwsh -File shared/ci/linting/Invoke-TerraformTest.ps1", - "lint:go": "pwsh -File shared/ci/linting/Invoke-GoLint.ps1", "test:go": "pwsh -File shared/ci/linting/Invoke-GoTest.ps1", "prepare": "husky", "docs:build": "cd docs/docusaurus && npm run build", diff --git a/shared/ci/linting/Invoke-GoLint.ps1 b/shared/ci/linting/Invoke-GoLint.ps1 index c7ebfbd4..b7fd47fc 100644 --- a/shared/ci/linting/Invoke-GoLint.ps1 +++ b/shared/ci/linting/Invoke-GoLint.ps1 @@ -1,4 +1,4 @@ -#!/usr/bin/env pwsh +#!/usr/bin/env pwsh # Copyright (c) Microsoft Corporation. # SPDX-License-Identifier: MIT diff --git a/shared/ci/linting/Invoke-GoTest.ps1 b/shared/ci/linting/Invoke-GoTest.ps1 index f2871975..f492f099 100644 --- a/shared/ci/linting/Invoke-GoTest.ps1 +++ b/shared/ci/linting/Invoke-GoTest.ps1 @@ -43,10 +43,10 @@ function Write-EmptyResults { ) $results = @{ - timestamp = (Get-Date -Format 'o') - go_version = '' - packages = @() - summary = @{ + timestamp = (Get-Date -Format 'o') + go_version = '' + packages = @() + summary = @{ packages_tested = 0 packages_passed = 0 packages_skipped = 0 @@ -208,9 +208,9 @@ function Invoke-GoTestCore { $overallPassed = ($totalFailed -eq 0) $results = @{ - timestamp = (Get-Date -Format 'o') - go_version = $goVersion - packages = @($packages | ForEach-Object { + timestamp = (Get-Date -Format 'o') + go_version = $goVersion + packages = @($packages | ForEach-Object { @{ path = $_.path passed = $_.passed @@ -220,7 +220,7 @@ function Invoke-GoTestCore { test_runs = @($_.test_runs) } }) - summary = @{ + summary = @{ packages_tested = $packages.Count packages_passed = $packagesPassed packages_skipped = $packagesSkipped