diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d6ac7f63906..d8add188442 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "ghcr.io/devcontainers/features/go:1": { - "version": "1.24" + "version": "1.25" }, "ghcr.io/devcontainers/features/docker-in-docker:2.11.0": { "version": "latest", diff --git a/.devcontainer/light/devcontainer.json b/.devcontainer/light/devcontainer.json index a5ca6c919f3..c21d242d835 100644 --- a/.devcontainer/light/devcontainer.json +++ b/.devcontainer/light/devcontainer.json @@ -3,7 +3,7 @@ "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "ghcr.io/devcontainers/features/go:1": { - "version": "1.24" + "version": "1.25" }, "ghcr.io/devcontainers/features/docker-in-docker:2.11.0": { "version": "latest", diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 3d4ae5f156e..6bd0fc9d4bf 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -26,12 +26,12 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: "^1.24" + go-version: "^1.25" - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v7 + uses: golangci/golangci-lint-action@v8 with: - version: v2.0 + version: v2.4 args: -v --timeout 10m0s working-directory: cli/azd diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f12a99e1f9f..572b79d9340 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -38,7 +38,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "^1.24" + go-version: "^1.25" - name: Set up Node.js uses: actions/setup-node@v4 @@ -46,7 +46,7 @@ jobs: node-version: "20" - name: Install golangci-lint - run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.0.0 + run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.4.0 - name: Install cspell run: npm install -g cspell@8.13.1 diff --git a/cli/azd/CONTRIBUTING.md b/cli/azd/CONTRIBUTING.md index 404836e3e69..6008d756fb7 100644 --- a/cli/azd/CONTRIBUTING.md +++ b/cli/azd/CONTRIBUTING.md @@ -13,7 +13,7 @@ In general, to make contributions a smooth and easy experience, we encourage the Prerequisites: -- [Go](https://go.dev/dl/) 1.24 +- [Go](https://go.dev/dl/) 1.25 Build: diff --git a/cli/azd/ci-test.ps1 b/cli/azd/ci-test.ps1 index 5c5e3b6c2a9..c2f59857245 100644 --- a/cli/azd/ci-test.ps1 +++ b/cli/azd/ci-test.ps1 @@ -36,7 +36,7 @@ Write-Host "Running unit tests..." # --test.gocoverdir is currently a "under-the-cover" way to pass the coverage directory to a test binary # See https://github.com/golang/go/issues/51430#issuecomment-1344711300 # -# This may be improved in go1.21 with an official 'go test' flag. +# As of Go 1.25, it’s still an “under-the-hood” option. & $gotestsum -- ./... -short -v -cover -args --test.gocoverdir="$($unitCoverDir.FullName)" if ($LASTEXITCODE) { exit $LASTEXITCODE diff --git a/eng/pipelines/templates/stages/code-coverage-upload.yml b/eng/pipelines/templates/stages/code-coverage-upload.yml index e0a800d2348..84942e982fd 100644 --- a/eng/pipelines/templates/stages/code-coverage-upload.yml +++ b/eng/pipelines/templates/stages/code-coverage-upload.yml @@ -50,13 +50,13 @@ stages: go tool covdata textfmt -i=cover -o cover.out if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - go install github.com/axw/gocov/gocov@latest - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - go install github.com/AlekSi/gocov-xml@latest - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - - ~/go/bin/gocov convert cover.out | ~/go/bin/gocov-xml > coverage.xml - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + # Convert Go coverage to Cobertura XML format using custom script + Write-Host "Converting coverage to Cobertura XML format..." + pwsh -File "$(Build.SourcesDirectory)/eng/scripts/Convert-GoCoverageToCobertura.ps1" -CoverageFile cover.out -OutputFile coverage.xml -SourceRoot "$(Build.SourcesDirectory)" + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to convert coverage to XML" + exit $LASTEXITCODE + } displayName: Merge code coverage files - task: PublishCodeCoverageResults@1 diff --git a/eng/pipelines/templates/steps/setup-go.yml b/eng/pipelines/templates/steps/setup-go.yml index 7327ecf69b5..cd4375a9b15 100644 --- a/eng/pipelines/templates/steps/setup-go.yml +++ b/eng/pipelines/templates/steps/setup-go.yml @@ -1,5 +1,5 @@ parameters: - GoVersion: 1.24.0 + GoVersion: 1.25.0 Condition: succeeded() steps: diff --git a/eng/scripts/Convert-GoCoverageToCobertura.ps1 b/eng/scripts/Convert-GoCoverageToCobertura.ps1 new file mode 100644 index 00000000000..eefb4f01514 --- /dev/null +++ b/eng/scripts/Convert-GoCoverageToCobertura.ps1 @@ -0,0 +1,306 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Converts Go coverage profile to Cobertura XML format for Azure DevOps. + +.DESCRIPTION + This script takes a Go coverage profile (cover.out) and converts it to Cobertura XML format + that can be consumed by Azure DevOps for code coverage reporting. This replaces the need + for external tools like gocov and gocov-xml. + +.PARAMETER CoverageFile + Path to the Go coverage profile file (typically cover.out) + +.PARAMETER OutputFile + Path where the Cobertura XML file should be written + +.PARAMETER SourceRoot + Root directory of the source code (used for relative path calculation) + +.EXAMPLE + ./Convert-GoCoverageToCobertura.ps1 -CoverageFile cover.out -OutputFile coverage.xml -SourceRoot . +#> + +param( + [Parameter(Mandatory = $true)] + [string]$CoverageFile, + + [Parameter(Mandatory = $true)] + [string]$OutputFile, + + [Parameter(Mandatory = $false)] + [string]$SourceRoot = "." +) + +$ErrorActionPreference = 'Stop' + +function Get-RelativePath { + param( + [string]$Path, + [string]$BasePath + ) + + $resolvedPath = Resolve-Path $Path -Relative + return $resolvedPath.TrimStart('.', '\', '/') +} + +function Normalize-GoFilePath { + param( + [string]$GoFilePath, + [string]$SourceRoot + ) + + # Handle Go module paths like "github.com/azure/azure-dev/cli/azd/pkg/test/file.go" + # Convert to relative paths like "cli/azd/pkg/test/file.go" + + # Common Go module path patterns to normalize + $modulePatterns = @( + "github.com/azure/azure-dev/", + "github.com/Azure/azure-dev/" + ) + + $normalizedPath = $GoFilePath + + foreach ($pattern in $modulePatterns) { + if ($normalizedPath.StartsWith($pattern)) { + $normalizedPath = $normalizedPath.Substring($pattern.Length) + break + } + } + + # Ensure we use forward slashes for consistency + $normalizedPath = $normalizedPath.Replace('\', '/') + + # Verify the file exists relative to source root + $fullPath = Join-Path $SourceRoot $normalizedPath + if (Test-Path $fullPath) { + return $normalizedPath + } else { + # If the normalized path doesn't exist, try to find it relative to cli/azd + $cliPath = $normalizedPath + if ($normalizedPath.StartsWith("cli/azd/")) { + $cliPath = $normalizedPath.Substring("cli/azd/".Length) + } + + $cliFullPath = Join-Path $SourceRoot "cli/azd/$cliPath" + if (Test-Path $cliFullPath) { + return "cli/azd/$cliPath" + } + } + + # Return the normalized path even if file doesn't exist (for build artifacts) + return $normalizedPath +} + +function Parse-GoCoverageFile { + param( + [string]$FilePath, + [string]$SourceRoot + ) + + $coverage = @{} + $lines = Get-Content $FilePath + + # Skip the mode line (first line) + for ($i = 1; $i -lt $lines.Count; $i++) { + $line = $lines[$i].Trim() + if ([string]::IsNullOrEmpty($line)) { continue } + + # Parse line format: file.go:startLine.startCol,endLine.endCol numStmt count + if ($line -match '^(.+):(\d+)\.(\d+),(\d+)\.(\d+) (\d+) (\d+)$') { + $originalFile = $matches[1] + $startLine = [int]$matches[2] + $endLine = [int]$matches[4] + $numStmt = [int]$matches[6] + $count = [int]$matches[7] + + # Normalize the file path to be relative to source root + $file = Normalize-GoFilePath -GoFilePath $originalFile -SourceRoot $SourceRoot + + if (-not $coverage.ContainsKey($file)) { + $coverage[$file] = @{} + } + + # Mark lines as covered or uncovered + for ($lineNum = $startLine; $lineNum -le $endLine; $lineNum++) { + if (-not $coverage[$file].ContainsKey($lineNum)) { + $coverage[$file][$lineNum] = @{ + hits = 0 + statements = 0 + } + } + $coverage[$file][$lineNum].hits += $count + $coverage[$file][$lineNum].statements += $numStmt + } + } + } + + return $coverage +} + +function Generate-CoberturaXml { + param( + [hashtable]$Coverage, + [string]$SourceRoot + ) + + $xml = [System.Xml.XmlDocument]::new() + $declaration = $xml.CreateXmlDeclaration("1.0", "UTF-8", $null) + $xml.AppendChild($declaration) | Out-Null + + # Create root coverage element + $coverageElement = $xml.CreateElement("coverage") + $coverageElement.SetAttribute("line-rate", "0.0") + $coverageElement.SetAttribute("branch-rate", "0.0") + $coverageElement.SetAttribute("lines-covered", "0") + $coverageElement.SetAttribute("lines-valid", "0") + $coverageElement.SetAttribute("branches-covered", "0") + $coverageElement.SetAttribute("branches-valid", "0") + $coverageElement.SetAttribute("complexity", "0.0") + $coverageElement.SetAttribute("version", "1.0") + $coverageElement.SetAttribute("timestamp", [DateTimeOffset]::Now.ToUnixTimeSeconds()) + $xml.AppendChild($coverageElement) | Out-Null + + # Create sources element + $sourcesElement = $xml.CreateElement("sources") + $sourceElement = $xml.CreateElement("source") + $sourceElement.InnerText = $SourceRoot + $sourcesElement.AppendChild($sourceElement) | Out-Null + $coverageElement.AppendChild($sourcesElement) | Out-Null + + # Create packages element + $packagesElement = $xml.CreateElement("packages") + $coverageElement.AppendChild($packagesElement) | Out-Null + + $totalLinesCovered = 0 + $totalLinesValid = 0 + + # Group files by package (directory) + $packages = @{} + foreach ($file in $Coverage.Keys) { + $packageName = Split-Path $file -Parent + if ([string]::IsNullOrEmpty($packageName)) { + $packageName = "." + } + + if (-not $packages.ContainsKey($packageName)) { + $packages[$packageName] = @{} + } + $packages[$packageName][$file] = $Coverage[$file] + } + + foreach ($packageName in $packages.Keys) { + $packageElement = $xml.CreateElement("package") + $packageElement.SetAttribute("name", $packageName) + + $packageLinesCovered = 0 + $packageLinesValid = 0 + + # Create classes element for this package + $classesElement = $xml.CreateElement("classes") + $packageElement.AppendChild($classesElement) | Out-Null + + foreach ($file in $packages[$packageName].Keys) { + $classElement = $xml.CreateElement("class") + $fileName = Split-Path $file -Leaf + $classElement.SetAttribute("name", $fileName) + $classElement.SetAttribute("filename", $file) + + $fileLinesCovered = 0 + $fileLinesValid = 0 + + # Create methods element (empty for Go files) + $methodsElement = $xml.CreateElement("methods") + $classElement.AppendChild($methodsElement) | Out-Null + + # Create lines element + $linesElement = $xml.CreateElement("lines") + $classElement.AppendChild($linesElement) | Out-Null + + foreach ($lineNum in ($packages[$packageName][$file].Keys | Sort-Object)) { + $lineData = $packages[$packageName][$file][$lineNum] + $lineElement = $xml.CreateElement("line") + $lineElement.SetAttribute("number", $lineNum) + $lineElement.SetAttribute("hits", $lineData.hits) + $lineElement.SetAttribute("branch", "false") + $linesElement.AppendChild($lineElement) | Out-Null + + $fileLinesValid++ + if ($lineData.hits -gt 0) { + $fileLinesCovered++ + } + } + + $packageLinesCovered += $fileLinesCovered + $packageLinesValid += $fileLinesValid + + # Set class coverage attributes + $classLineRate = if ($fileLinesValid -gt 0) { $fileLinesCovered / $fileLinesValid } else { 0.0 } + $classElement.SetAttribute("line-rate", $classLineRate.ToString("F4")) + $classElement.SetAttribute("branch-rate", "0.0") + $classElement.SetAttribute("complexity", "0.0") + + $classesElement.AppendChild($classElement) | Out-Null + } + + $totalLinesCovered += $packageLinesCovered + $totalLinesValid += $packageLinesValid + + # Set package coverage attributes + $packageLineRate = if ($packageLinesValid -gt 0) { $packageLinesCovered / $packageLinesValid } else { 0.0 } + $packageElement.SetAttribute("line-rate", $packageLineRate.ToString("F4")) + $packageElement.SetAttribute("branch-rate", "0.0") + $packageElement.SetAttribute("complexity", "0.0") + + $packagesElement.AppendChild($packageElement) | Out-Null + } + + # Update overall coverage attributes + $overallLineRate = if ($totalLinesValid -gt 0) { $totalLinesCovered / $totalLinesValid } else { 0.0 } + $coverageElement.SetAttribute("line-rate", $overallLineRate.ToString("F4")) + $coverageElement.SetAttribute("lines-covered", $totalLinesCovered) + $coverageElement.SetAttribute("lines-valid", $totalLinesValid) + + return $xml +} + +# Main execution +Write-Host "Converting Go coverage file '$CoverageFile' to Cobertura XML format..." + +if (-not (Test-Path $CoverageFile)) { + throw "Coverage file '$CoverageFile' not found" +} + +# Parse the Go coverage file +$coverage = Parse-GoCoverageFile -FilePath $CoverageFile -SourceRoot $SourceRoot + +# Generate Cobertura XML +$xml = Generate-CoberturaXml -Coverage $coverage -SourceRoot $SourceRoot + +# Save the XML file +$outputPath = if (Test-Path $OutputFile -IsValid) { + if ([System.IO.Path]::IsPathRooted($OutputFile)) { + $OutputFile + } else { + Join-Path (Get-Location) $OutputFile + } +} else { + $OutputFile +} +$xml.Save($outputPath) + +Write-Host "Successfully converted coverage to Cobertura XML: '$OutputFile'" + +# Display summary +$totalFiles = $coverage.Keys.Count +$totalLines = ($coverage.Values | ForEach-Object { $_.Keys.Count } | Measure-Object -Sum).Sum +$coveredLines = ($coverage.Values | ForEach-Object { + $_.Values | Where-Object { $_.hits -gt 0 } +} | Measure-Object).Count + +$coveragePercentage = if ($totalLines -gt 0) { ($coveredLines / $totalLines) * 100 } else { 0 } + +Write-Host "Coverage Summary:" +Write-Host " Files: $totalFiles" +Write-Host " Lines: $coveredLines/$totalLines ($($coveragePercentage.ToString('F2'))%)" diff --git a/go.mod b/go.mod index ba009a74964..a2ca81a849b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/azure/azure-dev -go 1.24 +go 1.25 require ( dario.cat/mergo v1.0.1