Release #20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| schedule: | |
| - cron: '0 3 * * 0' | |
| workflow_dispatch: | |
| inputs: | |
| force: | |
| description: Create a release even if no commits exist since the latest published release | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| jobs: | |
| prepare-release: | |
| name: Prepare Release | |
| runs-on: ubuntu-slim | |
| outputs: | |
| should_release: ${{ steps.compute.outputs.should_release }} | |
| release_tag: ${{ steps.compute.outputs.release_tag }} | |
| release_name: ${{ steps.compute.outputs.release_name }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Determine release metadata | |
| id: compute | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| FORCE_RELEASE: ${{ inputs.force && 'true' || 'false' }} | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| git fetch --force --tags | |
| $latestReleaseTag = $null | |
| try { | |
| $latestReleaseTag = gh api "repos/$env:GITHUB_REPOSITORY/releases/latest" --jq '.tag_name' | |
| } | |
| catch { | |
| Write-Host 'No published release found yet.' | |
| } | |
| $changeCount = if ([string]::IsNullOrWhiteSpace($latestReleaseTag)) { | |
| [int](git rev-list --count HEAD) | |
| } | |
| else { | |
| [int](git rev-list --count "$latestReleaseTag..HEAD") | |
| } | |
| $parisNow = [System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTimeOffset]::UtcNow, 'Europe/Paris') | |
| $datePrefix = 'v{0}.{1}.{2}' -f ($parisNow.Year % 100), $parisNow.Month, $parisNow.Day | |
| $existingTags = @(git tag -l "$datePrefix.*") | |
| $nextBuild = 1 | |
| if ($existingTags.Count -gt 0) { | |
| $existingBuilds = $existingTags | | |
| ForEach-Object { | |
| if ($_ -match '^v\d{2}\.\d{1,2}\.\d{1,2}\.(\d+)$') { | |
| [int]$Matches[1] | |
| } | |
| } | | |
| Sort-Object -Descending | |
| if ($existingBuilds.Count -gt 0) { | |
| $nextBuild = $existingBuilds[0] + 1 | |
| } | |
| } | |
| $releaseTag = "$datePrefix.$nextBuild" | |
| $releaseName = "Foundry $releaseTag" | |
| $shouldRelease = $changeCount -gt 0 -or $env:FORCE_RELEASE -eq 'true' | |
| "should_release=$($shouldRelease.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT | |
| "release_tag=$releaseTag" >> $env:GITHUB_OUTPUT | |
| "release_name=$releaseName" >> $env:GITHUB_OUTPUT | |
| if ($shouldRelease) { | |
| Write-Host "Release will be created with tag '$releaseTag'." | |
| } | |
| else { | |
| Write-Host "No commits found since '$latestReleaseTag'. Skipping release creation." | |
| } | |
| - name: Create GitHub release | |
| if: steps.compute.outputs.should_release == 'true' | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh release create "${{ steps.compute.outputs.release_tag }}" ` | |
| --title "${{ steps.compute.outputs.release_name }}" ` | |
| --target "${{ github.sha }}" ` | |
| --generate-notes | |
| publish-release-assets: | |
| name: Publish Release Assets | |
| runs-on: windows-latest | |
| needs: prepare-release | |
| if: needs.prepare-release.outputs.should_release == 'true' | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up .NET SDK | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ${{ runner.os }}-${{ runner.arch }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/Directory.Build.props', 'global.json', 'NuGet.config', 'nuget.config') }} | |
| restore-keys: | | |
| ${{ runner.os }}-${{ runner.arch }}-nuget- | |
| - name: Validate release tag and apply version | |
| shell: pwsh | |
| run: | | |
| $tag = '${{ needs.prepare-release.outputs.release_tag }}' | |
| $match = [regex]::Match($tag, '^v(?<year>\d{2})\.(?<month>\d{1,2})\.(?<day>\d{1,2})\.(?<build>[1-9]\d*)$') | |
| if (-not $match.Success) { | |
| throw "Release tag '$tag' must use the format vYY.M.D.Build." | |
| } | |
| $year = 2000 + [int]$match.Groups['year'].Value | |
| $month = [int]$match.Groups['month'].Value | |
| $day = [int]$match.Groups['day'].Value | |
| try { | |
| [void][datetime]::new($year, $month, $day, 0, 0, 0, [DateTimeKind]::Utc) | |
| } | |
| catch { | |
| throw "Release tag '$tag' does not contain a valid calendar date." | |
| } | |
| $version = $tag.Substring(1) | |
| $propsPath = Join-Path $env:GITHUB_WORKSPACE 'src\Directory.Build.props' | |
| $content = Get-Content -Path $propsPath -Raw | |
| foreach ($propertyName in @('Version', 'AssemblyVersion', 'FileVersion', 'InformationalVersion')) { | |
| $pattern = "(?<=<$propertyName>)[^<]+(?=</$propertyName>)" | |
| if (-not [regex]::IsMatch($content, $pattern)) { | |
| throw "Unable to locate <$propertyName> in '$propsPath'." | |
| } | |
| $content = [regex]::Replace($content, $pattern, $version) | |
| } | |
| Set-Content -Path $propsPath -Value $content -NoNewline | |
| Add-Content -Path $env:GITHUB_ENV -Value "RELEASE_VERSION=$version" | |
| - name: Build release assets | |
| shell: pwsh | |
| run: | | |
| $workspace = $env:GITHUB_WORKSPACE | |
| $releaseRoot = Join-Path $workspace 'artifacts\release' | |
| $publishRoot = Join-Path $releaseRoot 'publish' | |
| $foundryProject = Join-Path $workspace 'src\Foundry\Foundry.csproj' | |
| $connectProject = Join-Path $workspace 'src\Foundry.Connect\Foundry.Connect.csproj' | |
| $deployProject = Join-Path $workspace 'src\Foundry.Deploy\Foundry.Deploy.csproj' | |
| $publishProperties = @( | |
| 'PublishSingleFile=true', | |
| 'EnableCompressionInSingleFile=true', | |
| 'IncludeNativeLibrariesForSelfExtract=true', | |
| 'IncludeAllContentForSelfExtract=true', | |
| 'DebugType=None', | |
| 'GenerateDocumentationFile=false' | |
| ) | |
| function Invoke-Publish { | |
| param( | |
| [Parameter(Mandatory = $true)] | |
| [string]$ProjectPath, | |
| [Parameter(Mandatory = $true)] | |
| [string]$RuntimeIdentifier, | |
| [Parameter(Mandatory = $true)] | |
| [string]$OutputPath | |
| ) | |
| if (Test-Path -Path $OutputPath) { | |
| Remove-Item -Path $OutputPath -Recurse -Force | |
| } | |
| New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null | |
| $publishArgs = @( | |
| 'publish', | |
| $ProjectPath, | |
| '-c', 'Release', | |
| '-r', $RuntimeIdentifier, | |
| '--self-contained', 'true', | |
| '-o', $OutputPath, | |
| '--nologo' | |
| ) | |
| foreach ($property in $publishProperties) { | |
| $publishArgs += "-p:$property" | |
| } | |
| dotnet @publishArgs | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "dotnet publish failed for '$ProjectPath' ($RuntimeIdentifier)." | |
| } | |
| } | |
| if (Test-Path -Path $releaseRoot) { | |
| Remove-Item -Path $releaseRoot -Recurse -Force | |
| } | |
| New-Item -Path $releaseRoot -ItemType Directory -Force | Out-Null | |
| New-Item -Path $publishRoot -ItemType Directory -Force | Out-Null | |
| foreach ($runtimeIdentifier in @('win-x64', 'win-arm64')) { | |
| $architecture = if ($runtimeIdentifier -eq 'win-x64') { 'x64' } else { 'arm64' } | |
| $foundryPublishPath = Join-Path $publishRoot (Join-Path 'Foundry' $runtimeIdentifier) | |
| Invoke-Publish -ProjectPath $foundryProject -RuntimeIdentifier $runtimeIdentifier -OutputPath $foundryPublishPath | |
| $foundryExecutable = Join-Path $foundryPublishPath 'Foundry.exe' | |
| if (-not (Test-Path -Path $foundryExecutable -PathType Leaf)) { | |
| throw "Expected Foundry executable not found: '$foundryExecutable'." | |
| } | |
| $foundryAssetPath = Join-Path $releaseRoot "Foundry-${architecture}.exe" | |
| Copy-Item -Path $foundryExecutable -Destination $foundryAssetPath -Force | |
| $deployPublishPath = Join-Path $publishRoot (Join-Path 'Foundry.Deploy' $runtimeIdentifier) | |
| Invoke-Publish -ProjectPath $deployProject -RuntimeIdentifier $runtimeIdentifier -OutputPath $deployPublishPath | |
| $deployExecutable = Join-Path $deployPublishPath 'Foundry.Deploy.exe' | |
| if (-not (Test-Path -Path $deployExecutable -PathType Leaf)) { | |
| throw "Expected Foundry.Deploy executable not found: '$deployExecutable'." | |
| } | |
| $deployAssetPath = Join-Path $releaseRoot "Foundry.Deploy-$runtimeIdentifier.zip" | |
| Compress-Archive -Path (Join-Path $deployPublishPath '*') -DestinationPath $deployAssetPath -CompressionLevel Optimal -Force | |
| $connectPublishPath = Join-Path $publishRoot (Join-Path 'Foundry.Connect' $runtimeIdentifier) | |
| Invoke-Publish -ProjectPath $connectProject -RuntimeIdentifier $runtimeIdentifier -OutputPath $connectPublishPath | |
| $connectExecutable = Join-Path $connectPublishPath 'Foundry.Connect.exe' | |
| if (-not (Test-Path -Path $connectExecutable -PathType Leaf)) { | |
| throw "Expected Foundry.Connect executable not found: '$connectExecutable'." | |
| } | |
| $connectAssetPath = Join-Path $releaseRoot "Foundry.Connect-$runtimeIdentifier.zip" | |
| Compress-Archive -Path (Join-Path $connectPublishPath '*') -DestinationPath $connectAssetPath -CompressionLevel Optimal -Force | |
| } | |
| - name: Upload release assets | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| $releaseTag = '${{ needs.prepare-release.outputs.release_tag }}' | |
| $releaseRoot = Join-Path $env:GITHUB_WORKSPACE 'artifacts\release' | |
| $assets = @( | |
| (Join-Path $releaseRoot 'Foundry-x64.exe'), | |
| (Join-Path $releaseRoot 'Foundry-arm64.exe'), | |
| (Join-Path $releaseRoot 'Foundry.Connect-win-x64.zip'), | |
| (Join-Path $releaseRoot 'Foundry.Connect-win-arm64.zip'), | |
| (Join-Path $releaseRoot 'Foundry.Deploy-win-x64.zip'), | |
| (Join-Path $releaseRoot 'Foundry.Deploy-win-arm64.zip') | |
| ) | |
| foreach ($asset in $assets) { | |
| if (-not (Test-Path -Path $asset -PathType Leaf)) { | |
| throw "Release asset was not created: '$asset'." | |
| } | |
| } | |
| gh release upload $releaseTag @assets --clobber |