From 682ae6180b36e1558074c69c2fb41b3bea41e6fc Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 9 Oct 2026 16:52:25 -0700 Subject: [PATCH 1/2] fix(hidi): retain signed assembly in official tool package Replace the SDK tool publish input with the exact ESRP-signed staging assembly and verify payload provenance before NuGet signing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 61e122e7-9a38-4455-8cb1-343245120242 --- .azure-pipelines/hidi-release.yml | 7 +- .github/workflows/sonarcloud.yml | 72 ++++++++------- scripts/verify-hidi-package-assembly.ps1 | 59 ++++++++++++ .../Microsoft.OpenApi.Hidi.csproj | 20 +++++ src/Microsoft.OpenApi.Hidi/readme.md | 15 ++++ .../verify-hidi-package-assembly.Tests.ps1 | 90 +++++++++++++++++++ 6 files changed, 228 insertions(+), 35 deletions(-) create mode 100644 scripts/verify-hidi-package-assembly.ps1 create mode 100644 test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1 diff --git a/.azure-pipelines/hidi-release.yml b/.azure-pipelines/hidi-release.yml index 5329000f..659c14a6 100644 --- a/.azure-pipelines/hidi-release.yml +++ b/.azure-pipelines/hidi-release.yml @@ -185,14 +185,17 @@ extends: MaxRetryAttempts: '5' PendingAnalysisWaitTimeoutMinutes: '5' - pwsh: | - Copy-Item artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll src\Microsoft.OpenApi.Hidi\bin\$(buildConfiguration)\net8.0\Microsoft.OpenApi.Hidi.dll + $ErrorActionPreference = 'Stop' Copy-Item artifacts\hidi\signing\Microsoft.OpenApi.Hidi.exe artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe New-Item -ItemType Directory -Force '$(Build.ArtifactStagingDirectory)\hidi' | Out-Null - dotnet pack src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj -c $(buildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg -o '$(Build.ArtifactStagingDirectory)\hidi' + dotnet pack src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj -c $(buildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg '/p:HidiSignedAssemblyPath=$(Build.SourcesDirectory)\artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll' -o '$(Build.ArtifactStagingDirectory)\hidi' if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } Compress-Archive -Path artifacts\hidi\win-x64\* -DestinationPath '$(Build.ArtifactStagingDirectory)\hidi\hidi-win-x64-$(HidiVersion).zip' Copy-Item artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe '$(Build.ArtifactStagingDirectory)\hidi\Microsoft.OpenApi.Hidi.exe' displayName: Pack signed hidi binaries + - pwsh: | + .\scripts\verify-hidi-package-assembly.ps1 -PackagePath '$(Build.ArtifactStagingDirectory)\hidi\Microsoft.OpenApi.Hidi.$(HidiVersion).nupkg' -SignedAssemblyPath 'artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll' + displayName: Verify signed Hidi DLL payload before NuGet signing - task: EsrpCodeSigning@6 displayName: Sign hidi NuGet package inputs: diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index a7910c11..32ef64ba 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -60,59 +60,65 @@ jobs: - name: Install PowerShell test dependency shell: pwsh run: Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force - - name: Test Hidi NuGet helper with measured coverage + - name: Test Hidi NuGet helpers with measured coverage shell: pwsh run: | $ErrorActionPreference = 'Stop' Import-Module Pester -RequiredVersion 5.7.1 -ErrorAction Stop - $helperPath = 'scripts/check-nuget-package-published.ps1' - $helper = (Resolve-Path $helperPath).Path + $helperPaths = @('scripts/check-nuget-package-published.ps1', 'scripts/verify-hidi-package-assembly.ps1') + $helpers = @($helperPaths | ForEach-Object { (Resolve-Path $_).Path }) $outputRoot = Join-Path $PWD 'artifacts\hidi\powershell-coverage' New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null $configuration = New-PesterConfiguration - $configuration.Run.Path = 'test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1' + $configuration.Run.Path = @('test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1', 'test\Microsoft.OpenApi.Hidi.Tests\verify-hidi-package-assembly.Tests.ps1') $configuration.Run.PassThru = $true $configuration.CodeCoverage.Enabled = $true - $configuration.CodeCoverage.Path = $helper + $configuration.CodeCoverage.Path = $helpers $configuration.CodeCoverage.OutputFormat = 'JaCoCo' $configuration.CodeCoverage.OutputPath = Join-Path $outputRoot 'pester-coverage.xml' $configuration.CodeCoverage.CoveragePercentTarget = 80 $result = Invoke-Pester -Configuration $configuration - if ($result.FailedCount -gt 0 -or $result.PassedCount -lt 24) { - throw "Hidi NuGet helper tests failed or did not execute all 24 cases." + if ($result.FailedCount -gt 0 -or $result.PassedCount -lt 45) { + throw "Hidi NuGet helper tests failed or did not execute all 45 cases." } $report = [xml](Get-Content $configuration.CodeCoverage.OutputPath.Value -Raw) $sourceFiles = @($report.SelectNodes('//sourcefile')) - if ($sourceFiles.Count -ne 1 -or $sourceFiles[0].name -ne [IO.Path]::GetFileName($helper)) { - throw 'Expected measured coverage of only the Hidi NuGet helper.' - } - $lines = @($sourceFiles[0].SelectNodes('line')) - $sourceLineCount = @(Get-Content $helper).Count - $lineNumbers = @($lines | ForEach-Object { [int]$_.nr }) - if ($lines.Count -eq 0 -or @($lineNumbers | Select-Object -Unique).Count -ne $lines.Count -or - @($lines | Where-Object { - [int]$_.nr -le 0 -or [int]$_.nr -gt $sourceLineCount -or - -not $_.HasAttribute('ci') -or -not $_.HasAttribute('mi') -or - [int]$_.ci -lt 0 -or [int]$_.mi -lt 0 -or ([int]$_.ci + [int]$_.mi) -le 0 - }).Count -gt 0) { - throw 'Missing or invalid measured helper coverage lines.' - } - $covered = @($lines | Where-Object { [int]$_.ci -gt 0 }).Count - if ($covered / $lines.Count -lt 0.8) { - throw "Insufficient measured Hidi helper coverage: $covered/$($lines.Count) lines." + if ($sourceFiles.Count -ne $helpers.Count) { + throw 'Expected measured coverage of both Hidi NuGet helpers.' } $coverage = [xml]'' - $file = $coverage.CreateElement('file') - $file.SetAttribute('path', $helperPath) - [void]$coverage.DocumentElement.AppendChild($file) - foreach ($line in $lines) { - $entry = $coverage.CreateElement('lineToCover') - $entry.SetAttribute('lineNumber', $line.nr) - $entry.SetAttribute('covered', ([int]$line.ci -gt 0).ToString().ToLowerInvariant()) - [void]$file.AppendChild($entry) + foreach ($helperPath in $helperPaths) { + $helper = (Resolve-Path $helperPath).Path + $sourceFile = @($sourceFiles | Where-Object { $_.name -eq [IO.Path]::GetFileName($helper) }) + if ($sourceFile.Count -ne 1) { throw "Missing or ambiguous helper coverage: $helperPath" } + $lines = @($sourceFile[0].SelectNodes('line')) + $sourceLineCount = @(Get-Content $helper).Count + $lineNumbers = @($lines | ForEach-Object { [int]$_.nr }) + if ($lines.Count -eq 0 -or @($lineNumbers | Select-Object -Unique).Count -ne $lines.Count -or + @($lines | Where-Object { + [int]$_.nr -le 0 -or [int]$_.nr -gt $sourceLineCount -or + -not $_.HasAttribute('ci') -or -not $_.HasAttribute('mi') -or + [int]$_.ci -lt 0 -or [int]$_.mi -lt 0 -or ([int]$_.ci + [int]$_.mi) -le 0 + }).Count -gt 0) { + throw "Missing or invalid measured helper coverage lines: $helperPath" + } + $covered = @($lines | Where-Object { [int]$_.ci -gt 0 }).Count + if ($covered / $lines.Count -lt 0.8) { + throw "Insufficient measured Hidi helper coverage: $helperPath $covered/$($lines.Count) lines." + } + $file = $coverage.CreateElement('file') + $file.SetAttribute('path', $helperPath) + [void]$coverage.DocumentElement.AppendChild($file) + foreach ($line in $lines) { + $entry = $coverage.CreateElement('lineToCover') + $entry.SetAttribute('lineNumber', $line.nr) + $entry.SetAttribute('covered', ([int]$line.ci -gt 0).ToString().ToLowerInvariant()) + [void]$file.AppendChild($entry) + } + Write-Host "Measured Hidi helper coverage: $helperPath $covered/$($lines.Count) lines." } $coverage.Save((Join-Path $outputRoot 'sonar-coverage.xml')) - Write-Host "Measured Hidi NuGet helper coverage: $covered/$($lines.Count) lines; Sonar generic report generated." + Write-Host 'Sonar generic report generated for both Hidi NuGet helpers.' - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any diff --git a/scripts/verify-hidi-package-assembly.ps1 b/scripts/verify-hidi-package-assembly.ps1 new file mode 100644 index 00000000..53365fe7 --- /dev/null +++ b/scripts/verify-hidi-package-assembly.ps1 @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +<# +.SYNOPSIS +Verifies the Hidi tool package contains the exact Microsoft-signed staging DLL. +.DESCRIPTION +Run after tool packing and before NuGet signing. A signed NuGet container does +not prove that its assembly payload retained the ESRP signature. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackagePath, + [Parameter(Mandatory = $true)] + [string]$SignedAssemblyPath +) + +$ErrorActionPreference = 'Stop' + +foreach ($path in @($PackagePath, $SignedAssemblyPath)) { + if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { + throw "Missing Hidi signing verification input: $path" + } +} + +$temporaryDirectory = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString()) +New-Item -ItemType Directory -Path $temporaryDirectory | Out-Null +$packagedAssembly = Join-Path $temporaryDirectory 'Microsoft.OpenApi.Hidi.dll' +$archive = $null +try { + $archive = [IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $PackagePath).Path) + $assemblies = @($archive.Entries | Where-Object { $_.Name -ieq 'Microsoft.OpenApi.Hidi.dll' }) + if ($assemblies.Count -ne 1 -or $assemblies[0].FullName -cne 'tools/net8.0/any/Microsoft.OpenApi.Hidi.dll') { + throw 'Expected exactly one Hidi DLL at tools/net8.0/any/Microsoft.OpenApi.Hidi.dll.' + } + [IO.Compression.ZipFileExtensions]::ExtractToFile($assemblies[0], $packagedAssembly) + + $stagingHash = (Get-FileHash -LiteralPath $SignedAssemblyPath -Algorithm SHA256).Hash + $packageHash = (Get-FileHash -LiteralPath $packagedAssembly -Algorithm SHA256).Hash + if ($packageHash -cne $stagingHash) { + throw "Packaged Hidi DLL differs from the signed staging DLL: $packageHash != $stagingHash" + } + foreach ($assembly in @($SignedAssemblyPath, $packagedAssembly)) { + $signature = Get-AuthenticodeSignature -LiteralPath $assembly + if ($signature.Status -ne 'Valid' -or + $signature.SignerCertificate.Subject -notmatch '(^|,\s*)O=Microsoft Corporation(,|$)') { + throw "Hidi DLL must have a valid Microsoft Corporation Authenticode signature: $assembly ($($signature.Status))" + } + } + Write-Host "Verified packaged Hidi DLL: valid Microsoft Authenticode signature; signed staging SHA256=$stagingHash" +} +finally { + if ($null -ne $archive) { $archive.Dispose() } + if (Test-Path -LiteralPath $packagedAssembly) { + Remove-Item -LiteralPath $packagedAssembly -Force + } + Remove-Item -LiteralPath $temporaryDirectory -Force +} diff --git a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj index 629b9d3a..f52a996a 100644 --- a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj +++ b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj @@ -70,4 +70,24 @@ + + + + <_HidiAssemblyToReplace Include="@(ResolvedFileToPublish)" + Condition="'%(ResolvedFileToPublish.RelativePath)' == '$(TargetFileName)'" /> + + + + + + + + + diff --git a/src/Microsoft.OpenApi.Hidi/readme.md b/src/Microsoft.OpenApi.Hidi/readme.md index 0d9b83a9..9f99b6fd 100644 --- a/src/Microsoft.OpenApi.Hidi/readme.md +++ b/src/Microsoft.OpenApi.Hidi/readme.md @@ -73,6 +73,16 @@ the signed Hidi `.nupkg`, a matching `.snupkg` and the private-feed version-chec script in the `Hidi` artifact. The `nuget-org` release job consumes that artifact without a repository checkout or the Docker context. +Tool packing republishes the SDK's intermediate assembly, so replacing the `bin` +DLL does not preserve signing. The official no-build pack supplies +`HidiSignedAssemblyPath` to replace only the Hidi `ResolvedFileToPublish` item +with the ESRP-signed staging DLL. Missing or ambiguous inputs stop packing. +Before NuGet signing, `scripts/verify-hidi-package-assembly.ps1` requires exactly +one Hidi DLL at the expected tool path, byte-for-byte SHA256 equality with staging, +and valid Microsoft Corporation Authenticode signatures on both DLLs. +A signed NuGet container alone is not proof of a signed assembly payload. +Local packing without this property retains the normal SDK behavior. + Before ESRP publication, the job requires the exact `hidi-v3.*` release package and symbols with a version newer than 3.10.2. It checks `GraphDeveloperExperiences_Public` using `System.AccessToken` through @@ -94,6 +104,11 @@ execute the cases. The test resides physically inside the Hidi test project so SonarScanner for .NET classifies it as test code, rather than root source code. The Sonar workflow also measures helper line coverage and imports a generic coverage report alongside the existing C# OpenCover reports. +The payload verifier has 21 isolated Pester cases in +`test\Microsoft.OpenApi.Hidi.Tests\verify-hidi-package-assembly.Tests.ps1`, +included in that measured coverage. These tests mock signature verification; +they do not establish real ESRP signing. Final readiness requires a +publish-disabled official build and verification of the downloaded DLL payload. ### Windows executable diff --git a/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1 b/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1 new file mode 100644 index 00000000..32cd844c --- /dev/null +++ b/test/Microsoft.OpenApi.Hidi.Tests/verify-hidi-package-assembly.Tests.ps1 @@ -0,0 +1,90 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +BeforeDiscovery { + $cases = @( + @{ Name = 'matching signed DLL before NuGet signing' } + @{ Name = 'different payload bytes'; Payload = 'unsigned-build'; ExpectedError = '*differs from the signed staging DLL*' } + @{ Name = 'missing tool DLL'; Entries = @(); ExpectedError = '*exactly one Hidi DLL*' } + @{ Name = 'wrong tool location'; Entries = @('lib/net8.0/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } + @{ Name = 'duplicate tool DLL'; Entries = @('tools/net8.0/any/Microsoft.OpenApi.Hidi.dll', 'tools/net8.0/any/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } + @{ Name = 'extra tool DLL elsewhere'; Entries = @('tools/net8.0/any/Microsoft.OpenApi.Hidi.dll', 'lib/net8.0/Microsoft.OpenApi.Hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } + @{ Name = 'wrong entry casing'; Entries = @('tools/net8.0/any/microsoft.openapi.hidi.dll'); ExpectedError = '*exactly one Hidi DLL*' } + @{ Name = 'unsigned package payload'; PackageStatus = 'NotSigned'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'damaged package signature'; PackageStatus = 'HashMismatch'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'unverifiable package signature'; PackageStatus = 'UnknownError'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'unsigned staging DLL'; StagingStatus = 'NotSigned'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'damaged staging signature'; StagingStatus = 'HashMismatch'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'non-Microsoft payload signer'; PackageSubject = 'CN=Example, O=Example'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'non-Microsoft staging signer'; StagingSubject = 'CN=Example, O=Example'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'misleading Microsoft signer name'; PackageSubject = 'CN=Microsoft Corporation, O=Example'; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'missing payload certificate'; MissingPackageCertificate = $true; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'missing staging certificate'; MissingStagingCertificate = $true; ExpectedError = '*valid Microsoft Corporation Authenticode signature*' } + @{ Name = 'missing package'; MissingPackage = $true; ExpectedError = '*Missing Hidi signing verification input*' } + @{ Name = 'missing staging DLL'; MissingStaging = $true; ExpectedError = '*Missing Hidi signing verification input*' } + @{ Name = 'invalid package archive'; InvalidArchive = $true; ExpectedError = '*' } + @{ Name = 'signature verification error'; SignatureError = $true; ExpectedError = '*Signature verification unavailable*' } + ) +} + +Describe 'Hidi package signed-assembly provenance' { + BeforeAll { + $helper = Join-Path $PSScriptRoot '..\..\scripts\verify-hidi-package-assembly.ps1' + } + + It '' -ForEach $cases { + $testCase = $_ + $directory = Join-Path $TestDrive ([guid]::NewGuid().ToString()) + New-Item -ItemType Directory -Path $directory | Out-Null + $package = Join-Path $directory 'Microsoft.OpenApi.Hidi.3.10.2.nupkg' + $staging = Join-Path $directory 'Microsoft.OpenApi.Hidi.dll' + if (-not $MissingStaging) { [IO.File]::WriteAllText($staging, 'signed-staging') } + if (-not $MissingPackage) { + if ($InvalidArchive) { + [IO.File]::WriteAllText($package, 'not-a-zip') + } + else { + $archive = [IO.Compression.ZipFile]::Open($package, 'Create') + try { + $entryNames = if ($testCase.ContainsKey('Entries')) { $Entries } else { @('tools/net8.0/any/Microsoft.OpenApi.Hidi.dll') } + foreach ($name in $entryNames) { + $writer = [IO.StreamWriter]::new($archive.CreateEntry($name).Open()) + try { $writer.Write($(if ($Payload) { $Payload } else { 'signed-staging' })) } + finally { $writer.Dispose() } + } + } + finally { $archive.Dispose() } + } + } + $inspectedPaths = [Collections.Generic.List[string]]::new() + Mock Get-AuthenticodeSignature { + param($LiteralPath) + $inspectedPaths.Add($LiteralPath) + if ($SignatureError) { throw 'Signature verification unavailable' } + $isStaging = $LiteralPath -eq $staging + $status = if ($isStaging -and $StagingStatus) { $StagingStatus } + elseif (-not $isStaging -and $PackageStatus) { $PackageStatus } + else { 'Valid' } + $subject = if ($isStaging -and $StagingSubject) { $StagingSubject } + elseif (-not $isStaging -and $PackageSubject) { $PackageSubject } + else { 'CN=Microsoft Corporation, O=Microsoft Corporation, C=US' } + $certificate = if (($isStaging -and $MissingStagingCertificate) -or + (-not $isStaging -and $MissingPackageCertificate)) { $null } + else { [pscustomobject]@{ Subject = $subject } } + [pscustomobject]@{ Status = $status; SignerCertificate = $certificate } + } + + if ($ExpectedError) { + { & $helper -PackagePath $package -SignedAssemblyPath $staging } | Should -Throw $ExpectedError + } + else { + $output = & $helper -PackagePath $package -SignedAssemblyPath $staging 6>&1 + $output | Should -Match "signed staging SHA256=$((Get-FileHash -LiteralPath $staging).Hash)" + Should -Invoke Get-AuthenticodeSignature -Times 2 -Exactly + } + foreach ($path in $inspectedPaths | Where-Object { $_ -ne $staging }) { + Test-Path -LiteralPath $path | Should -BeFalse + Test-Path -LiteralPath (Split-Path $path -Parent) | Should -BeFalse + } + } +} From 66c3478f2e168543dfa3af03e58b91286860878b Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 9 Oct 2026 16:57:54 -0700 Subject: [PATCH 2/2] fix(ci): authenticate repository metadata request Use the workflow-provided token for the existing metadata GET to avoid shared runner anonymous API-rate-limit failures, without changing branch outputs, permissions or publishing conditions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 61e122e7-9a38-4455-8cb1-343245120242 --- .github/workflows/ci-cd.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 15036350..e52b1085 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -26,10 +26,12 @@ jobs: - name: Data gatherer id: data_gatherer shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} run: | # Get default branch $repo = 'microsoft/OpenAPI.NET.OData' - $defaultBranch = Invoke-RestMethod -Method GET -Uri https://api.github.com/repos/$repo | Select-Object -ExpandProperty default_branch + $defaultBranch = Invoke-RestMethod -Method GET -Uri https://api.github.com/repos/$repo -Headers @{ Authorization = "Bearer $env:GITHUB_TOKEN" } | Select-Object -ExpandProperty default_branch Write-Output "default_branch=$(echo $defaultBranch) >> $GITHUB_OUTPUT" - name: Conditionals handler