Skip to content
Open
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
55 changes: 49 additions & 6 deletions scripts/lib/release-flow.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -776,13 +776,56 @@ function Write-Changelog {

$currentDate = (Get-Date).ToString('yyyy-MM-dd')

# Get commits since the latest tag (unreleased commits)
# Get commits since the latest tag (unreleased commits) that touched this package, together
# with the per-commit list of files each one changed within the folder. A single `git log
# --name-only` does this in one process invocation (rather than a `git show` per commit), so
# releases with large ranges stay fast. Each commit record is prefixed with a record separator
# (0x1e); within the header the hash and subject are split by a unit separator (0x1f); the
# file list then follows on subsequent lines (emitted by --name-only).
$range = if ($latestTag) { "$latestTag..HEAD" } else { "HEAD" }
$rawCommits = Invoke-Git -Arguments @('log', $range, '--pretty=format:%s', '--', $packageFolder)
if ($null -eq $rawCommits -or $rawCommits.Count -eq 0) {
$rawCommits = @()
} else {
$rawCommits = @($rawCommits)
$recordSep = [char]0x1e
$unitSep = [char]0x1f
$logOutput = Invoke-Git -Arguments @('log', $range, '--name-only', "--pretty=format:${recordSep}%H${unitSep}%s", '--', $packageFolder)
$logText = @($logOutput) -join "`n"

# Files in a package folder that are produced or maintained automatically rather than by a
# human authoring a change. README.md is regenerated by `just readme`, which rewrites every
# crate's README workspace-wide; an unrelated commit (e.g. one that introduced a *different*
# package) therefore "touches" this folder solely through that regeneration. CHANGELOG.md is
# release bookkeeping. A commit whose ONLY changes within this folder are such files did not
# actually change the package, so its subject must not leak into the changelog.
#
# Only the crate-ROOT README.md / CHANGELOG.md are auto-maintained. Nested files are
# hand-authored and must still count as meaningful changes — including ones that merely share
# the leaf name (crates/<pkg>/examples/README.md) AND ones that also share the parent leaf
# (crates/<pkg>/<pkg>/README.md). Matching on the parent leaf alone misclassifies the latter,
# so we build the exact repo-relative crate-root paths and require a full-path match.
#
# git emits repo-relative, forward-slash paths regardless of platform. The package's
# repo-relative directory is its last two path segments (crates/<pkg>); deriving it this way
# avoids hard-coding the 'crates' prefix while still anchoring the match to the crate root.
$packageSegments = $packageFolder -split '[\\/]' | Where-Object { $_ }
$packageRelDir = ($packageSegments | Select-Object -Last 2) -join '/'
$autoMaintainedRootPaths = @('README.md', 'CHANGELOG.md') | ForEach-Object { "$packageRelDir/$_" }

$rawCommits = @()
foreach ($record in ($logText -split $recordSep)) {
if ([string]::IsNullOrWhiteSpace($record)) { continue }

$header, $fileBlock = $record -split "`n", 2
$subject = ($header -split $unitSep, 2)[-1].Trim()
$changedFiles = @(($fileBlock -split "`n") | ForEach-Object { $_.Trim() } | Where-Object { $_ })

$hasMeaningfulChange = $false
foreach ($file in $changedFiles) {
if ($autoMaintainedRootPaths -notcontains $file) {
$hasMeaningfulChange = $true
break
}
}
if ($hasMeaningfulChange) {
$rawCommits += $subject
}
Comment on lines +826 to +828
}

$formattedCommits = @()
Expand Down
100 changes: 100 additions & 0 deletions scripts/tests/Pester/unit/releasing/WriteChangelog.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,103 @@ initial
}
}
}

Describe 'Write-Changelog commit filtering' {
# Regression: Write-Changelog must not attribute a commit to a package's
# changelog when the commit's ONLY change inside the package folder is an
# auto-maintained crate-root file (README.md / CHANGELOG.md). README.md is
# regenerated workspace-wide by `just readme`, so an unrelated commit can
# "touch" this package's folder solely through that regeneration.
#
# The single `git log --name-only` invocation is mocked. Its output encodes,
# per commit, a record separator (0x1e), the hash, a unit separator (0x1f),
# the subject, then the changed-file paths on following lines.

BeforeEach {
Mock -CommandName Get-Date -MockWith { [datetime]'2026-06-15T00:00:00Z' }

$script:ChangelogPath = Join-Path $TestDrive ("filter-" + [guid]::NewGuid().Guid.Substring(0, 8) + ".md")
Set-Content -LiteralPath $script:ChangelogPath -Value "# Changelog`n`n" -NoNewline -Encoding utf8
}

It 'excludes a commit whose only package-folder change is the crate-root README.md, but keeps a real source commit' {
$rs = [char]0x1e; $us = [char]0x1f
$script:LogText = "${rs}hReal${us}feat(pkg): genuine source change (#11)`n`ncrates/pkg/src/lib.rs`n${rs}hDoc${us}feat: introduce unrelated crate (#22)`n`ncrates/pkg/README.md"

Mock -CommandName Invoke-Git -MockWith {
if ($Arguments -contains 'tag') { return @() }
if ($Arguments[0] -eq 'log') { return $script:LogText }
return @()
}

Write-Changelog -packageName 'pkg' -newVersion '0.2.0' `
-packageFolder (Join-Path $TestDrive 'crates\pkg') `
-changelogFile $script:ChangelogPath -prBaseUrl 'http://x' `
-WarningAction SilentlyContinue

$content = Get-Content -LiteralPath $script:ChangelogPath -Raw
$content | Should -Match 'genuine source change'
$content | Should -Not -Match 'introduce unrelated crate'
}

It 'keeps a commit that touches a NESTED README.md (e.g. examples/README.md), which is hand-authored' {
$rs = [char]0x1e; $us = [char]0x1f
$script:LogText = "${rs}hNested${us}docs(pkg): expand examples readme (#33)`n`ncrates/pkg/examples/README.md"

Mock -CommandName Invoke-Git -MockWith {
if ($Arguments -contains 'tag') { return @() }
if ($Arguments[0] -eq 'log') { return $script:LogText }
return @()
}

Write-Changelog -packageName 'pkg' -newVersion '0.2.0' `
-packageFolder (Join-Path $TestDrive 'crates\pkg') `
-changelogFile $script:ChangelogPath -prBaseUrl 'http://x' `
-WarningAction SilentlyContinue

$content = Get-Content -LiteralPath $script:ChangelogPath -Raw
$content | Should -Match 'expand examples readme'
}

It 'keeps a commit touching a nested file that shares BOTH leaf and parent name (crates/<pkg>/<pkg>/README.md)' {
# Edge case: matching on the parent leaf alone would misclassify this as
# the crate root. The exact repo-relative path match keeps it.
$rs = [char]0x1e; $us = [char]0x1f
$script:LogText = "${rs}hNested${us}docs(pkg): nested same-named readme (#44)`n`ncrates/pkg/pkg/README.md"

Mock -CommandName Invoke-Git -MockWith {
if ($Arguments -contains 'tag') { return @() }
if ($Arguments[0] -eq 'log') { return $script:LogText }
return @()
}

Write-Changelog -packageName 'pkg' -newVersion '0.2.0' `
-packageFolder (Join-Path $TestDrive 'crates\pkg') `
-changelogFile $script:ChangelogPath -prBaseUrl 'http://x' `
-WarningAction SilentlyContinue

$content = Get-Content -LiteralPath $script:ChangelogPath -Raw
$content | Should -Match 'nested same-named readme'
}

It 'warns and writes nothing when every in-range commit is crate-root README-only' {
$rs = [char]0x1e; $us = [char]0x1f
$script:LogText = "${rs}hDoc${us}feat: introduce unrelated crate (#22)`n`ncrates/pkg/README.md"

Mock -CommandName Invoke-Git -MockWith {
if ($Arguments -contains 'tag') { return @() }
if ($Arguments[0] -eq 'log') { return $script:LogText }
return @()
}

$before = Get-Content -LiteralPath $script:ChangelogPath -Raw

Write-Changelog -packageName 'pkg' -newVersion '0.2.0' `
-packageFolder (Join-Path $TestDrive 'crates\pkg') `
-changelogFile $script:ChangelogPath -prBaseUrl 'http://x' `
-WarningAction SilentlyContinue

$after = Get-Content -LiteralPath $script:ChangelogPath -Raw
$after | Should -Be $before
}
}
Loading