Skip to content

Release

Release #20

Workflow file for this run

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