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
78 changes: 78 additions & 0 deletions .github/workflows/bump-deps.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: 'Bump dependencies'

on:
workflow_dispatch:
inputs:
allowed-licenses:
description: 'Comma-separated SPDX allowlist'
required: false
default: 'MIT,Apache-2.0'
dry-run:
description: 'Dry run (no commits, no PR)'
required: false
default: 'false'
schedule:
# Weekly, Mondays at 06:00 UTC
- cron: '0 6 * * 1'

permissions:
contents: write
pull-requests: write

jobs:
bump:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

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

- name: Run bump script
id: bump
shell: pwsh
run: |
$licenses = '${{ inputs.allowed-licenses || ''MIT,Apache-2.0'' }}' -split ','
$dryRun = '${{ inputs.dry-run }}' -eq 'true'
$args = @{
AllowedLicenses = $licenses
GithubOutput = $true
}
if ($dryRun) { $args.DryRun = $true }
./scripts/Bump-Deps.ps1 @args

- name: Verify build
if: steps.bump.outputs.bumped == 'true' && inputs.dry-run != 'true'
run: |
dotnet restore TailoredApps.Shared.sln
dotnet build TailoredApps.Shared.sln --no-restore --nologo

- name: Create pull request
if: steps.bump.outputs.bumped == 'true' && inputs.dry-run != 'true'
uses: peter-evans/create-pull-request@v7
with:
commit-message: 'chore(deps): automated dependency bump'
title: 'chore(deps): automated dependency bump'
body: |
Automated bump produced by `.github/workflows/bump-deps.yml`.

Allowed licenses: `${{ inputs.allowed-licenses || 'MIT,Apache-2.0' }}`

### Bumps and skips
See workflow run log for the full BUMP/SKIP report.
Packages with license drift or licenses outside the allowlist are skipped automatically.

### Test plan
- [x] `dotnet restore` (workflow)
- [x] `dotnet build` (workflow)
- [ ] CI green
- [ ] Spot-check API surface for any breaking changes
branch: chore/auto-bump-deps
base: master
delete-branch: true
labels: dependencies, automated
245 changes: 245 additions & 0 deletions scripts/Bump-Deps.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
<#
.SYNOPSIS
Bump NuGet PackageReference versions to latest stable, with license-awareness.

.DESCRIPTION
Scans all *.csproj files, queries nuget.org for latest stable versions, and
rewrites <PackageReference Version="..."> values when safe.

Rules:
- Wildcard versions (e.g. "10.*", "2.*") are left alone — they self-update at restore.
- The latest version's SPDX license expression must be in -AllowedLicenses.
- If the SPDX license of the currently-pinned version differs from the latest's,
the bump is SKIPPED (license-change rule — catches relicensing like MediatR).
- Packages with no declared license expression are skipped by default
(override with -AllowUndeclaredLicense).
- Pre-release versions are ignored.

.PARAMETER AllowedLicenses
SPDX expressions considered acceptable. Defaults to MIT and Apache-2.0.

.PARAMETER AllowUndeclaredLicense
If set, packages whose latest version has no licenseExpression are bumped anyway.

.PARAMETER DryRun
Print the bump plan but do not modify files.

.PARAMETER Path
Root path to scan. Defaults to current directory.

.PARAMETER GithubOutput
If set, also writes a summary line to $env:GITHUB_OUTPUT for CI consumption
(key: bumped, value: true|false).

.PARAMETER Ignore
Hashtable of package name -> max allowed version (exclusive ceiling) or $null
to block all bumps. Used to enforce known license-change cutoffs that the
SPDX check cannot detect (e.g. MediatR 13+ switched to RPL-1.5 without
declaring SPDX). Names are matched case-insensitively.

.EXAMPLE
.\scripts\Bump-Deps.ps1 -DryRun
.EXAMPLE
.\scripts\Bump-Deps.ps1 -AllowedLicenses MIT,Apache-2.0,BSD-3-Clause
#>
[CmdletBinding()]
param(
[string[]]$AllowedLicenses = @('MIT', 'Apache-2.0'),
[switch]$AllowUndeclaredLicense,
[switch]$DryRun,
[string]$Path = '.',
[switch]$GithubOutput,
[hashtable]$Ignore = @{
# MediatR 13+ relicensed Apache-2.0 -> RPL-1.5 (Lucky Penny Software).
# SPDX is not published, so the license-change rule cannot catch it.
'MediatR' = '13.0.0'
}
)

# Normalize ignore keys to lowercase for case-insensitive lookup.
$normalizedIgnore = @{}
foreach ($k in $Ignore.Keys) { $normalizedIgnore[$k.ToLowerInvariant()] = $Ignore[$k] }
$Ignore = $normalizedIgnore

$ErrorActionPreference = 'Stop'

$script:cache = @{}
function Get-PackageInfo {
param([string]$Name)
if ($script:cache.ContainsKey($Name)) { return $script:cache[$Name] }
$lower = $Name.ToLowerInvariant()
$url = "https://api.nuget.org/v3/registration5-gz-semver2/$lower/index.json"
try {
$info = Invoke-RestMethod -Uri $url -ErrorAction Stop
} catch {
$info = $null
}
$script:cache[$Name] = $info
return $info
}

function Get-CatalogEntries {
param($Info)
$entries = @()
foreach ($page in $Info.items) {
# Some pages are inlined; others must be fetched.
if ($page.items) {
$entries += $page.items
} else {
try {
$inner = Invoke-RestMethod -Uri $page.'@id' -ErrorAction Stop
if ($inner.items) { $entries += $inner.items }
} catch { }
}
}
return $entries
}

function Get-LatestStableEntry {
param($Entries)
$stable = $Entries |
Where-Object { $_.catalogEntry.listed -ne $false -and $_.catalogEntry.version -notmatch '-' }
if (-not $stable) { return $null }
return ($stable |
Sort-Object {
$v = $_.catalogEntry.version -replace '\+.*$',''
try { [version]$v } catch { [version]'0.0.0.0' }
} |
Select-Object -Last 1).catalogEntry
}

function Get-LicenseForVersion {
param($Entries, [string]$Version)
foreach ($e in $Entries) {
if ($e.catalogEntry.version -eq $Version) { return $e.catalogEntry.licenseExpression }
}
return $null
}

$root = Resolve-Path $Path
$csprojs = Get-ChildItem -Path $root -Recurse -Filter *.csproj
Write-Host "Scanning $($csprojs.Count) csproj files under $root" -ForegroundColor Cyan

$bumpedAny = $false
$summary = @()

foreach ($file in $csprojs) {
$xml = New-Object System.Xml.XmlDocument
$xml.PreserveWhitespace = $true
$xml.Load($file.FullName)

$changed = $false
$refs = $xml.SelectNodes('//PackageReference')
foreach ($ref in $refs) {
$name = $ref.GetAttribute('Include')
if (-not $name) { continue }
$cur = $ref.GetAttribute('Version')
if (-not $cur) { continue }
if ($cur -match '\*') { continue } # leave wildcards alone

$lname = $name.ToLowerInvariant()
if ($Ignore.ContainsKey($lname)) {
$ceiling = $Ignore[$lname]
if ($null -eq $ceiling) {
Write-Host "SKIP $name (ignore list, all bumps blocked)" -ForegroundColor Yellow
$summary += "SKIP $name (ignore list)"
continue
}
}

$info = Get-PackageInfo -Name $name
if (-not $info) {
Write-Warning "no nuget metadata for $name (skipping)"
continue
}
$entries = Get-CatalogEntries -Info $info
if (-not $entries) {
Write-Warning "no catalog entries for $name (skipping)"
continue
}
$latest = Get-LatestStableEntry -Entries $entries
if (-not $latest) { continue }

# Apply ignore-list version ceiling, if any.
if ($Ignore.ContainsKey($lname) -and $null -ne $Ignore[$lname]) {
$ceiling = [version]($Ignore[$lname])
try { $latestVer = [version]($latest.version -replace '\+.*$','') } catch { $latestVer = $null }
if ($latestVer -and $latestVer -ge $ceiling) {
# Latest is at or above ceiling — find highest stable strictly below it.
$belowCeiling = $entries |
Where-Object {
$_.catalogEntry.listed -ne $false -and
$_.catalogEntry.version -notmatch '-'
} |
Where-Object {
try { ([version]($_.catalogEntry.version -replace '\+.*$','')) -lt $ceiling } catch { $false }
} |
Sort-Object {
try { [version]($_.catalogEntry.version -replace '\+.*$','') } catch { [version]'0.0.0.0' }
} |
Select-Object -Last 1
if (-not $belowCeiling) {
Write-Host "SKIP $name ${cur} -> $($latest.version): ignore-list ceiling $ceiling, no version below it" -ForegroundColor Yellow
$summary += "SKIP $name (no version below ignore ceiling $ceiling)"
continue
}
Write-Host "CLAMP $name latest=$($latest.version) -> $($belowCeiling.catalogEntry.version) (ignore ceiling $ceiling)" -ForegroundColor DarkYellow
$latest = $belowCeiling.catalogEntry
}
}

if ($latest.version -eq $cur) { continue }

$curLic = Get-LicenseForVersion -Entries $entries -Version $cur
$latestLic = $latest.licenseExpression

if (-not $latestLic) {
if (-not $AllowUndeclaredLicense) {
Write-Host "SKIP $name ${cur} -> $($latest.version): no declared license" -ForegroundColor Yellow
$summary += "SKIP $name -> $($latest.version) (no declared license)"
continue
}
} elseif ($curLic -and $curLic -ne $latestLic) {
Write-Host "SKIP $name ${cur} -> $($latest.version): license changed ($curLic -> $latestLic)" -ForegroundColor Yellow
$summary += "SKIP $name -> $($latest.version) (license $curLic -> $latestLic)"
continue
} elseif ($latestLic -notin $AllowedLicenses) {
Write-Host "SKIP $name ${cur} -> $($latest.version): license $latestLic not in allowlist" -ForegroundColor Yellow
$summary += "SKIP $name -> $($latest.version) (license $latestLic not allowed)"
continue
}

Write-Host "BUMP $name ${cur} -> $($latest.version) ($latestLic) in $($file.Name)" -ForegroundColor Green
$summary += "BUMP $name $cur -> $($latest.version) ($latestLic)"
$ref.SetAttribute('Version', $latest.version)
$changed = $true
$bumpedAny = $true
}

if ($changed -and -not $DryRun) {
# Preserve file encoding (utf-8 no BOM is the .NET default for csproj).
$settings = New-Object System.Xml.XmlWriterSettings
$settings.OmitXmlDeclaration = -not $xml.FirstChild.NodeType.Equals([System.Xml.XmlNodeType]::XmlDeclaration)
$settings.Encoding = New-Object System.Text.UTF8Encoding($false)
$settings.Indent = $false
$writer = [System.Xml.XmlWriter]::Create($file.FullName, $settings)
try { $xml.Save($writer) } finally { $writer.Dispose() }
}
}

Write-Host ""
Write-Host "=== Summary ===" -ForegroundColor Cyan
if ($summary.Count -eq 0) {
Write-Host "No changes." -ForegroundColor Gray
} else {
$summary | ForEach-Object { Write-Host $_ }
}

if ($GithubOutput -and $env:GITHUB_OUTPUT) {
"bumped=$($bumpedAny.ToString().ToLower())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
$sumPath = Join-Path ([System.IO.Path]::GetTempPath()) "bump-summary.txt"
$summary | Out-File -FilePath $sumPath -Encoding utf8
"summary-file=$sumPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
}

if ($DryRun) { Write-Host "(dry run — no files modified)" -ForegroundColor Gray }
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MailKit" Version="4.15.1" />
<PackageReference Include="MimeKit" Version="4.15.1" />
<PackageReference Include="Microsoft.Identity.Web" Version="3.11.0" />
<PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference Include="MimeKit" Version="4.16.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="4.9.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.*" Condition="'$(TargetFramework)' == 'net3.1'" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.*" Condition="'$(TargetFramework)' == 'net3.1'" />

<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.6" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.7.2" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.*" />
<PackageReference Include="xunit" Version="2.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild" Version="6.*">
<PackageReference Include="coverlet.msbuild" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MimeKit" Version="4.15.1" />
<PackageReference Include="MimeKit" Version="4.16.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.*" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.*" />


<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.*" />
<PackageReference Include="xunit" Version="2.*" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild" Version="6.*">
<PackageReference Include="coverlet.msbuild" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Loading
Loading