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
3 changes: 0 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,6 @@ jobs:
path: appcast/appcast.xml
retention-days: 90

- name: Extract release notes from CHANGELOG.md
run: scripts/ci/extract-release-notes.sh "${GITHUB_REF#refs/tags/v}"

- name: Create GitHub Release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/repo-hygiene.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ jobs:
- name: Validate the test quarantine script
run: python3 scripts/ci/test_quarantine_args.py

- name: Validate release note extraction
run: python3 scripts/ci/test_release_notes.py

# A check that guarded a real invariant and ran nowhere. Pure grep over Swift sources, so it
# belongs on the free Linux runner. The MongoDB filter-shape check is the other one that was
# orphaned, but it compiles a C probe against Libs/libbson, so it lives in the macOS build
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Update release notes show all changes for the offered version, with new features before fixes and properly formatted Markdown. The full changelog is also available from Help and Software Update settings.
- Blank welcome window list when a search matched nothing and a favorite existed.
- Welcome window reading No Connections while a tag filter hid every connection.
- Favorited connection inside a group listed twice on the welcome window.
Expand Down
5 changes: 5 additions & 0 deletions TablePro/Core/Menu/AppDelegate+MainMenuActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ extension AppDelegate: NSMenuItemValidation {
open(MainMenuLink.documentation)
}

@objc func openChangelog(_ sender: Any?) {
open(MainMenuLink.changelog)
}

@objc func openGitHubRepository(_ sender: Any?) {
open(MainMenuLink.repository)
}
Expand All @@ -127,6 +131,7 @@ extension AppDelegate: NSMenuItemValidation {
}

enum MainMenuLink {
static let changelog = "https://docs.tablepro.app/changelog"
static let website = "https://tablepro.app"
static let documentation = "https://docs.tablepro.app"
static let repository = "https://github.com/TableProApp/TablePro"
Expand Down
4 changes: 4 additions & 0 deletions TablePro/Core/Menu/HelpMenuBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ enum HelpMenuBuilder {
String(localized: "GitHub Repository"),
action: #selector(AppDelegate.openGitHubRepository(_:))
),
MenuItemFactory.item(
String(localized: "What's New"),
action: #selector(AppDelegate.openChangelog(_:))
),
MenuItemFactory.separator,
MenuItemFactory.item(
String(localized: "Getting Started"),
Expand Down
4 changes: 4 additions & 0 deletions TablePro/Views/Settings/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ struct GeneralSettingsView: View {
updaterBridge.checkForUpdates()
}
.disabled(!updaterBridge.canCheckForUpdates)

if let changelogURL = URL(string: MainMenuLink.changelog) {
Link("What's New", destination: changelogURL)
}
}

Section {
Expand Down
11 changes: 11 additions & 0 deletions TableProTests/Core/Menu/MainMenuBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ private func flatten(_ menu: NSMenu) -> [NSMenuItem] {
@Suite("Main menu structure")
@MainActor
struct MainMenuStructureTests {
@Test("What's New stays reachable from Help without an active connection")
func changelogIsReachable() throws {
let help = try #require(buildMenu().items.first { $0.title == String(localized: "Help") }?.submenu)
let item = try #require(help.items.first { $0.title == String(localized: "What's New") })
#expect(item.action == #selector(AppDelegate.openChangelog(_:)))
#expect(item.target == nil)
#expect(item.keyEquivalent.isEmpty)
#expect(AppDelegate().validateMenuItem(item))
#expect(MainMenuLink.changelog == "https://docs.tablepro.app/changelog")
}

@Test("Top level order follows the macOS HIG")
func topLevelOrder() {
let titles = buildMenu().items.map(\.title)
Expand Down
4 changes: 4 additions & 0 deletions docs/customization/general-settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ External links you chose to always allow, each with **Forget**, plus **Forget Al

## Software update

The update window shows the complete changelog for the offered version: new features first, then improvements and fixes. Longer notes scroll inside the window; no entries are shortened or omitted.

**What's New** opens the full changelog, including features and fixes in each release. It is also available from **Help > What's New**, even after dismissing an update notification.

**Automatically check for updates** is on and checks in the background. **Check for Updates…** checks now, here or as **TablePro > Check for Updates…** in the menu bar. Updates come through [Sparkle](https://sparkle-project.org/) and are signature-checked before they install.

## Privacy
Expand Down
73 changes: 73 additions & 0 deletions scripts/ci/extract-release-notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Keep a release's complete Markdown, with new features before fixes."""

from pathlib import Path
import re
import sys


SECTION_ORDER = {
"added": 1,
"features": 1,
"new features": 1,
"changed": 2,
"performance": 2,
"fixed": 3,
}


def extract_notes(changelog, version):
# Keep each section intact, including nested lists, code, and unknown headings.
# The sort is stable, so sections at the same priority keep their original order.
sections = [(0, [])]
found = False
fence = None
for line in changelog.splitlines(keepends=True):
if fence is not None:
if re.fullmatch(r" {0,3}" + re.escape(fence[0]) + "{" + str(len(fence)) + r",}\s*", line):
fence = None
if found:
sections[-1][1].append(line)
continue

opening_fence = re.match(r"^ {0,3}(`{3,}|~{3,})", line)
if opening_fence:
fence = opening_fence[1]
if found:
sections[-1][1].append(line)
continue

release = re.match(r"^## \[([^]]+)\]", line)
if release:
if found:
break
found = release[1] == version
continue

if found:
heading = re.match(r"^### (.+?)\s*$", line)
if heading:
sections.append((SECTION_ORDER.get(heading[1].casefold(), 4), []))
sections[-1][1].append(line)

blocks = ["".join(lines).strip("\n") for _, lines in sorted(sections, key=lambda section: section[0])]
notes = "\n\n".join(block for block in blocks if block.strip())
if not notes.strip():
raise ValueError(f"No release notes found for version {version} in CHANGELOG.md")
return notes + "\n"


def main():
if len(sys.argv) != 2:
sys.exit("Usage: extract-release-notes.py <version>")
try:
notes = extract_notes(Path("CHANGELOG.md").read_text(encoding="utf-8"), sys.argv[1])
Path("release_notes.md").write_text(notes, encoding="utf-8")
except (OSError, ValueError) as error:
sys.exit(f"ERROR: {error}")
print(f"Release notes extracted for {sys.argv[1]}:")
print(notes, end="")


if __name__ == "__main__":
main()
22 changes: 1 addition & 21 deletions scripts/ci/extract-release-notes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,4 @@ set -euo pipefail

VERSION="${1:?Usage: extract-release-notes.sh <version>}"

echo "Extracting release notes for version: $VERSION"

# Extract the section for this version from CHANGELOG.md
# Matches from "## [X.Y.Z]" until the next "## [" or end of file
NOTES=$(awk -v ver="$VERSION" '
/^## \[/ {
if (found) exit
if ($0 ~ "\\[" ver "\\]") { found=1; next }
}
found { print }
' CHANGELOG.md)

if [ -z "$NOTES" ]; then
echo "⚠️ No changelog entry found for version $VERSION, using fallback"
echo "- Bug fixes and improvements" > release_notes.md
else
echo "$NOTES" > release_notes.md
fi

echo "✅ Release notes extracted"
cat release_notes.md
python3 "$(dirname "$0")/extract-release-notes.py" "$VERSION"
46 changes: 14 additions & 32 deletions scripts/ci/sign-and-appcast.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ if [ -z "${SPARKLE_PRIVATE_KEY:-}" ]; then
fi

# ---------------------------------------------------------------------------
# 1. Locate Sparkle tools
# 1. Extract the same version-specific notes used by the GitHub release
# ---------------------------------------------------------------------------
bash "$(dirname "$0")/extract-release-notes.sh" "$VERSION"

# ---------------------------------------------------------------------------
# 2. Locate Sparkle tools
# ---------------------------------------------------------------------------
# Pinned and checksum-verified rather than installed from a cask that tracks latest. This step
# holds the EdDSA private key that signs every update every user receives, so it should not run a
Expand All @@ -35,35 +40,6 @@ echo "$SPARKLE_SHA256 $SPARKLE_DIR/sparkle.tar.xz" | shasum -a 256 -c -
tar xf "$SPARKLE_DIR/sparkle.tar.xz" -C "$SPARKLE_DIR"
SPARKLE_BIN="$SPARKLE_DIR/bin"

# ---------------------------------------------------------------------------
# 2. Extract release notes from CHANGELOG.md → HTML
# ---------------------------------------------------------------------------
if [ -f release_notes.md ]; then
NOTES=$(cat release_notes.md)
else
NOTES=$(awk "/^## \\[${VERSION}\\]/{flag=1; next} /^## \\[/{flag=0} flag" CHANGELOG.md)
fi

if [ -z "$NOTES" ]; then
RELEASE_HTML="<ul><li>Bug fixes and improvements</li></ul>"
else
RELEASE_HTML=$(echo "$NOTES" | sed -E \
-e 's/^### (.+)$/<h3>\1<\/h3>/' \
-e 's/^- (.+)$/<li>\1<\/li>/' \
-e '/^[[:space:]]*$/d' \
| awk '
/<li>/ {
if (!in_list) { print "<ul>"; in_list=1 }
print; next
}
{
if (in_list) { print "</ul>"; in_list=0 }
print
}
END { if (in_list) print "</ul>" }
')
fi

DOWNLOAD_PREFIX="${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REPOSITORY:-TableProApp/TablePro}/releases/download/v${VERSION}/"

KEY_FILE=$(mktemp)
Expand Down Expand Up @@ -91,9 +67,14 @@ for arch in "${ARCHS[@]}"; do

cp "$ZIP" "$STAGING/"

# Release notes file matching archive name
# Sparkle 2.9 renders Markdown natively, including code and links. Feeding hand-built HTML
# left Markdown visible and interpreted literal SQL/XML angle brackets as HTML tags.
basename="${STAGING}/TablePro-${VERSION}-${arch}"
echo "$RELEASE_HTML" > "${basename}.html"
{
printf "# What's New in TablePro %s\n\n" "$VERSION"
cat release_notes.md
printf '\n[View full changelog](https://docs.tablepro.app/changelog)\n'
} > "${basename}.md"

# Seed the generator with the feed that is actually published, so every version already in it
# survives. The default is the checkout's own appcast.xml, which is the file as of the tag
Expand All @@ -106,6 +87,7 @@ for arch in "${ARCHS[@]}"; do
--ed-key-file "$KEY_FILE" \
--download-url-prefix "$DOWNLOAD_PREFIX" \
--embed-release-notes \
--full-release-notes-url "https://docs.tablepro.app/changelog" \
--maximum-versions 0 \
"$STAGING"

Expand Down
Loading
Loading