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
165 changes: 165 additions & 0 deletions .codex/scripts/prerelease-notes.sh
Original file line number Diff line number Diff line change
@@ -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" <<EOF
## Eclipse ${VERSION} pre-release

> [!NOTE]
> This GitHub pre-release is the source artifact for the matching Thunderstore publish. Thunderstore receives package version \`${VERSION}\` from tag \`${TAG}\`.

<details open>
<summary>Good to know before Thunderstore</summary>

| 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}\` |

</details>

### Changes

${version_body}
EOF

echo "prerelease-notes: wrote $OUTPUT_PATH"
169 changes: 169 additions & 0 deletions .codex/scripts/prerelease-notes.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
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 '<details open>' -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
}
}

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"
17 changes: 12 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,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

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading
Loading