From f0a46134740c9596d06d2b94c675404f74f2396e Mon Sep 17 00:00:00 2001 From: mfoltz Date: Mon, 18 May 2026 08:57:42 -0500 Subject: [PATCH 1/3] [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/3] [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/3] [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