From 5d0b38f361a1961da2b86c89f994ca6960f5784d Mon Sep 17 00:00:00 2001 From: routersys Date: Wed, 9 Sep 2026 13:12:37 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E3=83=AA=E3=83=AA=E3=83=BC=E3=82=B9?= =?UTF-8?q?=E3=83=8E=E3=83=BC=E3=83=88=E3=82=92=E7=A8=AE=E5=88=A5=E3=81=94?= =?UTF-8?q?=E3=81=A8=E3=81=AB=E4=B8=A6=E3=81=B9=E3=82=8B=E9=81=93=E5=85=B7?= =?UTF-8?q?=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/generate-release-notes.ps1 | 351 +++++++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 build/generate-release-notes.ps1 diff --git a/build/generate-release-notes.ps1 b/build/generate-release-notes.ps1 new file mode 100644 index 00000000..37d79fd9 --- /dev/null +++ b/build/generate-release-notes.ps1 @@ -0,0 +1,351 @@ +#Requires -Version 7 + +<# +.SYNOPSIS + Writes the release notes for a tag from the commits and the merged pull requests in its range. + +.DESCRIPTION + The notes were the commit subjects of the range and nothing else. A release carrying two + hundred of them buries the few entries an upgrading caller has to read, and nothing in a + subject says whether it moved the public API or the build. Every pull request here declares + its kinds and carries the matching labels, so this reads those labels and lists the pull + requests that carry one first, grouped by kind. The commit list follows unchanged, because + commits also reach the default branch without a pull request and dropping them loses them. + + A pull request is listed once, under the first kind it carries in the order below, so one that + is both a public API addition and documentation reads as the addition. A pull request carrying + no kind is listed apart rather than dropped, because a missing label is exactly the thing that + should be visible in the published notes. + + The grouped section is not a precondition for describing the release. When nothing resolves to + a pull request, a line in its place says the section is missing and the commit list still + carries the whole range. Whether that was the API being out of reach or the numbers naming + something else goes to the log, because it is not something a reader of the notes can act on. + + A tag that does not exist yet, given with the tip of the default branch, produces the notes the + next release would carry. That is the same thing to read before deciding to cut one, and before + merging a pull request whose label decides where it lands. + + The order below is not the order of the template. It is the order an upgrading caller reads: + first what can break them, then what they can adopt, then what changed underneath them. A + public API addition cannot break an existing caller, so it does not lead. + + That order rests on what the template says each kind means, not on how the labels have been + applied, because the application is not uniform. Two merged pull requests turn a dispatch that + used to run into a throw: the one that introduced the requirement carries no behavior label, + and the one that widened where the requirement is detected carries it. This orders the labels + it is given; which labels a pull request carries is settled in that pull request, and a + caller-breaking change labelled as neither of the first two kinds is ranked below them. + +.PARAMETER Version + The version being released, as it appears in the install line. + +.PARAMETER CurrentTag + The tag of this release. It is excluded when the previous tag is looked up. + +.PARAMETER CurrentSha + The commit being released. + +.PARAMETER Repository + The owner and name of the repository the pull requests are read from. Defaults to + GITHUB_REPOSITORY, and to the origin remote when that is not set. + +.PARAMETER OutputPath + The file the notes are written to. + +.PARAMETER TestsNotRun + States that the release was packed without running the test suites. + +.PARAMETER Path + The repository root. Defaults to the parent of this script. + +.EXAMPLE + pwsh build/generate-release-notes.ps1 -Version 2.5.0 -CurrentTag v2.5.0 -CurrentSha v2.5.0 +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory)] [string] $Version, + [Parameter(Mandatory)] [string] $CurrentTag, + [string] $CurrentSha = 'HEAD', + [string] $Repository = $env:GITHUB_REPOSITORY, + [string] $OutputPath = 'release-notes.md', + [switch] $TestsNotRun, + [string] $Path +) + +$ErrorActionPreference = 'Stop' + +# A number that names an issue rather than a pull request answers 404, and that has to be skipped +# rather than end the release. Whether a non-zero native exit throws is a preference whose default +# has moved between PowerShell versions, so it is set here and the exit codes are read below. +$PSNativeCommandUseErrorActionPreference = $false + +# git and gh answer in UTF-8, and PowerShell decodes a native command's output with the console +# encoding, which is not UTF-8 everywhere. Run from a shell whose console is Shift-JIS, the subjects +# arrive mangled and the JSON of a Japanese title stops parsing, so this is set rather than +# inherited. Setting it works with the output redirected, which is how a workflow runs it. +[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) + +if ([string]::IsNullOrEmpty($Path)) +{ + $Path = Split-Path -Parent $PSScriptRoot +} + +$root = (Resolve-Path -LiteralPath $Path).Path + +# The kinds of the pull request template, paired with the labels CONTRIBUTING gives them, in the +# order an upgrading caller reads. build/verify-release-note-categories.ps1 holds this to both. +$kinds = @( + @{ Label = 'behavior change'; Heading = 'API変更を伴わない挙動の変更' } + @{ Label = 'public api'; Heading = '公開APIの追加' } + @{ Label = 'analyzer or generator'; Heading = 'アナライザーまたはジェネレーター' } + @{ Label = 'bug'; Heading = '不具合修正' } + @{ Label = 'performance'; Heading = '性能' } + @{ Label = 'documentation'; Heading = '文書' } + @{ Label = 'build and ci'; Heading = 'ビルド、パッケージ、CI' } +) + +# Runs git in the repository and fails the whole script when git does, so that an empty range is +# never mistaken for a release with nothing in it. +function Invoke-Git +{ + param([string[]] $Arguments) + + $output = @(git -C $root @Arguments) + + if ($LASTEXITCODE -ne 0) + { + throw "git $($Arguments -join ' ') failed with exit code $LASTEXITCODE." + } + + return @($output) +} + +if ([string]::IsNullOrWhiteSpace($Repository)) +{ + $url = @(git -C $root remote get-url origin 2>$null) + + if ($LASTEXITCODE -eq 0 -and $url.Count -gt 0) + { + $match = [regex]::Match($url[0], '[:/]([^/:]+/[^/]+?)(?:\.git)?$') + + if ($match.Success) + { + $Repository = $match.Groups[1].Value + } + } +} + +# One line, taken through the pipeline so that a single line does not index by character. +$sha = [string] (Invoke-Git @('rev-parse', $CurrentSha) | Select-Object -First 1) + +# The tag of the previous release, so the range is what this release adds. Tags that are not +# ancestors of this commit are not releases of this branch and are left out by --merged. +$previous = Invoke-Git @('tag', '--merged', $sha, '--sort=-version:refname') | + Where-Object { $_ -match '^v[0-9]+\.[0-9]+\.[0-9]+$' -and $_ -ne $CurrentTag } | + Select-Object -First 1 + +# The first release has no previous tag, so it falls back to the most recent commits. +$range = if ($previous) { @("$previous..$sha") } else { @('-n', '30', $sha) } + +# A revision range and a path can be spelled the same, so the range is closed off from paths. +$terminator = if ($previous) { @('--') } else { @() } + +$commits = @(Invoke-Git (@('log') + $range + @('--no-merges', '--pretty=format:- %s (%h)') + $terminator) | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + +$subjects = @(Invoke-Git (@('log') + $range + @('--pretty=format:%s') + $terminator) | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + +# The pull requests of the range, in the order their commits appear. GitHub writes the number into +# the subject of a merge commit, and into the subject itself when the merge is squashed; this tree +# had 122 of the first and none of the second on 2026-09-08, and reading both keeps the section +# alive if the merge strategy changes. +$numbers = [System.Collections.Generic.List[int]]::new() + +foreach ($subject in $subjects) +{ + $match = [regex]::Match($subject, '^Merge pull request #([0-9]+)\b') + + if (-not $match.Success) + { + $match = [regex]::Match($subject, '\(#([0-9]+)\)\s*$') + } + + if (-not $match.Success) + { + continue + } + + $number = [int] $match.Groups[1].Value + + if (-not $numbers.Contains($number)) + { + $numbers.Add($number) + } +} + +$pulls = [System.Collections.Generic.List[object]]::new() + +$available = ($null -ne (Get-Command gh -ErrorAction SilentlyContinue)) -and -not [string]::IsNullOrWhiteSpace($Repository) + +if ($numbers.Count -gt 0 -and $available) +{ + foreach ($number in $numbers) + { + $json = gh api "repos/$Repository/pulls/$number" --jq '{number: .number, title: .title, labels: [.labels[].name]}' 2>$null + + # A number that names an issue rather than a pull request answers 404 and is skipped. + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($json)) + { + continue + } + + $pulls.Add(($json | ConvertFrom-Json)) + } +} + +# Nothing resolving is either the API being out of reach or every number naming something else. Both +# leave the grouped section with no content, so the notes state that outcome and the log says which. +$resolvedAny = $pulls.Count -gt 0 + +$grouped = [ordered]@{} + +foreach ($kind in $kinds) +{ + $grouped[$kind.Heading] = [System.Collections.Generic.List[string]]::new() +} + +$uncategorized = [System.Collections.Generic.List[string]]::new() + +foreach ($pull in $pulls) +{ + $heading = $null + + foreach ($kind in $kinds) + { + if ($pull.labels -contains $kind.Label) + { + $heading = $kind.Heading + + break + } + } + + $entry = "- $($pull.title) (#$($pull.number))" + + if ($null -ne $heading) + { + $grouped[$heading].Add($entry) + } + else + { + $uncategorized.Add($entry) + } +} + +$lines = [System.Collections.Generic.List[string]]::new() + +if ($numbers.Count -gt 0) +{ + $lines.Add('### 種別ごとの変更') + $lines.Add('') + + if (-not $resolvedAny) + { + $lines.Add('この範囲では種別ごとの一覧を作れませんでした。次の一覧が範囲の全体です。') + $lines.Add('') + } + else + { + foreach ($heading in $grouped.Keys) + { + if ($grouped[$heading].Count -eq 0) + { + continue + } + + $lines.Add("#### $heading") + $lines.Add('') + $lines.AddRange($grouped[$heading]) + $lines.Add('') + } + + # A pull request without a kind is listed rather than dropped, so the missing label shows. + if ($uncategorized.Count -gt 0) + { + $lines.Add('#### 種別の指定が無いもの') + $lines.Add('') + $lines.AddRange($uncategorized) + $lines.Add('') + } + } +} + +$lines.Add('### すべてのコミット') +$lines.Add('') + +if ($commits.Count -gt 0) +{ + $lines.AddRange([string[]] $commits) +} +else +{ + # git failing throws above, so an empty list is a range that holds no commit of its own. + $lines.Add('この範囲に見出しはありません。') +} + +$lines.Add('') +$lines.Add('### インストール') +$lines.Add('') +$lines.Add('```sh') +$lines.Add("dotnet add package ComputeWeave --version $Version") +$lines.Add('```') +$lines.Add('') +$lines.Add('### NuGet') +$lines.Add('') +$lines.Add("https://www.nuget.org/packages/ComputeWeave/$Version") + +if ($TestsNotRun) +{ + $lines.Add('') + $lines.Add('テストは実行していません。パッケージ作成時のビルドだけを実行しています。') +} + +$text = ($lines -join "`n") + "`n" + +[System.IO.File]::WriteAllText($OutputPath, $text, [System.Text.UTF8Encoding]::new($false)) + +$counted = @($grouped.Keys | ForEach-Object { $grouped[$_].Count } | Measure-Object -Sum).Sum + +if ($null -eq $counted) +{ + $counted = 0 +} + +# The range as git was given it, rather than as it was asked for, so the two cannot drift apart. +$described = if ($previous) { "$previous..$($sha.Substring(0, 8))" } else { "the most recent 30 commits of $($sha.Substring(0, 8))" } + +Write-Host ("Range {0}, {1} commit(s), {2} reference(s), {3} resolved, {4} grouped, {5} without a kind." -f + $described, + $commits.Count, + $numbers.Count, + $pulls.Count, + $counted, + $uncategorized.Count) + +# The notes say only that the grouped section is missing. Which of the two produced that is here. +if ($numbers.Count -gt 0 -and -not $resolvedAny) +{ + if (-not $available) + { + Write-Host 'The GitHub CLI or the repository name was not available, so the notes carry the commit list alone.' + } + else + { + Write-Host 'No reference in the range resolved to a pull request, so the notes carry the commit list alone.' + } +} + +exit 0 From 0e4187228750d1f8c316d316a5462ff123040b70 Mon Sep 17 00:00:00 2001 From: routersys Date: Wed, 9 Sep 2026 13:12:38 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E3=83=AA=E3=83=AA=E3=83=BC=E3=82=B9?= =?UTF-8?q?=E3=83=8E=E3=83=BC=E3=83=88=E3=81=AE=E7=A8=AE=E5=88=A5=E3=81=AE?= =?UTF-8?q?=E9=A3=9F=E3=81=84=E9=81=95=E3=81=84=E3=82=92=E6=A4=9C=E5=87=BA?= =?UTF-8?q?=E3=81=99=E3=82=8B=E9=81=93=E5=85=B7=E3=82=92=E8=B6=B3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/verify-release-note-categories.ps1 | 179 +++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 build/verify-release-note-categories.ps1 diff --git a/build/verify-release-note-categories.ps1 b/build/verify-release-note-categories.ps1 new file mode 100644 index 00000000..83a7abd7 --- /dev/null +++ b/build/verify-release-note-categories.ps1 @@ -0,0 +1,179 @@ +#Requires -Version 7 + +<# +.SYNOPSIS + Verifies that the kinds the release notes group by are the kinds this repository declares. + +.DESCRIPTION + build/generate-release-notes.ps1 groups the pull requests of a release by the labels they + carry. The labels live in CONTRIBUTING, the kinds they mirror live in the pull request + template, and the generator holds a copy of both so that a release does not depend on parsing + prose. A copy drifts silently: a renamed label or a new kind leaves the generator with a group + nothing lands in, and the pull requests fall into the section for a missing kind instead. That + only becomes visible once a release is published, so it is compared here instead. + + Only the sets are compared. The generator lists the kinds in the order an upgrading caller + reads them, which is deliberately not the order either document uses, so the order is not + compared. The pairing of a label to its heading is the generator's own, and it is read there. + + A scan that reads nothing agrees with everything, so a document this cannot read at all is + reported rather than passed over. + +.PARAMETER Path + The repository root. Defaults to the parent of this script. + +.EXAMPLE + pwsh build/verify-release-note-categories.ps1 +#> + +[CmdletBinding()] +param( + [string] $Path +) + +$ErrorActionPreference = 'Stop' + +if ([string]::IsNullOrEmpty($Path)) +{ + $Path = Split-Path -Parent $PSScriptRoot +} + +$root = (Resolve-Path -LiteralPath $Path).Path + +$generatorFile = 'build/generate-release-notes.ps1' +$contributingFile = 'CONTRIBUTING.md' +$templateFile = '.github/pull_request_template.md' + +# Reads a file that has to exist, because a missing one is the scan reading nothing. +function Get-RequiredText +{ + param([string] $File) + + $full = Join-Path $root $File + + if (-not (Test-Path -LiteralPath $full)) + { + throw "$File is missing, so the kinds cannot be compared against it." + } + + return [System.IO.File]::ReadAllText($full) +} + +$failures = [System.Collections.Generic.List[string]]::new() + +# The generator's own table, as label and heading pairs. +$generator = Get-RequiredText -File $generatorFile + +$declared = [ordered]@{} + +foreach ($match in [regex]::Matches($generator, "@\{\s*Label\s*=\s*'([^']+)';\s*Heading\s*=\s*'([^']+)'\s*\}")) +{ + $label = $match.Groups[1].Value + + if ($declared.Contains($label)) + { + $failures.Add("$generatorFile lists the label ``$label`` twice.") + + continue + } + + $declared[$label] = $match.Groups[2].Value +} + +# The labels CONTRIBUTING names, in the sentence that states the correspondence. +$contributing = Get-RequiredText -File $contributingFile + +$sentence = [regex]::Match($contributing, 'The kinds and the labels correspond one for one:([^.]+)\.') + +$labels = @() + +if (-not $sentence.Success) +{ + $failures.Add("$contributingFile no longer states the correspondence between the kinds and the labels in the wording this reads, so nothing names the labels. Update the pattern in this script when that sentence is reworded.") +} +else +{ + $labels = @([regex]::Matches($sentence.Groups[1].Value, '`([^`]+)`') | ForEach-Object { $_.Groups[1].Value }) +} + +# The kinds the template offers, taken from the Japanese half of each line of its Kind section. +$template = Get-RequiredText -File $templateFile + +$section = [regex]::Match($template, '(?ms)^## Kind / 種別\s*$(.*?)^## ') + +$kinds = @() + +if (-not $section.Success) +{ + $failures.Add("$templateFile no longer has a Kind section in the shape this reads, so nothing names the kinds. Update the pattern in this script when that section is reshaped.") +} +else +{ + $kinds = @([regex]::Matches($section.Groups[1].Value, '(?m)^- (?:.+) / (.+?)\s*$') | ForEach-Object { $_.Groups[1].Value }) +} + +if ($declared.Count -eq 0) +{ + $failures.Add("$generatorFile declares no kinds, so every pull request would be listed as having none.") +} + +if ($sentence.Success -and $labels.Count -eq 0) +{ + $failures.Add("$contributingFile states the correspondence and names no label.") +} + +if ($section.Success -and $kinds.Count -eq 0) +{ + $failures.Add("$templateFile has a Kind section and offers no kind.") +} + +# Compares two sets and names what each side holds alone, so the drift reads as a direction. +function Compare-Set +{ + param([string[]] $Declared, [string[]] $Documented, [string] $What, [string] $File) + + foreach ($value in ($Declared | Where-Object { $Documented -notcontains $_ })) + { + $failures.Add("$generatorFile groups by the $What ``$value`` and $File does not name it.") + } + + foreach ($value in ($Documented | Where-Object { $Declared -notcontains $_ })) + { + $failures.Add("$File names the $What ``$value`` and $generatorFile does not group by it.") + } +} + +if ($declared.Count -gt 0 -and $labels.Count -gt 0) +{ + Compare-Set -Declared @($declared.Keys) -Documented $labels -What 'label' -File $contributingFile +} + +if ($declared.Count -gt 0 -and $kinds.Count -gt 0) +{ + Compare-Set -Declared @($declared.Values) -Documented $kinds -What 'kind' -File $templateFile +} + +Write-Host ("{0,-44} {1} kind(s)" -f $generatorFile, $declared.Count) +Write-Host ("{0,-44} {1} label(s)" -f $contributingFile, $labels.Count) +Write-Host ("{0,-44} {1} kind(s)" -f $templateFile, $kinds.Count) +Write-Host '' + +if ($failures.Count -eq 0) +{ + Write-Host 'The release notes group by every kind this repository declares, and by nothing else.' + + exit 0 +} + +Write-Host "$($failures.Count) kind(s) do not line up:" +Write-Host '' + +foreach ($failure in $failures) +{ + Write-Host " $failure" +} + +Write-Host '' +Write-Host 'A pull request that renames a label or changes the kinds has to carry the change into the release notes. A pull request that only rewords the sentence or the section this reads has to carry the new wording into the patterns above.' + +exit 1 From a75a210682daedfe4a195c24d2f265e22e3dd9fc Mon Sep 17 00:00:00 2001 From: routersys Date: Wed, 9 Sep 2026 13:12:38 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E3=83=AA=E3=83=AA=E3=83=BC=E3=82=B9?= =?UTF-8?q?=E3=83=8E=E3=83=BC=E3=83=88=E3=81=AE=E7=94=9F=E6=88=90=E3=81=A8?= =?UTF-8?q?=E7=A8=AE=E5=88=A5=E3=81=AE=E6=A4=9C=E8=A8=BC=E3=82=92=E3=83=AF?= =?UTF-8?q?=E3=83=BC=E3=82=AF=E3=83=95=E3=83=AD=E3=83=BC=E3=81=B8=E8=B6=B3?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docs.yml | 9 ++++++ .github/workflows/quick-release.yml | 44 +++++++++++------------------ .github/workflows/release.yml | 40 ++++++++++---------------- 3 files changed, 41 insertions(+), 52 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cb7c207d..90c7a25f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -12,6 +12,8 @@ on: - "build/verify-markdown-tables.ps1" - "build/verify-readme-counts.ps1" - "build/verify-analyzer-releases.ps1" + - "build/verify-release-note-categories.ps1" + - "build/generate-release-notes.ps1" - "src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs" - "src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs" pull_request: @@ -23,6 +25,8 @@ on: - "build/verify-markdown-tables.ps1" - "build/verify-readme-counts.ps1" - "build/verify-analyzer-releases.ps1" + - "build/verify-release-note-categories.ps1" + - "build/generate-release-notes.ps1" - "src/ComputeWeave.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs" - "src/ComputeWeave.D2D1.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs" @@ -52,3 +56,8 @@ jobs: # 重大度が動いた規則を追加した規則と同じ形で移すと、構築は通るのに履歴が二重になる。 - name: アナライザーのリリース記録を検証 run: pwsh build/verify-analyzer-releases.ps1 + + # ラベルの改名も種別の追加も Markdown の側で起きる。ノートの側が置き去りになると、 + # 出荷して初めて分かる。 + - name: リリースノートの種別を検証 + run: pwsh build/verify-release-note-categories.ps1 diff --git a/.github/workflows/quick-release.yml b/.github/workflows/quick-release.yml index 6260f5f9..562a2fc2 100644 --- a/.github/workflows/quick-release.yml +++ b/.github/workflows/quick-release.yml @@ -263,6 +263,10 @@ jobs: - name: README の件数を検証 run: pwsh build/verify-readme-counts.ps1 + # 種別の食い違いはノートを作る工程まで現れない。そこは公開の後なので、公開の前に確かめる。 + - name: リリースノートの種別を検証 + run: pwsh build/verify-release-note-categories.ps1 + - name: パッケージをアーティファクトとして保存 uses: actions/upload-artifact@v4 with: @@ -501,6 +505,8 @@ jobs: if: ${{ inputs.create_release }} permissions: contents: write + # ノートは併合済みプルリクエストのラベルを読んで種別ごとに並べる。 + pull-requests: read defaults: run: shell: bash @@ -517,35 +523,19 @@ jobs: name: nuget-packages path: ./artifacts - - name: コミット履歴からリリースノートを生成 + # 見出しをそのまま並べると、上げる側に効く数件が二百件の中に埋もれる。 + # このワークフローは手動でしか起動せず、手元でもCIでも走らない。生成をタグ側と同じ + # script に寄せてあるので、片方だけが古くなることが無い。 + - name: リリースノートを生成 env: - CURRENT_TAG: v${{ needs.validate.outputs.version }} - CURRENT_SHA: ${{ github.sha }} - VERSION: ${{ needs.validate.outputs.version }} + GH_TOKEN: ${{ github.token }} run: | - PREV=$(git tag --merged "${CURRENT_SHA}" --sort=-version:refname \ - | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ - | grep -Fxv "${CURRENT_TAG}" \ - | head -n 1 || true) - if [ -n "${PREV}" ]; then - LOG=$(git log "${PREV}..${CURRENT_SHA}" --pretty=format:"- %s (%h)" --no-merges --) - else - LOG=$(git log -n 30 --pretty=format:"- %s (%h)" --no-merges) - fi - { - echo "### 変更内容" - printf '%s\n' "${LOG:-変更履歴を取得できませんでした。}" - echo "" - echo "### インストール" - echo '```sh' - echo "dotnet add package ComputeWeave --version ${VERSION}" - echo '```' - echo "" - echo "### NuGet" - echo "https://www.nuget.org/packages/ComputeWeave/${VERSION}" - echo "" - echo "テストは実行していません。パッケージ作成時のビルドだけを実行しています。" - } > release-notes.md + pwsh build/generate-release-notes.ps1 \ + -Version "${{ needs.validate.outputs.version }}" \ + -CurrentTag "v${{ needs.validate.outputs.version }}" \ + -CurrentSha "${{ github.sha }}" \ + -Repository "${{ github.repository }}" \ + -TestsNotRun # タグは GITHUB_TOKEN で作られるため release.yml は起動しない。 - name: GitHub Release を作成してパッケージを添付 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88e63de1..cfefe0c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -291,6 +291,10 @@ jobs: - name: README の件数を検証 run: pwsh build/verify-readme-counts.ps1 + # 種別の食い違いはノートを作る工程まで現れない。そこは公開の後なので、公開の前に確かめる。 + - name: リリースノートの種別を検証 + run: pwsh build/verify-release-note-categories.ps1 + - name: パッケージをアーティファクトとして保存 uses: actions/upload-artifact@v4 with: @@ -426,6 +430,8 @@ jobs: needs: [validate, pack, publish-nuget] permissions: contents: write + # ノートは併合済みプルリクエストのラベルを読んで種別ごとに並べる。 + pull-requests: read defaults: run: shell: bash @@ -442,33 +448,17 @@ jobs: name: nuget-packages path: ./artifacts - - name: コミット履歴からリリースノートを生成 + # 見出しをそのまま並べると、上げる側に効く数件が二百件の中に埋もれる。 + # 生成は高速リリースと同じ script が行う。二重に書くと片方だけが古くなる。 + - name: リリースノートを生成 env: - CURRENT_TAG: ${{ github.ref_name }} - VERSION: ${{ needs.validate.outputs.version }} + GH_TOKEN: ${{ github.token }} run: | - CURRENT_SHA=$(git rev-parse HEAD) - PREV=$(git tag --merged "${CURRENT_SHA}" --sort=-version:refname \ - | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ - | grep -Fxv "${CURRENT_TAG}" \ - | head -n 1 || true) - if [ -n "${PREV}" ]; then - LOG=$(git log "${PREV}..${CURRENT_SHA}" --pretty=format:"- %s (%h)" --no-merges --) - else - LOG=$(git log -n 30 --pretty=format:"- %s (%h)" --no-merges) - fi - { - echo "### 変更内容" - printf '%s\n' "${LOG:-変更履歴を取得できませんでした。}" - echo "" - echo "### インストール" - echo '```sh' - echo "dotnet add package ComputeWeave --version ${VERSION}" - echo '```' - echo "" - echo "### NuGet" - echo "https://www.nuget.org/packages/ComputeWeave/${VERSION}" - } > release-notes.md + pwsh build/generate-release-notes.ps1 \ + -Version "${{ needs.validate.outputs.version }}" \ + -CurrentTag "${{ github.ref_name }}" \ + -CurrentSha "${{ github.sha }}" \ + -Repository "${{ github.repository }}" - name: GitHub Release を作成してパッケージを添付 uses: softprops/action-gh-release@v3 From 52955273b238fa0af7137d03ccfd0d3554a7f127 Mon Sep 17 00:00:00 2001 From: routersys Date: Wed, 9 Sep 2026 13:12:38 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=E3=83=A9=E3=83=99=E3=83=AB=E3=81=8C?= =?UTF-8?q?=E3=83=8E=E3=83=BC=E3=83=88=E3=81=AE=E4=B8=A6=E3=81=B3=E3=82=92?= =?UTF-8?q?=E6=B1=BA=E3=82=81=E3=82=8B=E3=81=93=E3=81=A8=E3=82=92=E8=A6=8F?= =?UTF-8?q?=E7=B4=84=E3=81=B8=E6=9B=B8=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9370b29e..9614c1be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -162,7 +162,7 @@ Throughout, distinguish what you observed, what you derived from reading the cod ### Labels -Apply the label that matches each kind you kept in the template. The kinds and the labels correspond one for one: `bug`, `public api`, `behavior change`, `performance`, `analyzer or generator`, `documentation` and `build and ci`. Nothing applies them for you, and the automated check described below does not set them. Labels are how the merged history is filtered by kind, so a pull request that declares a kind and carries no label is invisible to that filter. +Apply the label that matches each kind you kept in the template. The kinds and the labels correspond one for one: `bug`, `public api`, `behavior change`, `performance`, `analyzer or generator`, `documentation` and `build and ci`. Nothing applies them for you, and the automated check described below does not set them. Labels are how the merged history is filtered by kind, so a pull request that declares a kind and carries no label is invisible to that filter. They also decide where the pull request appears in the notes of the next release: the notes group the merged pull requests of a release by these labels, in the order a caller upgrading reads them, and a pull request carrying none of them is listed apart from the grouped ones. ### Automated checks