Release #29
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 13 * * 2' | |
| workflow_dispatch: | |
| inputs: | |
| force: | |
| description: Create a release even if no commits exist since the latest published release | |
| required: false | |
| default: false | |
| type: boolean | |
| draft: | |
| description: Create the GitHub release as a draft | |
| required: false | |
| default: true | |
| type: boolean | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| jobs: | |
| prepare-release: | |
| name: Prepare Release | |
| runs-on: ubuntu-slim | |
| if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' | |
| outputs: | |
| should_release: ${{ steps.compute.outputs.should_release }} | |
| release_tag: ${{ steps.compute.outputs.release_tag }} | |
| release_name: ${{ steps.compute.outputs.release_name }} | |
| release_version: ${{ steps.compute.outputs.release_version }} | |
| velopack_version: ${{ steps.compute.outputs.velopack_version }} | |
| draft_release: ${{ steps.compute.outputs.draft_release }} | |
| 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' }} | |
| DRAFT_RELEASE: ${{ inputs.draft && '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.' | |
| } | |
| $changedSourceFiles = if ([string]::IsNullOrWhiteSpace($latestReleaseTag)) { | |
| @(git ls-files 'src') | |
| } | |
| else { | |
| @(git diff --name-only "$latestReleaseTag..HEAD" -- src) | |
| } | |
| $sourceChangeCount = $changedSourceFiles.Count | |
| $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" | |
| $releaseVersion = $releaseTag.Substring(1) | |
| $versionParts = $releaseVersion.Split('.') | |
| $velopackVersion = '{0}.{1}.{2}-build.{3}' -f $versionParts[0], $versionParts[1], $versionParts[2], $versionParts[3] | |
| $releaseName = "Foundry $releaseTag" | |
| $isManualDispatch = $env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' | |
| $shouldRelease = $sourceChangeCount -gt 0 -or $env:FORCE_RELEASE -eq 'true' -or $isManualDispatch | |
| "should_release=$($shouldRelease.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT | |
| "release_tag=$releaseTag" >> $env:GITHUB_OUTPUT | |
| "release_name=$releaseName" >> $env:GITHUB_OUTPUT | |
| "release_version=$releaseVersion" >> $env:GITHUB_OUTPUT | |
| "velopack_version=$velopackVersion" >> $env:GITHUB_OUTPUT | |
| "draft_release=$env:DRAFT_RELEASE" >> $env:GITHUB_OUTPUT | |
| if ($shouldRelease) { | |
| Write-Host "Release will be created with tag '$releaseTag'." | |
| Write-Host "Velopack package version will be '$velopackVersion'." | |
| Write-Host "Detected $sourceChangeCount source file change(s) since the latest published release." | |
| } | |
| else { | |
| Write-Host "No source changes 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: | | |
| $releaseArgs = @( | |
| '${{ steps.compute.outputs.release_tag }}', | |
| '--title', '${{ steps.compute.outputs.release_name }}', | |
| '--target', '${{ github.sha }}', | |
| '--generate-notes' | |
| ) | |
| if ('${{ steps.compute.outputs.draft_release }}' -eq 'true') { | |
| $releaseArgs += '--draft' | |
| } | |
| gh release create @releaseArgs | |
| 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@v5 | |
| 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 = '${{ needs.prepare-release.outputs.release_version }}' | |
| $velopackVersion = '${{ needs.prepare-release.outputs.velopack_version }}' | |
| $expectedVelopackVersion = '{0}.{1}.{2}-build.{3}' -f $match.Groups['year'].Value, $match.Groups['month'].Value, $match.Groups['day'].Value, $match.Groups['build'].Value | |
| if ($velopackVersion -ne $expectedVelopackVersion) { | |
| throw "Velopack version '$velopackVersion' does not match expected value '$expectedVelopackVersion'." | |
| } | |
| $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" | |
| Add-Content -Path $env:GITHUB_ENV -Value "VELOPACK_VERSION=$velopackVersion" | |
| - name: Prepare release notes | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| $releaseTag = '${{ needs.prepare-release.outputs.release_tag }}' | |
| $releaseNotesPath = Join-Path $env:GITHUB_WORKSPACE 'artifacts\release-notes.md' | |
| New-Item -Path (Split-Path -Parent $releaseNotesPath) -ItemType Directory -Force | Out-Null | |
| $body = gh release view $releaseTag --json body --jq '.body' | |
| if ([string]::IsNullOrWhiteSpace($body)) { | |
| $body = "No release notes were generated." | |
| } | |
| Set-Content -Path $releaseNotesPath -Value "# Foundry OSD $releaseTag`n`n$body" -NoNewline | |
| Add-Content -Path $env:GITHUB_ENV -Value "RELEASE_NOTES_PATH=$releaseNotesPath" | |
| - name: Build release assets | |
| shell: pwsh | |
| run: | | |
| $workspace = $env:GITHUB_WORKSPACE | |
| $releaseRoot = Join-Path $workspace 'artifacts\release' | |
| $publishRoot = Join-Path $releaseRoot 'publish' | |
| $connectProject = Join-Path $workspace 'src\Foundry.Connect\Foundry.Connect.csproj' | |
| $deployProject = Join-Path $workspace 'src\Foundry.Deploy\Foundry.Deploy.csproj' | |
| $velopackScript = Join-Path $workspace 'scripts\Publish-FoundryVelopack.ps1' | |
| $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]$Platform, | |
| [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', | |
| "-p:Platform=$Platform" | |
| ) | |
| 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')) { | |
| $platform = if ($runtimeIdentifier -eq 'win-x64') { 'x64' } else { 'ARM64' } | |
| powershell -ExecutionPolicy Bypass -File $velopackScript ` | |
| -RuntimeIdentifier $runtimeIdentifier ` | |
| -PackVersion $env:VELOPACK_VERSION ` | |
| -ReleaseNotesPath $env:RELEASE_NOTES_PATH | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Velopack packaging failed for Foundry ($runtimeIdentifier)." | |
| } | |
| $deployPublishPath = Join-Path $publishRoot (Join-Path 'Foundry.Deploy' $runtimeIdentifier) | |
| Invoke-Publish -ProjectPath $deployProject -RuntimeIdentifier $runtimeIdentifier -Platform $platform -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 -Platform $platform -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 }}' | |
| $releaseName = '${{ needs.prepare-release.outputs.release_name }}' | |
| $releaseRoot = Join-Path $env:GITHUB_WORKSPACE 'artifacts\release' | |
| $velopackRoot = Join-Path $env:GITHUB_WORKSPACE 'artifacts\velopack' | |
| $vpkExecutable = Join-Path $env:GITHUB_WORKSPACE 'artifacts\tools\vpk\vpk.exe' | |
| if (-not (Test-Path -Path $vpkExecutable -PathType Leaf)) { | |
| throw "Velopack CLI was not installed: '$vpkExecutable'." | |
| } | |
| foreach ($runtimeIdentifier in @('win-x64', 'win-arm64')) { | |
| $velopackReleasePath = Join-Path $velopackRoot (Join-Path 'releases' $runtimeIdentifier) | |
| if (-not (Test-Path -Path $velopackReleasePath -PathType Container)) { | |
| throw "Velopack release output was not created: '$velopackReleasePath'." | |
| } | |
| & $vpkExecutable upload github ` | |
| --outputDir $velopackReleasePath ` | |
| --channel $runtimeIdentifier ` | |
| --repoUrl "https://github.com/${{ github.repository }}" ` | |
| --token $env:GH_TOKEN ` | |
| --merge true ` | |
| --releaseName $releaseName ` | |
| --tag $releaseTag | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Velopack GitHub upload failed for '$runtimeIdentifier'." | |
| } | |
| $setupAssetNames = gh release view $releaseTag --json assets --jq '.assets[].name' | | |
| Where-Object { $_ -like "*-$runtimeIdentifier-Setup.exe" } | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Unable to inspect GitHub release assets for '$releaseTag'." | |
| } | |
| foreach ($setupAssetName in $setupAssetNames) { | |
| gh release delete-asset $releaseTag $setupAssetName --yes | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "Unable to delete GitHub release asset '$setupAssetName'." | |
| } | |
| } | |
| } | |
| $assets = @( | |
| (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 |