diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9496b08..9b7964f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,31 @@ jobs: echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "version=${tag#v}" >> "$GITHUB_OUTPUT" + - name: Checkout release tooling + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Verify changelog and build release notes + shell: bash + env: + RELEASE_TAG: ${{ steps.release.outputs.tag }} + run: | + set -euo pipefail + if ! git rev-parse -q --verify "refs/tags/$RELEASE_TAG" >/dev/null; then + git fetch --no-tags --depth=1 origin "refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG" + fi + git show "refs/tags/$RELEASE_TAG:CHANGELOG.md" > CHANGELOG.md + scripts/changelog.sh release-notes "$RELEASE_TAG" --output dist/release-notes.md + + - name: Upload release notes + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: PtionsPlus-release-notes + path: dist/release-notes.md + if-no-files-found: error + tests: name: Test release commit needs: metadata @@ -150,6 +175,12 @@ jobs: name: PtionsPlus-release-assets path: release-assets + - name: Download release notes + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: PtionsPlus-release-notes + path: release-notes + - name: Attach assets to GitHub Release shell: bash env: @@ -159,10 +190,19 @@ jobs: RELEASE_VERSION: ${{ needs.metadata.outputs.version }} run: | set -euo pipefail - if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + notes="release-notes/release-notes.md" + if [[ ! -s "$notes" ]]; then + echo "Release notes from the changelog are missing." + exit 1 + fi + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + gh release edit "$RELEASE_TAG" \ + --title "Ptions+ ${RELEASE_VERSION}" \ + --notes-file "$notes" + else gh release create "$RELEASE_TAG" \ --title "Ptions+ ${RELEASE_VERSION}" \ - --notes "Signed, notarized, stapled, and verified macOS release artifacts." + --notes-file "$notes" fi gh release upload "$RELEASE_TAG" \ "release-assets/Ptions+.dmg" \ diff --git a/AGENTS.md b/AGENTS.md index 7a3ae58..5d87af4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,6 +85,18 @@ bash scripts/sign-release.sh bash scripts/notarize.sh ``` +Release notes are never written by hand. Every user-facing change is described +under `## Unreleased` in `CHANGELOG.md`, `bump-version.sh` promotes those +entries into the dated `## X.Y.Z` section it creates, and the release workflow +publishes exactly that section as the GitHub release body. A tag whose version +has no changelog entries, or that leaves entries under `## Unreleased`, fails +before anything is built or signed. + +```bash +# Preview the notes a tag would publish +scripts/changelog.sh release-notes v1.2.0 +``` + ## High-risk operations Never perform any of the following without an explicit, specific instruction @@ -141,6 +153,7 @@ Generated or tool-owned. Do not hand-edit. | `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION` in `PtionsPlus.xcodeproj/project.pbxproj` | `scripts/bump-version.sh` | | `PtionsPlus/Utilities/ProductIdentity.swift` version constants | Derived from the bundle at runtime; values come from the build | | `"softwareVersion"` and the version badge in `docs/index.html` | `scripts/bump-version.sh` | +| The dated `## X.Y.Z` headings in `CHANGELOG.md` | `scripts/bump-version.sh` promotes the entries you wrote under `## Unreleased` | | `docs/assets/core.tokens.css`, `docs/assets/instrument-workshop.css`, `docs/assets/instrument-workshop-fonts.css`, `docs/assets/fonts/**` | Vendored from `trsdn/design-system`. Re-vendor from a tag; never patch in place. | | `.github/badges/*.svg` | Generated from `.github/conformance.yml` and the stats workflow | | `build/`, `dist/`, `TestResults/` | Build output, gitignored | diff --git a/CHANGELOG.md b/CHANGELOG.md index fd042af..7be4049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +- Added an About window reachable from the menu bar that shows the app version and links to the repository, issue tracker, and licence (#32). +- Reworked the published website onto vendored design-system styling with self-hosted fonts, removing the third-party font requests that previously observed every visitor (#32). +- Added `AGENTS.md` as the single home for build, test, and release instructions, plus `scripts/validate.sh` as one validation command (#32). +- Added a changelog gate to the release pipeline: a tag whose version has no dated `CHANGELOG.md` section, or that leaves entries under `## Unreleased`, now fails before anything is built or signed. +- Added `scripts/changelog.sh` and made the GitHub release body the changelog section of the released version instead of fixed boilerplate text. +- Added changelog promotion to `scripts/bump-version.sh`, which moves the unreleased entries into a dated section for the new version. +- Added changelog verification to the documentation checks, so the version being shipped is always described. + ## 1.2.0 - 2026-08-31 - Added Next Space and Previous Space preset actions so spaces can be switched from a mouse button (#28). diff --git a/README.md b/README.md index efc2c75..78b9647 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ Build, test, lint, and release commands are documented in ### Signed Release Build +Every release describes itself through `CHANGELOG.md`. Add each user-facing change under `## Unreleased` while you work; `bump-version.sh` then promotes those entries into a dated section for the new version, and the release workflow publishes exactly that section as the GitHub release notes. + The release flow is three commands: bump version, sign, notarize. First create your local release config: @@ -138,6 +140,12 @@ xcrun notarytool store-credentials "PtionsPlus" \ bash scripts/notarize.sh ``` +Preview the notes a tag would publish: + +```bash +scripts/changelog.sh release-notes v1.2.0 +``` + If you already have a working `notarytool` keychain profile from another project, set it in `.release.env` or inline: ```bash @@ -155,7 +163,7 @@ The release scripts produce four outputs: The final ZIP and DMG are rebuilt from the stapled app and verified before upload. -The GitHub release workflow builds signed, notarized artifacts on `v*` tags. Configure these repository secrets first: +The GitHub release workflow builds signed, notarized artifacts on `v*` tags. It refuses to release when the changelog has no entries for the tagged version or when entries are still parked under `## Unreleased`. Configure these repository secrets first: `MACOS_CERTIFICATE`, `MACOS_CERTIFICATE_PWD`, `APPLE_ID`, `APPLE_TEAM_ID`, and `APPLE_APP_PASSWORD`. ## Versioning and compatibility diff --git a/docs/self-assessment.md b/docs/self-assessment.md index 16e91f6..fb16700 100644 --- a/docs/self-assessment.md +++ b/docs/self-assessment.md @@ -83,7 +83,7 @@ Not applicable, with rationale: | R03 | pass | A `v*` tag runs [`release.yml`](../.github/workflows/release.yml), which builds, signs, notarises, staples, and uploads a ZIP, a DMG, and a SHA-256 file. | | R04 | pass | `scripts/verify-version.sh --tag` rejects a tag that does not match `MARKETING_VERSION`. | | R05 | pass | `scripts/verify-release-artifacts.sh` rebuilds the ZIP and DMG from the stapled app and verifies them before upload; the release workflow reruns the full CI suite against the tagged commit first. | -| R06 | pass | `CHANGELOG.md` and GitHub release notes. | +| R06 | pass | The release workflow gates on `scripts/changelog.sh release-notes` and publishes the `CHANGELOG.md` section of the tagged version as the release body, so a release cannot ship without described changes. | ## Product Identity diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index f51ca41..419d9b5 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -5,6 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" PROJECT_FILE="$PROJECT_DIR/PtionsPlus.xcodeproj/project.pbxproj" WEBSITE_FILE="$PROJECT_DIR/docs/index.html" +CHANGELOG_FILE="$PROJECT_DIR/CHANGELOG.md" if [ $# -ne 1 ]; then echo "Usage: $0 patch|minor|major" @@ -39,17 +40,45 @@ esac next_version="$major.$minor.$patch" next_build=$((current_build + 1)) +release_date=$(date -u +%Y-%m-%d) + +unreleased_entries="$("$SCRIPT_DIR/changelog.sh" section unreleased)" +if [ -z "$unreleased_entries" ]; then + echo "Error: CHANGELOG.md has no entries under '## Unreleased'." + echo "Describe the changes of $next_version there before bumping the version." + exit 1 +fi + +if [ -n "$("$SCRIPT_DIR/changelog.sh" section "$next_version")" ]; then + echo "Error: CHANGELOG.md already has a section for $next_version." + exit 1 +fi perl -0pi -e "s/MARKETING_VERSION = \Q$current_version\E;/MARKETING_VERSION = $next_version;/g; s/CURRENT_PROJECT_VERSION = \Q$current_build\E;/CURRENT_PROJECT_VERSION = $next_build;/g" "$PROJECT_FILE" # The site is machine-owned for these three facts: the structured-data version, # every visible version marker, and the review date the page publishes. -review_date_iso=$(date -u +%Y-%m-%d) review_date_human=$(date -u "+%-d %B %Y") perl -0pi -e "s/\"softwareVersion\": \"[^\"]+\"/\"softwareVersion\": \"$next_version\"/" "$WEBSITE_FILE" perl -0pi -e "s/()[^<]*(<\/span>)/\${1}$next_version\${2}/g" "$WEBSITE_FILE" -perl -0pi -e "s/