diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 10155a107..dbf190268 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -62,9 +62,14 @@ jobs: - name: Release script tests shell: pwsh run: | - $result = Invoke-Pester ./scripts/verify-release.Tests.ps1 -Output Detailed -PassThru + $result = Invoke-Pester ./scripts -Output Detailed -PassThru if ($result.FailedCount -ne 0) { exit 1 } + # From pwsh, as build-release.ps1 calls it: mvnw.cmd 3.2.0 failed its checksum there on Windows. + - name: Maven wrapper + shell: pwsh + run: ./mvnw -v + # Runs the same probes twice, JIT compiled and published with Native AOT, so that a # difference between the two is caused by AOT rather than by the platform. The project # knows which probes are expected to fail in each mode and exits non-zero when reality @@ -74,12 +79,10 @@ jobs: env: "log4net.AotEnvironmentProbe": from-environment run: | + $PSNativeCommandUseErrorActionPreference = $true dotnet run --project ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } dotnet publish ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release -o ./aot-probes - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & "./aot-probes/log4net.Tests.Aot$($IsWindows ? '.exe' : '')" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } # A process that hosts the runtime natively has no entry assembly, so the configuration # system cannot work out where the config file is (issue #162). The check owns the process @@ -93,6 +96,4 @@ jobs: # makes it a green that cannot fail for the reason the check exists. - name: AOT probes, no entry assembly shell: pwsh - run: | - dotnet run --project ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release -- --no-entry-assembly - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } \ No newline at end of file + run: dotnet run --project ./src/log4net.Tests.Aot/log4net.Tests.Aot.csproj -c Release -- --no-entry-assembly \ No newline at end of file diff --git a/scripts/FakeCommands.TestHelper.ps1 b/scripts/FakeCommands.TestHelper.ps1 new file mode 100644 index 000000000..280ea4cd6 --- /dev/null +++ b/scripts/FakeCommands.TestHelper.ps1 @@ -0,0 +1,88 @@ +#Requires -Version 7.4 + +<# +.SYNOPSIS + Runs a build script in a scratch tree against fake native commands, for the Pester tests. + +.DESCRIPTION + The fakes log every call and create the files the scripts go on to read, so a test sees which + commands ran without building, signing, tagging or pushing anything. +#> + +# Logs its call, fails when named in FAKE_FAIL, and otherwise writes the files the real command +# would, where it is told to, so a wrong output path fails as it would for real. +$script:FakeCommand = @' +param([string]$Name) +Add-Content -Path $env:FAKE_LOG -Value "$Name $($args -join ' ')" +if ($env:FAKE_FAIL -eq $Name) { exit 1 } +$outputs = switch ($Name) +{ + 'dotnet' + { + $version = ($args -join ' ') -replace '.*PackageVersion=(\S+).*', '$1' + $build = "$(Split-Path $PSScriptRoot)/build" + "$build/artifacts/log4net.$version.nupkg", "$build/artifacts/log4net.Ext.Mail.$version.nupkg", "$build/Release/net462/log4net.dll" + } + 'git' { if ($args[0] -eq 'archive') { $args[$args.IndexOf('--output') + 1] } } + 'zip' { $args[1] } +} +$outputs | ForEach-Object { New-Item -ItemType File -Force -Path $_ | Out-Null } +'@ + +function New-ScratchTree +{ + param ([Parameter(Mandatory)][string]$Script) + + $root = New-Item -ItemType Directory -Path (Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid())) + $scripts = New-Item -ItemType Directory -Path (Join-Path $root 'scripts') + $bin = New-Item -ItemType Directory -Path (Join-Path $root 'fakebin') + Copy-Item (Join-Path $PSScriptRoot $Script) $scripts + Copy-Item (Join-Path $PSScriptRoot 'verify-release.*') $scripts + 'license' | Set-Content (Join-Path $root 'LICENSE') + 'notice' | Set-Content (Join-Path $root 'NOTICE') + Set-Content -Path (Join-Path $bin 'fake.ps1') -Value $script:FakeCommand + foreach ($name in 'dotnet', 'git', 'gpg', 'zip', 'mvnw') + { + # mvnw is called by path from the root, the others through PATH. + $directory = $name -eq 'mvnw' ? $root : $bin + if ($IsWindows) + { + Set-Content -Path (Join-Path $directory "$name.cmd") -Value "@pwsh -NoProfile -File `"$bin\fake.ps1`" $name %*`r`n@exit /b %ERRORLEVEL%" + } + else + { + $path = Join-Path $directory $name + Set-Content -Path $path -Value "#!/bin/sh`nexec pwsh -NoProfile -File '$bin/fake.ps1' $name `"`$@`"" + chmod +x $path + } + } + return $root.FullName +} + +function Invoke-InScratchTree +{ + param ([Parameter(Mandatory)][string]$Root, [Parameter(Mandatory)][string]$Script, [string]$Fail) + + $log = Join-Path $Root 'calls.log' + New-Item -ItemType File -Path $log -Force | Out-Null + $path = $env:PATH + try + { + $env:PATH = (Join-Path $Root 'fakebin') + [System.IO.Path]::PathSeparator + $path + $env:FAKE_LOG = $log + $env:FAKE_FAIL = $Fail + # NonInteractive makes the confirmation pause throw, standing in for a release manager who says no. + $output = pwsh -NoProfile -NonInteractive -File (Join-Path $Root 'scripts' $Script) 2>&1 + $exitCode = $LASTEXITCODE + } + finally + { + $env:PATH = $path + Remove-Item Env:FAKE_LOG, Env:FAKE_FAIL -ErrorAction SilentlyContinue + } + return [pscustomobject]@{ + ExitCode = $exitCode + Calls = @(Get-Content $log | ForEach-Object { ($_ -split ' ')[0..1] -join ' ' }) + Output = $output -join [Environment]::NewLine + } +} diff --git a/scripts/build-preview.Tests.ps1 b/scripts/build-preview.Tests.ps1 new file mode 100644 index 000000000..55617312b --- /dev/null +++ b/scripts/build-preview.Tests.ps1 @@ -0,0 +1,48 @@ +#Requires -Version 7.4 +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0' } + +<# +.SYNOPSIS + Checks that build-preview.ps1 stops at the first failure and tags nothing unconfirmed. + +.DESCRIPTION + Runs the script against fake native commands, so nothing is built, signed, tagged or pushed. + + Run with: Invoke-Pester ./scripts/build-preview.Tests.ps1 +#> + +BeforeAll { + . (Join-Path $PSScriptRoot 'FakeCommands.TestHelper.ps1') +} + +Describe 'build-preview.ps1' { + BeforeEach { + $script:Root = New-ScratchTree -Script 'build-preview.ps1' + } + + AfterEach { + Remove-Item $script:Root -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'signs nothing when the build fails' { + $result = Invoke-InScratchTree -Root $script:Root -Script 'build-preview.ps1' -Fail 'dotnet' + + $result.ExitCode | Should -Not -Be 0 + $result.Calls | Should -Be @('dotnet build') + } + + It 'tags nothing when signing fails' { + $result = Invoke-InScratchTree -Root $script:Root -Script 'build-preview.ps1' -Fail 'gpg' + + $result.ExitCode | Should -Not -Be 0 + $result.Calls | Should -Be @('dotnet build', 'gpg --armor') + } + + It 'signs both packages and tags nothing without confirmation' { + $result = Invoke-InScratchTree -Root $script:Root -Script 'build-preview.ps1' + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -BeLike '*NonInteractive*' + $result.Calls | Should -Be @('dotnet build', 'gpg --armor', 'gpg --armor') + } +} diff --git a/scripts/build-release.Tests.ps1 b/scripts/build-release.Tests.ps1 new file mode 100644 index 000000000..125634712 --- /dev/null +++ b/scripts/build-release.Tests.ps1 @@ -0,0 +1,65 @@ +#Requires -Version 7.4 +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0' } + +<# +.SYNOPSIS + Checks that build-release.ps1 stops at the first failure and tags nothing unconfirmed. + +.DESCRIPTION + Runs the script against fake native commands, so nothing is built, signed, tagged or pushed. + + Run with: Invoke-Pester ./scripts/build-release.Tests.ps1 +#> + +BeforeAll { + . (Join-Path $PSScriptRoot 'FakeCommands.TestHelper.ps1') +} + +Describe 'build-release.ps1' { + BeforeEach { + $script:Root = New-ScratchTree -Script 'build-release.ps1' + } + + AfterEach { + Remove-Item $script:Root -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'signs nothing when the build fails' { + $result = Invoke-InScratchTree -Root $script:Root -Script 'build-release.ps1' -Fail 'dotnet' + + $result.ExitCode | Should -Not -Be 0 + $result.Calls | Should -Be @('dotnet test') + } + + It 'builds no site when signing fails' { + $result = Invoke-InScratchTree -Root $script:Root -Script 'build-release.ps1' -Fail 'gpg' + + $result.ExitCode | Should -Not -Be 0 + $result.Calls | Should -Be @('dotnet test', 'git archive', 'zip -r', 'gpg --armor') + } + + It 'asks for no tag when the site build fails' { + $result = Invoke-InScratchTree -Root $script:Root -Script 'build-release.ps1' -Fail 'mvnw' + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -Not -BeLike '*NonInteractive*' + $result.Calls[-1] | Should -Be 'mvnw site' + } + + It 'signs all six artifacts and tags nothing without confirmation' { + $result = Invoke-InScratchTree -Root $script:Root -Script 'build-release.ps1' + + $result.ExitCode | Should -Not -Be 0 + $result.Output | Should -BeLike '*NonInteractive*' + $result.Calls | Should -Be @('dotnet test', 'git archive', 'zip -r', + 'gpg --armor', 'gpg --armor', 'gpg --armor', 'gpg --armor', 'gpg --armor', 'gpg --armor', 'mvnw site') + } + + It 'ships no artifact without a hash' { + Invoke-InScratchTree -Root $script:Root -Script 'build-release.ps1' | Out-Null + + $artifacts = Get-ChildItem (Join-Path $script:Root 'build' 'artifacts') -Exclude '*.sha512', '*.asc' + $artifacts.Name | Should -HaveCount 6 + $artifacts | Where-Object { !(Test-Path "$($_.FullName).sha512") } | Should -BeNullOrEmpty + } +} diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index de2f0c1ee..82e4cef01 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -34,7 +34,7 @@ pushd $PSScriptRoot/.. git archive --format=zip --output $PSScriptRoot/../build/artifacts/apache-log4net-source-$Version.zip master popd 'compressing binaries ...' -Copy-Item $PSScriptRoot/verify-release.* $PSScriptRoot/../build/artifacts/ +Copy-Item $PSScriptRoot/verify-release.ps1, $PSScriptRoot/verify-release.sh $PSScriptRoot/../build/artifacts/ Copy-Item $PSScriptRoot/../LICENSE $PSScriptRoot/../build/Release/ Copy-Item $PSScriptRoot/../NOTICE $PSScriptRoot/../build/Release/ pushd $PSScriptRoot/../build/Release diff --git a/src/changelog/3.5.0/.release.xml b/src/changelog/3.5.0/.release.xml index 4bba497b0..39dda551e 100644 --- a/src/changelog/3.5.0/.release.xml +++ b/src/changelog/3.5.0/.release.xml @@ -2,5 +2,5 @@ \ No newline at end of file