From f0a46134740c9596d06d2b94c675404f74f2396e Mon Sep 17 00:00:00 2001 From: mfoltz Date: Mon, 18 May 2026 08:57:42 -0500 Subject: [PATCH 1/6] [codex] Add prerelease changelog notes card --- .codex/scripts/prerelease-notes.sh | 165 ++++++++++++++++++++++ .codex/scripts/prerelease-notes.tests.ps1 | 140 ++++++++++++++++++ .github/workflows/build.yml | 17 ++- .github/workflows/release.yml | 25 +++- 4 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 .codex/scripts/prerelease-notes.sh create mode 100644 .codex/scripts/prerelease-notes.tests.ps1 diff --git a/.codex/scripts/prerelease-notes.sh b/.codex/scripts/prerelease-notes.sh new file mode 100644 index 0000000..0de5ddf --- /dev/null +++ b/.codex/scripts/prerelease-notes.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" + +CHANGELOG_PATH="$REPO_ROOT/CHANGELOG.md" +VERSION="" +TAG="" +BRANCH="" +COMMIT="" +RUN_ID="" +OUTPUT_PATH="" +CHECK_ONLY="false" + +fail() { + echo "Error: $1" >&2 + exit 1 +} + +usage() { + cat >&2 <<'EOF' +Usage: prerelease-notes.sh --version X.Y.Z [options] + +Options: + --changelog PATH CHANGELOG.md path to read. + --tag TAG GitHub Release tag. Defaults to vX.Y.Z-pre. + --branch NAME Source branch name for the notes card. + --commit SHA Source commit SHA for the notes card. + --run-id ID GitHub Actions run id for the notes card. + --output PATH Markdown file to write. + --check-only Validate changelog turnover without writing notes. +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --changelog) + CHANGELOG_PATH="${2:-}" + shift 2 + ;; + --version) + VERSION="${2:-}" + shift 2 + ;; + --tag) + TAG="${2:-}" + shift 2 + ;; + --branch) + BRANCH="${2:-}" + shift 2 + ;; + --commit) + COMMIT="${2:-}" + shift 2 + ;; + --run-id) + RUN_ID="${2:-}" + shift 2 + ;; + --output) + OUTPUT_PATH="${2:-}" + shift 2 + ;; + --check-only) + CHECK_ONLY="true" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + usage + fail "Unknown argument '$1'." + ;; + esac +done + +if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + fail "--version must use canonical X.Y.Z format." +fi + +if [ -z "$TAG" ]; then + TAG="v${VERSION}-pre" +fi + +if [ ! -f "$CHANGELOG_PATH" ]; then + fail "Unable to locate changelog at '$CHANGELOG_PATH'." +fi + +unreleased_body=$( + awk ' + /^##[[:space:]]*Unreleased[[:space:]]*$/ { in_unreleased = 1; next } + in_unreleased && (/^`[^`]+`[[:space:]]*$/ || /^## /) { exit } + in_unreleased { print } + ' "$CHANGELOG_PATH" +) + +if printf '%s' "$unreleased_body" | grep -q '[^[:space:]]'; then + fail "CHANGELOG.md ## Unreleased must be empty before creating or publishing a prerelease." +fi + +version_body=$( + awk -v version="$VERSION" ' + $0 == "`" version "`" { in_version = 1; next } + in_version && (/^`[^`]+`[[:space:]]*$/ || /^## /) { exit } + in_version { print } + ' "$CHANGELOG_PATH" +) + +if ! printf '%s' "$version_body" | grep -q '[^[:space:]]'; then + fail "CHANGELOG.md does not contain notes for '$VERSION'." +fi + +if [ "$CHECK_ONLY" = "true" ]; then + echo "prerelease-notes: changelog turnover validated for $VERSION." + exit 0 +fi + +if [ -z "$OUTPUT_PATH" ]; then + fail "--output is required unless --check-only is used." +fi + +BRANCH="${BRANCH:-unknown}" +COMMIT="${COMMIT:-unknown}" +RUN_ID="${RUN_ID:-unknown}" +short_commit="$COMMIT" +if [ ${#short_commit} -gt 12 ]; then + short_commit="${short_commit:0:12}" +fi + +run_detail="\`$RUN_ID\`" +if [ -n "${GITHUB_REPOSITORY:-}" ] && [ "$RUN_ID" != "unknown" ]; then + run_detail="[${RUN_ID}](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID})" +fi + +mkdir -p "$(dirname "$OUTPUT_PATH")" +cat > "$OUTPUT_PATH" < [!NOTE] +> This GitHub pre-release is the source artifact for the matching Thunderstore publish. Thunderstore receives package version \`${VERSION}\` from tag \`${TAG}\`. + +
+Good to know before Thunderstore + +| Item | Detail | +| --- | --- | +| Changelog turnover | \`## Unreleased\` is empty; notes below come from \`${VERSION}\`. | +| Source branch | \`${BRANCH}\` | +| Source commit | \`${short_commit}\` | +| Workflow run | ${run_detail} | +| GitHub tag | \`${TAG}\` | +| Thunderstore version | \`${VERSION}\` | + +
+ +### Changes + +${version_body} +EOF + +echo "prerelease-notes: wrote $OUTPUT_PATH" diff --git a/.codex/scripts/prerelease-notes.tests.ps1 b/.codex/scripts/prerelease-notes.tests.ps1 new file mode 100644 index 0000000..4796ca6 --- /dev/null +++ b/.codex/scripts/prerelease-notes.tests.ps1 @@ -0,0 +1,140 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$PrereleaseNotesPath = Join-Path $ScriptRoot "prerelease-notes.sh" +$BashPath = "C:\Program Files\Git\bin\bash.exe" + +if (-not (Test-Path -LiteralPath $BashPath)) { + throw "Git Bash was not found at $BashPath." +} + +function Assert-Match { + param( + [Parameter(Mandatory = $true)] + [string]$Text, + [Parameter(Mandatory = $true)] + [string]$Pattern, + [Parameter(Mandatory = $true)] + [string]$Message + ) + + if ($Text -notmatch $Pattern) { + throw $Message + } +} + +function New-Fixture { + $FixtureRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("eclipse-prerelease-notes-" + [guid]::NewGuid().ToString("N")) + New-Item -ItemType Directory -Path $FixtureRoot | Out-Null + return $FixtureRoot +} + +function Invoke-PrereleaseNotes { + param( + [Parameter(Mandatory = $true)] + [string[]]$Arguments + ) + + & $BashPath $PrereleaseNotesPath @Arguments 2>&1 | Out-String +} + +function Test-PrereleaseNotesIncludesChangelogAndDetailsCard { + $FixtureRoot = New-Fixture + try { + $ChangelogPath = Join-Path $FixtureRoot "CHANGELOG.md" + $OutputPath = Join-Path $FixtureRoot "prerelease-notes.md" + Set-Content -Path $ChangelogPath -Value @' +## Unreleased + +`1.2.3` +- fixed the client widget timing +- added a runtime receipt + +`1.2.2` +- previous release +'@ + + $Output = Invoke-PrereleaseNotes -Arguments @( + "--changelog", $ChangelogPath, + "--version", "1.2.3", + "--tag", "v1.2.3-pre", + "--branch", "main", + "--commit", "1234567890abcdef", + "--run-id", "42", + "--output", $OutputPath) + if ($LASTEXITCODE -ne 0) { + throw "prerelease-notes.sh exited with $LASTEXITCODE`n$Output" + } + + $Notes = Get-Content -Raw -Path $OutputPath + Assert-Match -Text $Notes -Pattern '
' -Message "Release notes did not include the details card." + Assert-Match -Text $Notes -Pattern 'Good to know before Thunderstore' -Message "Release notes did not include the card summary." + Assert-Match -Text $Notes -Pattern '## Unreleased.*empty' -Message "Release notes did not describe changelog turnover." + Assert-Match -Text $Notes -Pattern 'Thunderstore version.*1\.2\.3' -Message "Release notes did not include the Thunderstore package version." + Assert-Match -Text $Notes -Pattern 'fixed the client widget timing' -Message "Release notes did not include current version changelog notes." + Assert-Match -Text $Notes -Pattern '1234567890ab' -Message "Release notes did not include the short commit." + } + finally { + Remove-Item -Recurse -Force -LiteralPath $FixtureRoot + } +} + +function Test-PrereleaseNotesRejectsUnreleasedContent { + $FixtureRoot = New-Fixture + try { + $ChangelogPath = Join-Path $FixtureRoot "CHANGELOG.md" + Set-Content -Path $ChangelogPath -Value @' +## Unreleased +- still parked for the next release + +`1.2.3` +- fixed the client widget timing +'@ + + $Output = Invoke-PrereleaseNotes -Arguments @( + "--changelog", $ChangelogPath, + "--version", "1.2.3", + "--check-only") + if ($LASTEXITCODE -eq 0) { + throw "prerelease-notes.sh unexpectedly accepted non-empty Unreleased notes." + } + + Assert-Match -Text $Output -Pattern 'CHANGELOG\.md ## Unreleased must be empty' -Message "Unreleased rejection message was not specific." + } + finally { + Remove-Item -Recurse -Force -LiteralPath $FixtureRoot + } +} + +function Test-PrereleaseNotesRejectsMissingVersionEntry { + $FixtureRoot = New-Fixture + try { + $ChangelogPath = Join-Path $FixtureRoot "CHANGELOG.md" + Set-Content -Path $ChangelogPath -Value @' +## Unreleased + +`1.2.2` +- previous release +'@ + + $Output = Invoke-PrereleaseNotes -Arguments @( + "--changelog", $ChangelogPath, + "--version", "1.2.3", + "--check-only") + if ($LASTEXITCODE -eq 0) { + throw "prerelease-notes.sh unexpectedly accepted a missing version entry." + } + + Assert-Match -Text $Output -Pattern "does not contain notes for '1\.2\.3'" -Message "Missing version rejection message was not specific." + } + finally { + Remove-Item -Recurse -Force -LiteralPath $FixtureRoot + } +} + +Test-PrereleaseNotesIncludesChangelogAndDetailsCard +Test-PrereleaseNotesRejectsUnreleasedContent +Test-PrereleaseNotesRejectsMissingVersionEntry + +Write-Host "prerelease-notes tests passed" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31894b0..8b4d7b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -251,15 +251,22 @@ jobs: -p:Version="$version" \ -p:DeployToClient=false + - name: Prepare prerelease notes + shell: bash + run: | + bash .codex/scripts/prerelease-notes.sh \ + --version '${{ needs.prepare_prerelease.outputs.prerelease_version }}' \ + --tag '${{ needs.prepare_prerelease.outputs.prerelease_tag }}' \ + --branch '${{ github.ref_name }}' \ + --commit '${{ github.sha }}' \ + --run-id '${{ github.run_id }}' \ + --output ./dist/prerelease-notes.md + - name: GH Release (pre-release) uses: softprops/action-gh-release@v2.6.2 if: needs.prepare_prerelease.outputs.prerelease_version != '' with: - body: | - Automated pre-release for version ${{ needs.prepare_prerelease.outputs.prerelease_version }} generated from the main branch. - - Branch: ${{ github.ref_name }} - - Commit: ${{ github.sha }} - - Run: ${{ github.run_id }} + body_path: ./dist/prerelease-notes.md name: ${{ needs.prepare_prerelease.outputs.prerelease_name }} fail_on_unmatched_files: true prerelease: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb9ff9d..80b32ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,18 @@ jobs: - name: Validate canonical version metadata id: version_metadata shell: bash - run: bash .codex/scripts/version-metadata.sh + run: | + metadata_output=$(bash .codex/scripts/version-metadata.sh) + echo "$metadata_output" + canonical_version=$(printf '%s\n' "$metadata_output" | sed -n 's/^canonical_version=//p' | tail -n 1) + if [ -z "$canonical_version" ]; then + echo "Error: canonical version metadata was not emitted." >&2 + exit 1 + fi + + bash .codex/scripts/prerelease-notes.sh \ + --version "$canonical_version" \ + --check-only - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -80,6 +91,18 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ env.RELEASE_TAG }} + - name: Validate downloaded release changelog + shell: bash + run: | + bash .codex/scripts/prerelease-notes.sh \ + --changelog ./dist/CHANGELOG.md \ + --version "$PACKAGE_VERSION" \ + --tag "$RELEASE_TAG" \ + --check-only + env: + RELEASE_TAG: ${{ env.RELEASE_TAG }} + PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }} + - name: Install Thunderstore CLI run: dotnet tool install --global tcli From 384887e89271d57bd48bd5e85fcefd4d94564052 Mon Sep 17 00:00:00 2001 From: mfoltz Date: Mon, 18 May 2026 09:13:57 -0500 Subject: [PATCH 2/6] [codex] Bump prerelease notes workflow version --- CHANGELOG.md | 3 +++ Eclipse.csproj | 2 +- thunderstore.toml | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c3143f..ab6469b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +`1.3.17` +- added prerelease note generation and changelog turnover validation for GitHub and Thunderstore release flows + `1.3.16` - hardened attribute UI initialization so runtime-loaded clients defer when the inventory attribute hierarchy is not ready - collapsed Emberglass bridge progress receipts to one client log line per config/progress batch diff --git a/Eclipse.csproj b/Eclipse.csproj index 23bf62e..c73a95c 100644 --- a/Eclipse.csproj +++ b/Eclipse.csproj @@ -4,7 +4,7 @@ enable io.zfolmt.Eclipse Eclipse - 1.3.16 + 1.3.17 true true preview diff --git a/thunderstore.toml b/thunderstore.toml index f3ccaec..316a02e 100644 --- a/thunderstore.toml +++ b/thunderstore.toml @@ -11,7 +11,7 @@ v-rising = ["oakveil-update", "mods", "client"] [package] namespace = "zfolmt" name = "Eclipse" -versionNumber = "1.3.16" +versionNumber = "1.3.17" description = "Client UI for Bloodcraft!" websiteUrl = "https://github.com/mfoltz/Eclipse" containsNsfwContent = false From adb1d67b1dfca3fb9c3beed8a6c54ee4e43b2811 Mon Sep 17 00:00:00 2001 From: mfoltz Date: Mon, 18 May 2026 10:17:03 -0500 Subject: [PATCH 3/6] [codex] Keep Thunderstore changelog gate release-scoped --- .codex/scripts/prerelease-notes.tests.ps1 | 29 +++++++++++++++++++++++ .github/workflows/release.yml | 13 +--------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.codex/scripts/prerelease-notes.tests.ps1 b/.codex/scripts/prerelease-notes.tests.ps1 index 4796ca6..1e31df3 100644 --- a/.codex/scripts/prerelease-notes.tests.ps1 +++ b/.codex/scripts/prerelease-notes.tests.ps1 @@ -133,8 +133,37 @@ function Test-PrereleaseNotesRejectsMissingVersionEntry { } } +function Test-ReleaseWorkflowChecksOnlyDownloadedReleaseChangelog { + $WorkflowPath = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptRoot)) ".github/workflows/release.yml" + $WorkflowText = Get-Content -Raw -Path $WorkflowPath + + $DownloadMarker = " - name: Download Release" + $DownloadedChangelogMarker = " - name: Validate downloaded release changelog" + + $DownloadIndex = $WorkflowText.IndexOf($DownloadMarker, [StringComparison]::Ordinal) + $DownloadedChangelogIndex = $WorkflowText.IndexOf($DownloadedChangelogMarker, [StringComparison]::Ordinal) + + if ($DownloadIndex -lt 0) { + throw "release.yml is missing the Download Release step." + } + + if ($DownloadedChangelogIndex -lt 0) { + throw "release.yml is missing downloaded release changelog validation." + } + + if ($DownloadedChangelogIndex -lt $DownloadIndex) { + throw "Downloaded release changelog validation must run after the release download." + } + + $BeforeDownload = $WorkflowText.Substring(0, $DownloadIndex) + if ($BeforeDownload -match 'prerelease-notes\.sh[\s\S]*--check-only') { + throw "release.yml must not run prerelease-notes.sh --check-only against the checkout before downloading the selected release tag." + } +} + Test-PrereleaseNotesIncludesChangelogAndDetailsCard Test-PrereleaseNotesRejectsUnreleasedContent Test-PrereleaseNotesRejectsMissingVersionEntry +Test-ReleaseWorkflowChecksOnlyDownloadedReleaseChangelog Write-Host "prerelease-notes tests passed" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80b32ed..552d221 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,18 +23,7 @@ jobs: - name: Validate canonical version metadata id: version_metadata shell: bash - run: | - metadata_output=$(bash .codex/scripts/version-metadata.sh) - echo "$metadata_output" - canonical_version=$(printf '%s\n' "$metadata_output" | sed -n 's/^canonical_version=//p' | tail -n 1) - if [ -z "$canonical_version" ]; then - echo "Error: canonical version metadata was not emitted." >&2 - exit 1 - fi - - bash .codex/scripts/prerelease-notes.sh \ - --version "$canonical_version" \ - --check-only + run: bash .codex/scripts/version-metadata.sh - name: Setup .NET uses: actions/setup-dotnet@v4 From e9ebbd2b686554c94677ae0364e16d49ab0d854d Mon Sep 17 00:00:00 2001 From: mfoltz Date: Mon, 18 May 2026 10:56:08 -0500 Subject: [PATCH 4/6] [codex] Make prerelease handoff card glanceable --- .codex/scripts/prerelease-notes.sh | 23 ++++++++++------------- .codex/scripts/prerelease-notes.tests.ps1 | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/.codex/scripts/prerelease-notes.sh b/.codex/scripts/prerelease-notes.sh index 0de5ddf..7774afe 100644 --- a/.codex/scripts/prerelease-notes.sh +++ b/.codex/scripts/prerelease-notes.sh @@ -143,21 +143,18 @@ cat > "$OUTPUT_PATH" < [!NOTE] > This GitHub pre-release is the source artifact for the matching Thunderstore publish. Thunderstore receives package version \`${VERSION}\` from tag \`${TAG}\`. -
-Good to know before Thunderstore +### 📦 Thunderstore handoff -| Item | Detail | +| Signal | Detail | | --- | --- | -| Changelog turnover | \`## Unreleased\` is empty; notes below come from \`${VERSION}\`. | -| Source branch | \`${BRANCH}\` | -| Source commit | \`${short_commit}\` | -| Workflow run | ${run_detail} | -| GitHub tag | \`${TAG}\` | -| Thunderstore version | \`${VERSION}\` | - -
- -### Changes +| 📝 Changelog | \`## Unreleased\` is empty; notes below come from \`${VERSION}\`. | +| 🌿 Branch | \`${BRANCH}\` | +| 🔖 Commit | \`${short_commit}\` | +| ▶️ Run | ${run_detail} | +| 🏷️ Tag | \`${TAG}\` | +| 📦 Package | \`${VERSION}\` | + +### ✨ Changes ${version_body} EOF diff --git a/.codex/scripts/prerelease-notes.tests.ps1 b/.codex/scripts/prerelease-notes.tests.ps1 index 1e31df3..5026db5 100644 --- a/.codex/scripts/prerelease-notes.tests.ps1 +++ b/.codex/scripts/prerelease-notes.tests.ps1 @@ -68,10 +68,19 @@ function Test-PrereleaseNotesIncludesChangelogAndDetailsCard { } $Notes = Get-Content -Raw -Path $OutputPath - Assert-Match -Text $Notes -Pattern '
' -Message "Release notes did not include the details card." - Assert-Match -Text $Notes -Pattern 'Good to know before Thunderstore' -Message "Release notes did not include the card summary." + if ($Notes -match ' Date: Mon, 18 May 2026 11:20:45 -0500 Subject: [PATCH 5/6] [codex] Preserve release helpers for changelog validation --- .codex/scripts/prerelease-notes.tests.ps1 | 14 ++++++++++++++ .github/workflows/release.yml | 8 +++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.codex/scripts/prerelease-notes.tests.ps1 b/.codex/scripts/prerelease-notes.tests.ps1 index 5026db5..5e5075e 100644 --- a/.codex/scripts/prerelease-notes.tests.ps1 +++ b/.codex/scripts/prerelease-notes.tests.ps1 @@ -146,12 +146,18 @@ function Test-ReleaseWorkflowChecksOnlyDownloadedReleaseChangelog { $WorkflowPath = Join-Path (Split-Path -Parent (Split-Path -Parent $ScriptRoot)) ".github/workflows/release.yml" $WorkflowText = Get-Content -Raw -Path $WorkflowPath + $PreserveMarker = " - name: Preserve release helper scripts" $DownloadMarker = " - name: Download Release" $DownloadedChangelogMarker = " - name: Validate downloaded release changelog" + $PreserveIndex = $WorkflowText.IndexOf($PreserveMarker, [StringComparison]::Ordinal) $DownloadIndex = $WorkflowText.IndexOf($DownloadMarker, [StringComparison]::Ordinal) $DownloadedChangelogIndex = $WorkflowText.IndexOf($DownloadedChangelogMarker, [StringComparison]::Ordinal) + if ($PreserveIndex -lt 0) { + throw "release.yml is missing the Preserve release helper scripts step." + } + if ($DownloadIndex -lt 0) { throw "release.yml is missing the Download Release step." } @@ -160,6 +166,10 @@ function Test-ReleaseWorkflowChecksOnlyDownloadedReleaseChangelog { throw "release.yml is missing downloaded release changelog validation." } + if ($DownloadIndex -lt $PreserveIndex) { + throw "Release helper scripts must be preserved before downloading the selected release." + } + if ($DownloadedChangelogIndex -lt $DownloadIndex) { throw "Downloaded release changelog validation must run after the release download." } @@ -168,6 +178,10 @@ function Test-ReleaseWorkflowChecksOnlyDownloadedReleaseChangelog { if ($BeforeDownload -match 'prerelease-notes\.sh[\s\S]*--check-only') { throw "release.yml must not run prerelease-notes.sh --check-only against the checkout before downloading the selected release tag." } + + if ($WorkflowText -notmatch '\$RUNNER_TEMP/prerelease-notes\.sh') { + throw "release.yml should run changelog validation from the preserved helper script." + } } Test-PrereleaseNotesIncludesChangelogAndDetailsCard diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 552d221..c407c51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,6 +74,12 @@ jobs: echo "ALLOWED_TAG_PATTERN=$allowed_tag_pattern" >> "$GITHUB_ENV" echo "ALLOWED_PACKAGE_VERSION_PATTERN=$allowed_package_version_pattern" >> "$GITHUB_ENV" + - name: Preserve release helper scripts + shell: bash + run: | + cp .codex/scripts/prerelease-notes.sh "$RUNNER_TEMP/prerelease-notes.sh" + chmod +x "$RUNNER_TEMP/prerelease-notes.sh" + - name: Download Release run: gh release download "$RELEASE_TAG" -D ./dist env: @@ -83,7 +89,7 @@ jobs: - name: Validate downloaded release changelog shell: bash run: | - bash .codex/scripts/prerelease-notes.sh \ + bash "$RUNNER_TEMP/prerelease-notes.sh" \ --changelog ./dist/CHANGELOG.md \ --version "$PACKAGE_VERSION" \ --tag "$RELEASE_TAG" \ From 277d234661a358aa8f891605a86a6255380c5263 Mon Sep 17 00:00:00 2001 From: mfoltz Date: Mon, 18 May 2026 11:37:05 -0500 Subject: [PATCH 6/6] [codex] Require Unreleased header in prerelease checks --- .codex/scripts/prerelease-notes.sh | 4 ++++ .codex/scripts/prerelease-notes.tests.ps1 | 25 +++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/.codex/scripts/prerelease-notes.sh b/.codex/scripts/prerelease-notes.sh index 7774afe..1acc09d 100644 --- a/.codex/scripts/prerelease-notes.sh +++ b/.codex/scripts/prerelease-notes.sh @@ -90,6 +90,10 @@ if [ ! -f "$CHANGELOG_PATH" ]; then fail "Unable to locate changelog at '$CHANGELOG_PATH'." fi +if ! grep -Eq '^##[[:space:]]*Unreleased[[:space:]]*$' "$CHANGELOG_PATH"; then + fail "CHANGELOG.md must contain a ## Unreleased section before creating or publishing a prerelease." +fi + unreleased_body=$( awk ' /^##[[:space:]]*Unreleased[[:space:]]*$/ { in_unreleased = 1; next } diff --git a/.codex/scripts/prerelease-notes.tests.ps1 b/.codex/scripts/prerelease-notes.tests.ps1 index 5e5075e..9d75f88 100644 --- a/.codex/scripts/prerelease-notes.tests.ps1 +++ b/.codex/scripts/prerelease-notes.tests.ps1 @@ -116,6 +116,30 @@ function Test-PrereleaseNotesRejectsUnreleasedContent { } } +function Test-PrereleaseNotesRejectsMissingUnreleasedHeader { + $FixtureRoot = New-Fixture + try { + $ChangelogPath = Join-Path $FixtureRoot "CHANGELOG.md" + Set-Content -Path $ChangelogPath -Value @' +`1.2.3` +- fixed the client widget timing +'@ + + $Output = Invoke-PrereleaseNotes -Arguments @( + "--changelog", $ChangelogPath, + "--version", "1.2.3", + "--check-only") + if ($LASTEXITCODE -eq 0) { + throw "prerelease-notes.sh unexpectedly accepted a missing Unreleased header." + } + + Assert-Match -Text $Output -Pattern 'must contain a ## Unreleased section' -Message "Missing Unreleased rejection message was not specific." + } + finally { + Remove-Item -Recurse -Force -LiteralPath $FixtureRoot + } +} + function Test-PrereleaseNotesRejectsMissingVersionEntry { $FixtureRoot = New-Fixture try { @@ -186,6 +210,7 @@ function Test-ReleaseWorkflowChecksOnlyDownloadedReleaseChangelog { Test-PrereleaseNotesIncludesChangelogAndDetailsCard Test-PrereleaseNotesRejectsUnreleasedContent +Test-PrereleaseNotesRejectsMissingUnreleasedHeader Test-PrereleaseNotesRejectsMissingVersionEntry Test-ReleaseWorkflowChecksOnlyDownloadedReleaseChangelog