Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/Integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ jobs:
with:
dotnet-version: '10.0.x'
run-tests: true

artifact-contracts:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Test fail-closed release artifact contracts
shell: pwsh
run: .\scripts\test-release-artifact-contract.ps1
64 changes: 18 additions & 46 deletions .github/workflows/NuGetPublish.yml
Original file line number Diff line number Diff line change
@@ -1,77 +1,49 @@
name: Publish to NuGet.org
name: Publish validated release to NuGet.org

on:
release:
types: [published]

permissions:
contents: read
actions: read
id-token: write

concurrency:
group: nuget-${{ github.event.release.tag_name }}
cancel-in-progress: false

jobs:
publish:
if: ${{ !github.event.release.prerelease && !github.event.release.draft }}
runs-on: windows-latest
timeout-minutes: 90

timeout-minutes: 30
steps:
- name: Checkout
- name: Checkout release tag
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
fetch-depth: 0

- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'

- name: Extract version from tag
shell: pwsh
run: |
$version = $env:GITHUB_REF_NAME -replace '^v', ''
"VERSION=$version" >> $env:GITHUB_ENV

- name: Restore
run: dotnet restore WPF-Framework.sln

- name: Build
- name: Download and validate exact downstream-approved artifact
shell: pwsh
run: dotnet build WPF-Framework.sln -c Release --no-restore /p:Version=$env:VERSION

- name: Test
env:
VSTEST_CONNECTION_TIMEOUT: '600'
run: dotnet test WPF-Framework.sln -c Release --no-build

- name: Pack
GITHUB_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: .\scripts\download-validated-release.ps1 -Repository $env:GITHUB_REPOSITORY -Tag $env:RELEASE_TAG -Directory validated-packages
- name: Verify existing package versions before login
shell: pwsh
run: .\scripts\pack-wpf-framework.ps1 -Configuration Release -Version $env:VERSION -OutputDirectory packages -SkipRestore -SkipBuild

run: .\scripts\publish-validated-release.ps1 -Directory validated-packages -ReceiptPath downstream-validation.json
- name: NuGet login
uses: NuGet/login@v1
id: login
with:
user: USACE-RMC

- name: Push to NuGet.org
- name: Publish validated packages
shell: pwsh
env:
NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }}
run: |
$packageIds = @(
"RMC.Wpf.Framework.Core",
"RMC.Wpf.Framework.Models",
"RMC.Wpf.Framework.Support",
"RMC.Wpf.Framework.Controls"
)

foreach ($packageId in $packageIds) {
$packagePath = Join-Path "packages" "$packageId.$env:VERSION.nupkg"
if (-not (Test-Path $packagePath)) {
throw "Missing package: $packagePath"
}

dotnet nuget push $packagePath --api-key $env:NUGET_API_KEY --source "https://api.nuget.org/v3/index.json" --skip-duplicate
if ($LASTEXITCODE -ne 0) {
throw "NuGet push failed for $packagePath."
}
}
run: .\scripts\publish-validated-release.ps1 -Directory validated-packages -ReceiptPath downstream-validation.json -Push
15 changes: 12 additions & 3 deletions .github/workflows/Release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,18 @@ jobs:
shell: pwsh
run: .\scripts\pack-wpf-framework.ps1 -Configuration Release -Version $env:VERSION -OutputDirectory packages -SkipRestore -SkipBuild

- name: Validate artifact contracts
shell: pwsh
run: .\scripts\test-release-artifact-contract.ps1

- name: Write immutable release manifest
shell: pwsh
run: .\scripts\write-release-manifest.ps1 -Directory packages -Repository $env:GITHUB_REPOSITORY -Tag $env:GITHUB_REF_NAME -Version $env:VERSION -CommitSHA $env:GITHUB_SHA -RunID $env:GITHUB_RUN_ID -RunAttempt $env:GITHUB_RUN_ATTEMPT

- name: Upload packages
uses: actions/upload-artifact@v4
with:
name: wpf-framework-packages
path: packages/*.nupkg
retention-days: 30
name: wpf-framework-packages-${{ github.run_id }}-${{ github.run_attempt }}
path: packages/*
if-no-files-found: error
retention-days: 90
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ cff-version: 1.2.0
message: "If you use this software, please cite it as below."
type: software
title: "WPF Framework"
version: "1.0.4"
date-released: "2026-07-17"
version: "1.0.5"
date-released: "2026-10-07"
license: 0BSD
repository-code: "https://github.com/USACE-RMC/WPF-Framework"
url: "https://github.com/USACE-RMC/WPF-Framework"
Expand Down
6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<PackageProjectUrl>https://github.com/USACE-RMC/WPF-Framework</PackageProjectUrl>
<RepositoryUrl>https://github.com/USACE-RMC/WPF-Framework</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Version>1.0.4</Version>
<AssemblyVersion>1.0.4.0</AssemblyVersion>
<FileVersion>1.0.4.0</FileVersion>
<Version>1.0.5</Version>
<AssemblyVersion>1.0.5.0</AssemblyVersion>
<FileVersion>1.0.5.0</FileVersion>

<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<IsPackable>false</IsPackable>
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<ExcelNumberFormatVersion>1.1</ExcelNumberFormatVersion>
<FastMemberVersion>1.5.0</FastMemberVersion>
<MicrosoftNetTestSdkVersion>17.12.0</MicrosoftNetTestSdkVersion>
<MicrosoftSourceLinkGitHubVersion>10.0.102</MicrosoftSourceLinkGitHubVersion>
<MicrosoftSourceLinkGitHubVersion>10.0.111</MicrosoftSourceLinkGitHubVersion>
<MSTestTestAdapterVersion>3.5.2</MSTestTestAdapterVersion>
<MSTestTestFrameworkVersion>3.5.2</MSTestTestFrameworkVersion>
<NSubstituteVersion>4.2.1</NSubstituteVersion>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ dotnet test WPF-Framework.sln
### Build Packages

```bash
.\scripts\pack-wpf-framework.ps1 -Configuration Release -Version 1.0.4
.\scripts\pack-wpf-framework.ps1 -Configuration Release -Version 1.0.5
```

This creates and validates the following NuGet packages in `artifacts/packages/`:
Expand Down
4 changes: 2 additions & 2 deletions codemeta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"@type": "SoftwareSourceCode",
"name": "WPF Framework",
"description": "A free and open-source .NET 10 application framework for building Windows desktop project management applications. Provides a docking application shell with theming, project explorer, undo/redo, and specialized controls for charting, numeric input, databases, expression parsing, and directed acyclic graphs.",
"version": "1.0.4",
"version": "1.0.5",
"dateCreated": "2025-12-29",
"dateModified": "2026-07-17",
"dateModified": "2026-10-07",
"license": "https://spdx.org/licenses/0BSD",
"codeRepository": "https://github.com/USACE-RMC/WPF-Framework",
"issueTracker": "https://github.com/USACE-RMC/WPF-Framework/issues",
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ For NuGet consumption, reference the Controls bundle. It brings in Core, Models,

```xml
<ItemGroup>
<PackageReference Include="RMC.Wpf.Framework.Controls" Version="1.0.4" />
<PackageReference Include="RMC.Wpf.Framework.Controls" Version="1.0.5" />
</ItemGroup>
```

Expand Down
49 changes: 49 additions & 0 deletions scripts/download-validated-release.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<#
.SYNOPSIS
Downloads only the receipt-approved successful Release artifact and verifies its content.
#>
param(
[Parameter(Mandatory)][string]$Repository,
[Parameter(Mandatory)][string]$Tag,
[Parameter(Mandatory)][string]$Directory
)
. (Join-Path $PSScriptRoot 'release-artifact-contract.ps1')
# The current checkout is the release tag; resolve annotated tags to their commit.
$sha = & git rev-parse "$Tag^{commit}"
if ($LASTEXITCODE -ne 0) { throw 'Cannot peel release tag.' }
$head = & git rev-parse HEAD
if ($LASTEXITCODE -ne 0) { throw 'Cannot resolve checkout.' }
Assert-ReleaseEqual $head $sha 'Checkout/tag SHA'
if (Test-Path -LiteralPath $Directory) { throw 'Use a new empty artifact destination.' }
New-Item -ItemType Directory -Path $Directory | Out-Null
$headers = @{ Authorization = "Bearer $env:GITHUB_TOKEN"; Accept = 'application/vnd.github+json'; 'X-GitHub-Api-Version' = '2022-11-28' }
$base = "https://api.github.com/repos/$Repository"
$release = Invoke-RestMethod "$base/releases/tags/$([Uri]::EscapeDataString($Tag))" -Headers $headers
if ($release.draft -or $release.prerelease) { throw 'Only a published stable release is allowed.' }
$assets = @($release.assets | Where-Object { $_.name -ceq 'downstream-validation.json' })
if ($assets.Count -ne 1) { throw 'Exactly one downstream-validation.json release asset is required.' }
$receiptPath = Join-Path $Directory '..' 'downstream-validation.json'
$downloadHeaders = $headers.Clone(); $downloadHeaders.Accept = 'application/octet-stream'
Invoke-WebRequest "$base/releases/assets/$($assets[0].id)" -Headers $downloadHeaders -OutFile $receiptPath | Out-Null
$receipt = Read-ReleaseJson $receiptPath
Assert-ReleaseReceipt $receipt $Repository $Tag $sha
# Fetch exactly the recorded attempt, never the most recent run or artifact with a similar name.
$run = Invoke-RestMethod "$base/actions/runs/$($receipt.runID)/attempts/$($receipt.runAttempt)" -Headers $headers
$artifact = Invoke-RestMethod "$base/actions/artifacts/$($receipt.artifactID)" -Headers $headers
Assert-ReleaseRun $receipt $run $artifact $Repository $Tag $sha $release
$archivePath = Join-Path $Directory '..' 'release-artifact.zip'
Invoke-WebRequest "$base/actions/artifacts/$($receipt.artifactID)/zip" -Headers $headers -OutFile $archivePath | Out-Null
$archive = [IO.Compression.ZipFile]::OpenRead($archivePath)
try {
$seen = @{}
$allowed = @('release-manifest.json') + @($receipt.packages | ForEach-Object { $_.filename })
if ($archive.Entries.Count -ne 5) { throw 'Unexpected artifact ZIP entry count.' }
foreach ($entry in $archive.Entries) {
if ($allowed -cnotcontains $entry.FullName -or $seen.ContainsKey($entry.FullName)) { throw 'Unexpected/duplicate artifact ZIP entry.' }
$seen[$entry.FullName] = $true
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, (Join-Path $Directory $entry.FullName), $false)
}
}
finally { $archive.Dispose() }
$null = Assert-ReleaseArtifact $Directory $receipt (Split-Path $PSScriptRoot)
Write-Host "Validated exact artifact $($receipt.artifactID), run $($receipt.runID) attempt $($receipt.runAttempt), tag $Tag, SHA $sha."
8 changes: 4 additions & 4 deletions scripts/pack-wpf-framework.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ $expectedVersionedEntries = @{
}

$expectedReleaseNotes = @{
"RMC.Wpf.Framework.Core" = "Coordinated WPF Framework 1.0.4 release; no package-specific functional changes."
"RMC.Wpf.Framework.Models" = "Coordinated WPF Framework 1.0.4 release; no package-specific functional changes."
"RMC.Wpf.Framework.Support" = "Coordinated WPF Framework 1.0.4 release; no package-specific functional changes."
"RMC.Wpf.Framework.Controls" = "Version 1.0.4 updates RMC.Numerics to 2.1.4 and improves NumericControls validation and distribution selector compatibility."
"RMC.Wpf.Framework.Core" = "Version 1.0.5 keeps element display names synchronized when names change during undo or redo."
"RMC.Wpf.Framework.Models" = "Version 1.0.5 releases SQLite connections when initialization or database operations fail."
"RMC.Wpf.Framework.Support" = "Coordinated WPF Framework 1.0.5 release; no package-specific functional changes."
"RMC.Wpf.Framework.Controls" = "Version 1.0.5 keeps compaction active until database work completes, reports maintenance errors, and updates FrameworkUI demo workflows."
}

$expectedPackageDependencies = @{
Expand Down
56 changes: 56 additions & 0 deletions scripts/publish-validated-release.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<#
.SYNOPSIS
Preflights immutable package versions and optionally pushes only validated absent packages.
#>
param(
[Parameter(Mandatory)][string]$Directory,
[Parameter(Mandatory)][string]$ReceiptPath,
[switch]$Push
)
. (Join-Path $PSScriptRoot 'release-artifact-contract.ps1')
$receipt = Read-ReleaseJson $ReceiptPath
Assert-ReleaseReceipt $receipt $receipt.repository $receipt.frameworkTag $receipt.frameworkTagSHA
$manifest = Assert-ReleaseArtifact $Directory $receipt (Split-Path $PSScriptRoot)
$pending = @()
$temp = Join-Path ([IO.Path]::GetTempPath()) "nuget-existing-$([Guid]::NewGuid().ToString('N'))"
New-Item -ItemType Directory -Path $temp | Out-Null
try {
# Preflight every version before any push. All network errors other than 404 are fatal.
foreach ($package in $manifest.packages) {
$id = $package.id.ToLowerInvariant()
$version = $receipt.version.ToLowerInvariant()
$url = "https://api.nuget.org/v3-flatcontainer/$id/$version/$id.$version.nupkg"
$published = Join-Path $temp $package.filename
$exists = $true
try { Invoke-WebRequest $url -OutFile $published | Out-Null }
catch {
if ($null -ne $_.Exception.Response -and [int]$_.Exception.Response.StatusCode -eq 404) { $exists = $false }
else { throw }
}
$candidate = Join-Path $Directory $package.filename
if ($exists) {
Assert-PublishedPackageMatches $candidate $published {
param($path)
& dotnet nuget verify $path --all
if ($LASTEXITCODE -ne 0) { throw 'Published NuGet signature verification failed.' }
}
Write-Host "Already published with matching signed payload: $($package.filename)"
}
else { $pending += $candidate }
}
if ($Push) {
if ([string]::IsNullOrWhiteSpace($env:NUGET_API_KEY)) { throw 'Missing trusted NuGet API key.' }
foreach ($path in $pending) {
# No skip-duplicate: a concurrent conflicting publish must fail and be checked on a rerun.
& dotnet nuget push $path --api-key $env:NUGET_API_KEY --source 'https://api.nuget.org/v3/index.json'
if ($LASTEXITCODE -ne 0) { throw "NuGet publish failed: $path" }
}
}
else { Write-Host "Publication preflight passed; $($pending.Count) packages require publishing." }
}
finally {
$resolved = [IO.Path]::GetFullPath($temp)
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd([IO.Path]::DirectorySeparatorChar)
if ((Split-Path $resolved -Parent) -ne $tempRoot -or (Split-Path $resolved -Leaf) -notmatch '^nuget-existing-[a-f0-9]{32}$') { throw 'Unsafe temporary cleanup target.' }
Remove-Item -LiteralPath $resolved -Recurse -Force
}
Loading
Loading