From e7047123fb37b90bf4bbe98ab48d33f47543eac4 Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Wed, 15 Jul 2026 21:54:47 -0700 Subject: [PATCH 1/3] Add Sparkle auto-updates and a Sparkle-free Mac App Store target --- .github/workflows/ci.yml | 21 +++ .github/workflows/release.yml | 145 ++++++++++++++- CHANGELOG.md | 153 +++++++++++++++ ComputerSolitaire.xcodeproj/project.pbxproj | 175 +++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 15 ++ .../ComputerSolitaireAppStore.xcscheme | 80 ++++++++ .../ComputerSolitaire-macOS.entitlements | 17 ++ ComputerSolitaire/ComputerSolitaireApp.swift | 5 + ComputerSolitaire/Info-AppStore.plist | 8 + ComputerSolitaire/Info.plist | 6 + ComputerSolitaire/UpdaterController.swift | 65 +++++++ ComputerSolitaire/Views/MacSettingsView.swift | 9 + scripts/tag-version.sh | 13 +- 13 files changed, 697 insertions(+), 15 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 ComputerSolitaire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ComputerSolitaire.xcodeproj/xcshareddata/xcschemes/ComputerSolitaireAppStore.xcscheme create mode 100644 ComputerSolitaire/ComputerSolitaire-macOS.entitlements create mode 100644 ComputerSolitaire/Info-AppStore.plist create mode 100644 ComputerSolitaire/UpdaterController.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9947330..4d75643 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,3 +59,24 @@ jobs: -derivedDataPath "$RUNNER_TEMP/DerivedData" \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO + + mas-build: + name: Mac App Store Build + runs-on: macos-26 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + # Keeps the Sparkle-free target compiling: this build has no Sparkle + # package linked, so every canImport(Sparkle) guard must hold. + - name: Build app (Mac App Store target) + run: | + xcodebuild build \ + -project ComputerSolitaire.xcodeproj \ + -scheme ComputerSolitaireAppStore \ + -configuration Debug \ + -destination "platform=macOS" \ + -derivedDataPath "$RUNNER_TEMP/DerivedData" \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 796b840..4c92724 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,16 +20,15 @@ jobs: env: RELEASE_TAG: ${{ github.ref_name }} run: | - MARKETING_VERSIONS="$( + BUILD_SETTINGS="$( xcodebuild \ -project ComputerSolitaire.xcodeproj \ -scheme ComputerSolitaire \ -configuration Release \ -destination "generic/platform=macOS" \ - -showBuildSettings | - awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }' | - sort -u + -showBuildSettings )" + MARKETING_VERSIONS="$(printf '%s\n' "$BUILD_SETTINGS" | awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }' | sort -u)" VERSION_COUNT="$(printf '%s\n' "$MARKETING_VERSIONS" | awk 'NF { count++ } END { print count + 0 }')" if [[ "$VERSION_COUNT" -ne 1 ]]; then @@ -45,6 +44,13 @@ jobs: exit 1 fi + BUILD_VERSIONS="$(printf '%s\n' "$BUILD_SETTINGS" | awk '$1 == "CURRENT_PROJECT_VERSION" && $2 == "=" { print $3 }' | sort -u)" + if [[ "$BUILD_VERSIONS" != "$MARKETING_VERSIONS" ]]; then + echo "CURRENT_PROJECT_VERSION must equal MARKETING_VERSION ($MARKETING_VERSIONS), but found: $BUILD_VERSIONS" + echo "Sparkle compares CFBundleVersion to decide whether an update is newer, so the build version must advance with every release." + exit 1 + fi + - name: Import Apple signing certificate env: P12_BASE64: ${{ secrets.APPLE_CERT_P12_BASE64 }} @@ -169,13 +175,33 @@ jobs: - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} run: | - DMG_PATH="ComputerSolitaire-${{ github.ref_name }}-arm64.dmg" - if gh release view "${{ github.ref_name }}" >/dev/null 2>&1; then - gh release upload "${{ github.ref_name }}" "$DMG_PATH" --clobber + DMG_PATH="ComputerSolitaire-$RELEASE_TAG-arm64.dmg" + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + gh release upload "$RELEASE_TAG" "$DMG_PATH" --clobber + exit 0 + fi + + # Release notes come from this version's CHANGELOG.md section — the + # same text the Sparkle update dialog shows. Fall back to GitHub's + # PR-based notes if the section is missing. + NOTES_FILE="$RUNNER_TEMP/release-notes.md" + awk -v version="${RELEASE_TAG#v}" ' + $1 == "##" && $2 == version { in_section = 1; next } + $1 == "##" { in_section = 0 } + in_section { print } + ' CHANGELOG.md > "$NOTES_FILE" + + if [[ -s "$NOTES_FILE" ]]; then + gh release create "$RELEASE_TAG" \ + --title "$RELEASE_TAG" \ + --notes-file "$NOTES_FILE" \ + "$DMG_PATH" else - gh release create "${{ github.ref_name }}" \ - --title "${{ github.ref_name }}" \ + echo "::warning::CHANGELOG.md has no section for ${RELEASE_TAG#v}; using GitHub-generated notes." + gh release create "$RELEASE_TAG" \ + --title "$RELEASE_TAG" \ --generate-notes \ "$DMG_PATH" fi @@ -189,3 +215,104 @@ jobs: if [[ -n "${NOTARY_API_KEY_PATH:-}" ]]; then rm -f "$NOTARY_API_KEY_PATH" fi + + appcast: + needs: release + runs-on: macos-26 + permissions: + contents: read + + env: + # Releases older than this predate Sparkle (their CFBundleVersion is + # stuck at 1), so they never appear in the appcast. + FIRST_SPARKLE_TAG: v0.8.3 + SPARKLE_VERSION: 2.9.4 + SPARKLE_TOOLS_SHA256: ce89daf967db1e1893ed3ebd67575ed82d3902563e3191ca92aaec9164fbdef9 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Download release archives + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p archives + for TAG in $(gh release list --limit 100 --json tagName --jq '.[].tagName'); do + OLDEST="$(printf '%s\n%s\n' "$FIRST_SPARKLE_TAG" "$TAG" | sort -V | head -1)" + if [[ "$OLDEST" == "$FIRST_SPARKLE_TAG" ]]; then + gh release download "$TAG" --pattern '*.dmg' --dir archives + fi + done + ls archives + + - name: Extract release notes from CHANGELOG.md + run: | + for DMG in archives/*.dmg; do + BASENAME="$(basename "$DMG" .dmg)" + VERSION="$(printf '%s' "$BASENAME" | sed -E 's/^ComputerSolitaire-v([0-9.]+)-arm64$/\1/')" + awk -v version="$VERSION" ' + $1 == "##" && $2 == version { in_section = 1; next } + $1 == "##" { in_section = 0 } + in_section { print } + ' CHANGELOG.md > "archives/$BASENAME.md" + if [[ ! -s "archives/$BASENAME.md" ]]; then + echo "::warning::CHANGELOG.md has no section for $VERSION; its appcast entry will have no release notes." + rm "archives/$BASENAME.md" + fi + done + + - name: Fetch Sparkle tools + run: | + curl -sL -o sparkle-tools.tar.xz \ + "https://github.com/sparkle-project/Sparkle/releases/download/$SPARKLE_VERSION/Sparkle-$SPARKLE_VERSION.tar.xz" + echo "$SPARKLE_TOOLS_SHA256 sparkle-tools.tar.xz" | shasum -a 256 -c - + mkdir -p sparkle-tools + tar -xf sparkle-tools.tar.xz -C sparkle-tools + + - name: Generate appcast + env: + SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }} + run: | + if [[ -z "${SPARKLE_ED_PRIVATE_KEY:-}" ]]; then + echo "Missing required secret: SPARKLE_ED_PRIVATE_KEY" + exit 1 + fi + + mkdir -p site + printf '%s' "$SPARKLE_ED_PRIVATE_KEY" | ./sparkle-tools/bin/generate_appcast archives \ + --ed-key-file - \ + --download-url-prefix "https://github.com/austin-smith/ComputerSolitaire/releases/download/" \ + --link "https://github.com/austin-smith/ComputerSolitaire" \ + --embed-release-notes \ + --maximum-versions 5 \ + --maximum-deltas 0 \ + -o site/appcast.xml + + # Each DMG lives under its release's download path, not a flat + # directory; the tag is recoverable from the filename. + sed -i '' -E \ + 's|(releases/download/)(ComputerSolitaire-(v[0-9.]+)-arm64\.dmg)|\1\3/\2|g' \ + site/appcast.xml + + cat site/appcast.xml + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: site + + deploy-appcast: + needs: appcast + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e68fb29 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,153 @@ +# Changelog + +## 0.8.2 + +### Added + +- Animation Speed setting with Normal, Fast, and Instant options; Reduce Motion now suppresses gameplay animations and the win cascade. ([#76](https://github.com/austin-smith/ComputerSolitaire/pull/76), [#77](https://github.com/austin-smith/ComputerSolitaire/pull/77)) +- Blue and Pink alternate app icons. ([#74](https://github.com/austin-smith/ComputerSolitaire/pull/74)) + +### Changed + +- Updated the primary app icon from blue to teal. ([#74](https://github.com/austin-smith/ComputerSolitaire/pull/74)) + +### Fixed + +- Restored card-flight animations for tap-to-move, drag-and-drop, auto-finish, and invalid-drop returns. ([#75](https://github.com/austin-smith/ComputerSolitaire/pull/75)) + +**Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.8.1...v0.8.2 + +## 0.8.1 + +### Fixed + +#### Performance + +- Reduced per-move rendering across all ten games by limiting animation transactions to affected board regions and skipping unchanged cards, piles, and top rows. ([#70](https://github.com/austin-smith/ComputerSolitaire/pull/70), [#72](https://github.com/austin-smith/ComputerSolitaire/pull/72)) +- Moved saved-game sanitization and JSON encoding off the main thread and skipped superseded autosaves. ([#70](https://github.com/austin-smith/ComputerSolitaire/pull/70)) +- Limited per-frame drag and win-cascade updates to their overlays instead of re-evaluating the entire board, while preserving structural SwiftUI diffing through the main scene. ([#71](https://github.com/austin-smith/ComputerSolitaire/pull/71)) +- Short-circuited FreeCell auto-finish availability checks when cascade ordering cannot complete, avoiding a full win simulation on most moves. ([#70](https://github.com/austin-smith/ComputerSolitaire/pull/70)) + +**Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.8.0...v0.8.1 + +## 0.8.0 + +### Added + +#### Eight new game variants + +Each variant includes its own solver-backed hints, persistence, rules and scoring, statistics, and accessibility support. + +- Spider ([#48](https://github.com/austin-smith/ComputerSolitaire/pull/48)) +- Pyramid ([#47](https://github.com/austin-smith/ComputerSolitaire/pull/47)) +- TriPeaks ([#52](https://github.com/austin-smith/ComputerSolitaire/pull/52)) +- Golf ([#58](https://github.com/austin-smith/ComputerSolitaire/pull/58)) +- Yukon ([#42](https://github.com/austin-smith/ComputerSolitaire/pull/42)) +- Scorpion ([#60](https://github.com/austin-smith/ComputerSolitaire/pull/60)) +- Forty Thieves ([#61](https://github.com/austin-smith/ComputerSolitaire/pull/61)) +- Canfield ([#63](https://github.com/austin-smith/ComputerSolitaire/pull/63)) + +#### Settings and feedback + +- Option to disable haptic feedback on iOS ([#66](https://github.com/austin-smith/ComputerSolitaire/pull/66)) +- Option to hide in-game statistics ([#66](https://github.com/austin-smith/ComputerSolitaire/pull/66)) +- Option to hide stock counts ([#66](https://github.com/austin-smith/ComputerSolitaire/pull/66)) +- Haptic feedback for starting auto-finish and winning games ([#62](https://github.com/austin-smith/ComputerSolitaire/pull/62)) + +### Changed + +- Replaced Settings-based game selection with an on-board game picker. ([#53](https://github.com/austin-smith/ComputerSolitaire/pull/53)) +- Each game mode now keeps its own saved session and statistics; switching modes pauses the current game instead of ending it. ([#53](https://github.com/austin-smith/ComputerSolitaire/pull/53)) +- The game picker now indicates overflow with edge fades and opens with the current game in view. ([#68](https://github.com/austin-smith/ComputerSolitaire/pull/68)) +- Redesigned Statistics as a cross-game overview with per-game detail and mode-specific breakdowns. ([#53](https://github.com/austin-smith/ComputerSolitaire/pull/53)) +- Reorganized the iOS toolbar to keep Hint and Undo visible, show Auto Finish only when available, and move other actions into the More menu. ([#62](https://github.com/austin-smith/ComputerSolitaire/pull/62)) +- Reorganized the macOS toolbar with contextual Auto Finish, grouped Hint and Undo controls, and a combined New Game and Redeal menu. ([#62](https://github.com/austin-smith/ComputerSolitaire/pull/62)) +- Reorganized Settings into focused sections with dedicated appearance pages. ([#66](https://github.com/austin-smith/ComputerSolitaire/pull/66)) +- Replaced the macOS settings sheet with a standard Settings window. ([#66](https://github.com/austin-smith/ComputerSolitaire/pull/66)) +- Rules & Scoring can now display any game instead of only the game in progress. ([#66](https://github.com/austin-smith/ComputerSolitaire/pull/66)) +- Added macOS keyboard shortcuts for switching directly between games. ([#64](https://github.com/austin-smith/ComputerSolitaire/pull/64)) +- Moved statistics reset actions from the iOS toolbar into the statistics form. ([#64](https://github.com/austin-smith/ComputerSolitaire/pull/64)) +- Revised the Rules & Scoring text across all ten games for consistent terminology. ([#64](https://github.com/austin-smith/ComputerSolitaire/pull/64)) +- Added staggered stock-to-tableau deal animations and in-flight card flips for Spider and Scorpion. ([#60](https://github.com/austin-smith/ComputerSolitaire/pull/60), [#61](https://github.com/austin-smith/ComputerSolitaire/pull/61)) + +### Fixed + +- Restoring a saved game no longer replaces its board when the saved variant differs from the stored game setting. ([#47](https://github.com/austin-smith/ComputerSolitaire/pull/47)) +- Undoing Klondike stock draws and waste recycles now flips returning cards during their flights instead of snapping their face state at landing. ([#60](https://github.com/austin-smith/ComputerSolitaire/pull/60)) + +**Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.7.0...v0.8.0 + +## 0.7.0 + +### Added + +- New FreeCell game mode, including supermoves, hints, auto-finish, persistence, rules, and statistics. ([#26](https://github.com/austin-smith/ComputerSolitaire/pull/26)) +- Per-variant and combined statistics for Klondike and FreeCell. ([#26](https://github.com/austin-smith/ComputerSolitaire/pull/26)) +- Pixel and Simple card styles. ([#26](https://github.com/austin-smith/ComputerSolitaire/pull/26), [#32](https://github.com/austin-smith/ComputerSolitaire/pull/32)) +- Configurable card-back colors. ([#32](https://github.com/austin-smith/ComputerSolitaire/pull/32)) +- Alternate app icons. ([#28](https://github.com/austin-smith/ComputerSolitaire/pull/28)) +- About page on iOS. ([#34](https://github.com/austin-smith/ComputerSolitaire/pull/34)) + +### Changed + +- Redesigned the hint and move-advisor system with variant-specific logic, improved Klondike planning, solver-backed FreeCell hints, and updated tap-to-move behavior. ([#26](https://github.com/austin-smith/ComputerSolitaire/pull/26)) +- Redesigned Settings. ([#30](https://github.com/austin-smith/ComputerSolitaire/pull/30), [#32](https://github.com/austin-smith/ComputerSolitaire/pull/32)) +- Updated card rendering, board sizing, draw animations, and felt rendering. ([#32](https://github.com/austin-smith/ComputerSolitaire/pull/32)) +- Improved VoiceOver labels, selected states, and card/pile exposure. ([#39](https://github.com/austin-smith/ComputerSolitaire/pull/39)) + +### Fixed + +- Draw-one recycle penalties now use the draw mode selected when the game was dealt. ([#29](https://github.com/austin-smith/ComputerSolitaire/pull/29)) + +**Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.6.0...v0.7.0 + +## 0.6.0 + +### What's Changed + +* Enhance statistics view in https://github.com/austin-smith/ComputerSolitaire/pull/18 +* Add macOS menu bar game controls in https://github.com/austin-smith/ComputerSolitaire/pull/19 +* Add comprehensive unit tests in https://github.com/austin-smith/ComputerSolitaire/pull/20 + +**Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.5.0...v0.6.0 + +## 0.5.0 + +### What's Changed + +* Fix iOS SFX playback in silent mode in https://github.com/austin-smith/ComputerSolitaire/pull/4 +* Haptic feedback for iOS in https://github.com/austin-smith/ComputerSolitaire/pull/5 +* Replace AppKit & UIKit usage with SwiftUI in https://github.com/austin-smith/ComputerSolitaire/pull/6 +* Code cleanup & optimization in https://github.com/austin-smith/ComputerSolitaire/pull/7 +* Prevent board clipping with responsive board scaling in https://github.com/austin-smith/ComputerSolitaire/pull/8 +* Add hint system in https://github.com/austin-smith/ComputerSolitaire/pull/9 +* Cache hint availability to reduce recycle lag in https://github.com/austin-smith/ComputerSolitaire/pull/10 +* Add win cascade celebration animation in https://github.com/austin-smith/ComputerSolitaire/pull/11 +* Hint system improvements and macOS CI test workflow in https://github.com/austin-smith/ComputerSolitaire/pull/12 + +**Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.4.1...v0.5.0 + +## 0.4.1 + +### What's Changed + +* Add code signing & notary steps to GitHub release action in https://github.com/austin-smith/ComputerSolitaire/pull/3 + +**Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.4.0...v0.4.1 + +## 0.4.0 + +### What's Changed + +* Ensure timer is paused when menus are open in https://github.com/austin-smith/ComputerSolitaire/pull/2 + +**Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.3.0...v0.4.0 + +## 0.3.0 + +### What's Changed + +* Base game functionality +* Scoring and stats in https://github.com/austin-smith/ComputerSolitaire/pull/1 + +**Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/commits/v0.3.0 diff --git a/ComputerSolitaire.xcodeproj/project.pbxproj b/ComputerSolitaire.xcodeproj/project.pbxproj index 9cbbfce..a633f3f 100644 --- a/ComputerSolitaire.xcodeproj/project.pbxproj +++ b/ComputerSolitaire.xcodeproj/project.pbxproj @@ -6,6 +6,10 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 4B75C2102F1A240000761A12 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; platformFilters = (macos, ); productRef = 4B75C20F2F1A240000761A12 /* Sparkle */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ 4B75C1EA2F1A233500761A12 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -25,6 +29,7 @@ /* Begin PBXFileReference section */ 4B75C1D12F1A233400761A12 /* Computer Solitaire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Computer Solitaire.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B75C2212F1A250000761A12 /* Computer Solitaire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Computer Solitaire.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4B75C1E42F1A233500761A12 /* ComputerSolitaireTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ComputerSolitaireTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4B75C1F12F1A233600761A12 /* ComputerSolitaireUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ComputerSolitaireUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -33,10 +38,19 @@ 4B75C1DE2F1A233500761A12 /* Exceptions for "ComputerSolitaire" folder in "ComputerSolitaire" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + "Info-AppStore.plist", Info.plist, ); target = 4B75C1D02F1A233400761A12 /* ComputerSolitaire */; }; + 4B75C2282F1A250000761A12 /* Exceptions for "ComputerSolitaire" folder in "ComputerSolitaireAppStore" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + "Info-AppStore.plist", + Info.plist, + ); + target = 4B75C2202F1A250000761A12 /* ComputerSolitaireAppStore */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -44,6 +58,7 @@ isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( 4B75C1DE2F1A233500761A12 /* Exceptions for "ComputerSolitaire" folder in "ComputerSolitaire" target */, + 4B75C2282F1A250000761A12 /* Exceptions for "ComputerSolitaire" folder in "ComputerSolitaireAppStore" target */, ); path = ComputerSolitaire; sourceTree = ""; @@ -70,6 +85,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4B75C2102F1A240000761A12 /* Sparkle in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,6 +96,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4B75C2232F1A250000761A12 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4B75C1F32F1A233600761A12 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -105,6 +128,7 @@ isa = PBXGroup; children = ( 4B75C1D12F1A233400761A12 /* Computer Solitaire.app */, + 4B75C2212F1A250000761A12 /* Computer Solitaire.app */, 4B75C1E42F1A233500761A12 /* ComputerSolitaireTests.xctest */, 4B75C1F12F1A233600761A12 /* ComputerSolitaireUITests.xctest */, ); @@ -132,11 +156,35 @@ ); name = ComputerSolitaire; packageProductDependencies = ( + 4B75C20F2F1A240000761A12 /* Sparkle */, ); productName = ComputerSolitaire; productReference = 4B75C1D12F1A233400761A12 /* Computer Solitaire.app */; productType = "com.apple.product-type.application"; }; + 4B75C2202F1A250000761A12 /* ComputerSolitaireAppStore */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B75C2252F1A250000761A12 /* Build configuration list for PBXNativeTarget "ComputerSolitaireAppStore" */; + buildPhases = ( + 4B75C2222F1A250000761A12 /* Sources */, + 4B75C2232F1A250000761A12 /* Frameworks */, + 4B75C2242F1A250000761A12 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 4B75C1D32F1A233400761A12 /* ComputerSolitaire */, + 4B75C2002F1A233600761A12 /* Shared */, + ); + name = ComputerSolitaireAppStore; + packageProductDependencies = ( + ); + productName = ComputerSolitaireAppStore; + productReference = 4B75C2212F1A250000761A12 /* Computer Solitaire.app */; + productType = "com.apple.product-type.application"; + }; 4B75C1E92F1A233500761A12 /* ComputerSolitaireTests */ = { isa = PBXNativeTarget; buildConfigurationList = 4B75C1EC2F1A233500761A12 /* Build configuration list for PBXNativeTarget "ComputerSolitaireTests" */; @@ -197,6 +245,9 @@ 4B75C1D02F1A233400761A12 = { CreatedOnToolsVersion = 26.2; }; + 4B75C2202F1A250000761A12 = { + CreatedOnToolsVersion = 26.2; + }; 4B75C1E92F1A233500761A12 = { CreatedOnToolsVersion = 26.2; TestTargetID = 4B75C1D02F1A233400761A12; @@ -216,12 +267,16 @@ ); mainGroup = 4B75C1C82F1A233400761A12; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 4B75C2122F1A240000761A12 /* XCRemoteSwiftPackageReference "Sparkle" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 4B75C1D22F1A233400761A12 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 4B75C1D02F1A233400761A12 /* ComputerSolitaire */, + 4B75C2202F1A250000761A12 /* ComputerSolitaireAppStore */, 4B75C1E92F1A233500761A12 /* ComputerSolitaireTests */, 4B75C1F62F1A233600761A12 /* ComputerSolitaireUITests */, ); @@ -236,6 +291,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4B75C2242F1A250000761A12 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4B75C1E82F1A233500761A12 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -260,6 +322,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4B75C2222F1A250000761A12 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4B75C1E72F1A233500761A12 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -296,9 +365,9 @@ ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "ComputerSolitaireBlueAppIcon ComputerSolitairePinkAppIcon QueenOfHeartsAppIcon"; ASSETCATALOG_COMPILER_APPICON_NAME = ComputerSolitaireAppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "ComputerSolitaire/ComputerSolitaire-macOS.entitlements"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = T778QMSML9; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -324,7 +393,6 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 0.8.2; PRODUCT_BUNDLE_IDENTIFIER = com.crapshack.ComputerSolitaire; PRODUCT_NAME = "Computer Solitaire"; REGISTER_APP_GROUPS = YES; @@ -348,9 +416,9 @@ ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "ComputerSolitaireBlueAppIcon ComputerSolitairePinkAppIcon QueenOfHeartsAppIcon"; ASSETCATALOG_COMPILER_APPICON_NAME = ComputerSolitaireAppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = "ComputerSolitaire/ComputerSolitaire-macOS.entitlements"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = T778QMSML9; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; @@ -376,7 +444,6 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 0.8.2; PRODUCT_BUNDLE_IDENTIFIER = com.crapshack.ComputerSolitaire; PRODUCT_NAME = "Computer Solitaire"; REGISTER_APP_GROUPS = YES; @@ -428,6 +495,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 0.8.3; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = T778QMSML9; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -448,6 +516,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 0.8.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -490,6 +559,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 0.8.3; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = T778QMSML9; ENABLE_NS_ASSERTIONS = NO; @@ -504,12 +574,81 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 0.8.3; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; }; + 4B75C2262F1A250000761A12 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "ComputerSolitaireBlueAppIcon ComputerSolitairePinkAppIcon QueenOfHeartsAppIcon"; + ASSETCATALOG_COMPILER_APPICON_NAME = ComputerSolitaireAppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = T778QMSML9; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "ComputerSolitaire/Info-AppStore.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Computer Solitaire"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.card-games"; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 26.2; + PRODUCT_BUNDLE_IDENTIFIER = com.crapshack.ComputerSolitaire; + PRODUCT_NAME = "Computer Solitaire"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + 4B75C2272F1A250000761A12 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "ComputerSolitaireBlueAppIcon ComputerSolitairePinkAppIcon QueenOfHeartsAppIcon"; + ASSETCATALOG_COMPILER_APPICON_NAME = ComputerSolitaireAppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = T778QMSML9; + ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SELECTED_FILES = readonly; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "ComputerSolitaire/Info-AppStore.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "Computer Solitaire"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.card-games"; + LD_RUNPATH_SEARCH_PATHS = "@executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 26.2; + PRODUCT_BUNDLE_IDENTIFIER = com.crapshack.ComputerSolitaire; + PRODUCT_NAME = "Computer Solitaire"; + REGISTER_APP_GROUPS = YES; + SDKROOT = auto; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 6.0; + }; + name = Release; + }; 4B75C1ED2F1A233500761A12 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -605,6 +744,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 4B75C2252F1A250000761A12 /* Build configuration list for PBXNativeTarget "ComputerSolitaireAppStore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B75C2262F1A250000761A12 /* Debug */, + 4B75C2272F1A250000761A12 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 4B75C1EC2F1A233500761A12 /* Build configuration list for PBXNativeTarget "ComputerSolitaireTests" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -624,6 +772,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 4B75C2122F1A240000761A12 /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.7.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4B75C20F2F1A240000761A12 /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = 4B75C2122F1A240000761A12 /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 4B75C1C92F1A233400761A12 /* Project object */; } diff --git a/ComputerSolitaire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ComputerSolitaire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..feb7b4b --- /dev/null +++ b/ComputerSolitaire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "e721da7f9826abdffcb6185e886155efa2514bd6234475f1afa893e29eb258d6", + "pins" : [ + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7", + "version" : "2.9.4" + } + } + ], + "version" : 3 +} diff --git a/ComputerSolitaire.xcodeproj/xcshareddata/xcschemes/ComputerSolitaireAppStore.xcscheme b/ComputerSolitaire.xcodeproj/xcshareddata/xcschemes/ComputerSolitaireAppStore.xcscheme new file mode 100644 index 0000000..66fbc48 --- /dev/null +++ b/ComputerSolitaire.xcodeproj/xcshareddata/xcschemes/ComputerSolitaireAppStore.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ComputerSolitaire/ComputerSolitaire-macOS.entitlements b/ComputerSolitaire/ComputerSolitaire-macOS.entitlements new file mode 100644 index 0000000..14df848 --- /dev/null +++ b/ComputerSolitaire/ComputerSolitaire-macOS.entitlements @@ -0,0 +1,17 @@ + + + + + + com.apple.security.network.client + + + com.apple.security.temporary-exception.mach-lookup.global-name + + $(PRODUCT_BUNDLE_IDENTIFIER)-spks + $(PRODUCT_BUNDLE_IDENTIFIER)-spki + + + diff --git a/ComputerSolitaire/ComputerSolitaireApp.swift b/ComputerSolitaire/ComputerSolitaireApp.swift index 8758acb..9925f81 100644 --- a/ComputerSolitaire/ComputerSolitaireApp.swift +++ b/ComputerSolitaire/ComputerSolitaireApp.swift @@ -87,6 +87,11 @@ struct ComputerSolitaireApp: App { } ) } +#if canImport(Sparkle) + CommandGroup(after: .appInfo) { + CheckForUpdatesButton() + } +#endif CommandGroup(replacing: .help) { Button { NotificationCenter.default.post(name: .openRulesAndScoring, object: nil) diff --git a/ComputerSolitaire/Info-AppStore.plist b/ComputerSolitaire/Info-AppStore.plist new file mode 100644 index 0000000..bc11256 --- /dev/null +++ b/ComputerSolitaire/Info-AppStore.plist @@ -0,0 +1,8 @@ + + + + + ITSAppUsesNonExemptEncryption + + + diff --git a/ComputerSolitaire/Info.plist b/ComputerSolitaire/Info.plist index 793300d..a62b196 100644 --- a/ComputerSolitaire/Info.plist +++ b/ComputerSolitaire/Info.plist @@ -4,6 +4,12 @@ ITSAppUsesNonExemptEncryption + SUEnableInstallerLauncherService + + SUFeedURL + https://austin-smith.github.io/ComputerSolitaire/appcast.xml + SUPublicEDKey + l8WjhlfHwyMVRSanT6huChqGGfRKOcB1WyoEUd+4DBk= UIBackgroundModes remote-notification diff --git a/ComputerSolitaire/UpdaterController.swift b/ComputerSolitaire/UpdaterController.swift new file mode 100644 index 0000000..ecc0b86 --- /dev/null +++ b/ComputerSolitaire/UpdaterController.swift @@ -0,0 +1,65 @@ +#if os(macOS) && canImport(Sparkle) +import Combine +import Sparkle +import SwiftUI + +/// App-lifetime owner of the Sparkle updater for the direct-download build. +/// Mac App Store builds don't link Sparkle, so this file compiles away and +/// every call site is guarded the same way. +final class UpdaterController: ObservableObject { + static let shared = UpdaterController() + + @Published private(set) var canCheckForUpdates = false + + private let standardController: SPUStandardUpdaterController + + private init() { + standardController = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: nil, + userDriverDelegate: nil + ) + standardController.updater.publisher(for: \.canCheckForUpdates) + .assign(to: &$canCheckForUpdates) + } + + var automaticallyChecksForUpdates: Bool { + get { standardController.updater.automaticallyChecksForUpdates } + set { + objectWillChange.send() + standardController.updater.automaticallyChecksForUpdates = newValue + } + } + + func checkForUpdates() { + standardController.checkForUpdates(nil) + } +} + +struct CheckForUpdatesButton: View { + @ObservedObject private var updater = UpdaterController.shared + + var body: some View { + Button("Check for Updates…") { + updater.checkForUpdates() + } + .disabled(!updater.canCheckForUpdates) + } +} + +struct UpdatesSettingsRows: View { + @ObservedObject private var updater = UpdaterController.shared + + var body: some View { + Toggle("Automatically check for updates", isOn: automaticallyChecksForUpdates) + .toggleStyle(.switch) + } + + private var automaticallyChecksForUpdates: Binding { + Binding( + get: { updater.automaticallyChecksForUpdates }, + set: { updater.automaticallyChecksForUpdates = $0 } + ) + } +} +#endif diff --git a/ComputerSolitaire/Views/MacSettingsView.swift b/ComputerSolitaire/Views/MacSettingsView.swift index 7ef16c8..468fe13 100644 --- a/ComputerSolitaire/Views/MacSettingsView.swift +++ b/ComputerSolitaire/Views/MacSettingsView.swift @@ -10,7 +10,11 @@ import SwiftUI struct MacSettingsView: View { private enum PaneMetrics { static let width: CGFloat = 500 +#if canImport(Sparkle) + static let generalHeight: CGFloat = 460 +#else static let generalHeight: CGFloat = 355 +#endif static let appearanceHeight: CGFloat = 560 static let rulesHeight: CGFloat = 500 static let aboutHeight: CGFloat = 400 @@ -26,6 +30,11 @@ struct MacSettingsView: View { Section("Gameplay") { GameplaySettingsRows() } +#if canImport(Sparkle) + Section("Updates") { + UpdatesSettingsRows() + } +#endif } .formStyle(.grouped) .frame(width: PaneMetrics.width, height: PaneMetrics.generalHeight) diff --git a/scripts/tag-version.sh b/scripts/tag-version.sh index 11d1ede..eb10e82 100755 --- a/scripts/tag-version.sh +++ b/scripts/tag-version.sh @@ -71,14 +71,16 @@ if [[ "$head_commit" != "$main_commit" ]]; then exit 1 fi -versions="$({ +build_settings="$( xcodebuild \ -project ComputerSolitaire.xcodeproj \ -scheme ComputerSolitaire \ -configuration Release \ -destination "generic/platform=macOS" \ -showBuildSettings -} | awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }' | sort -u)" +)" + +versions="$(printf '%s\n' "$build_settings" | awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }' | sort -u)" version_count="$(printf '%s\n' "$versions" | awk 'NF { count++ } END { print count + 0 }')" if [[ "$version_count" -ne 1 ]]; then @@ -95,6 +97,13 @@ if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then exit 1 fi +build_versions="$(printf '%s\n' "$build_settings" | awk '$1 == "CURRENT_PROJECT_VERSION" && $2 == "=" { print $3 }' | sort -u)" +if [[ "$build_versions" != "$version" ]]; then + echo "CURRENT_PROJECT_VERSION must equal MARKETING_VERSION ($version), but found: $build_versions" >&2 + echo "Sparkle compares CFBundleVersion to decide whether an update is newer, so the build version must advance with every release." >&2 + exit 1 +fi + tag="v$version" if git show-ref --verify --quiet "refs/tags/$tag"; then From e9ca4745aef5e415583c1105b9b1cb6f66a1f0ab Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Wed, 15 Jul 2026 21:56:36 -0700 Subject: [PATCH 2/3] Update About page description --- ComputerSolitaire/Views/AboutView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ComputerSolitaire/Views/AboutView.swift b/ComputerSolitaire/Views/AboutView.swift index 3f3a93b..fadf6ae 100644 --- a/ComputerSolitaire/Views/AboutView.swift +++ b/ComputerSolitaire/Views/AboutView.swift @@ -32,7 +32,7 @@ struct AboutView: View { Text( "Computer Solitaire is a low-frills, ad-free solitaire game for your computer. " - + "Includes Klondike, Spider, FreeCell, and other things you enjoy." + + "It includes Klondike, Spider, FreeCell, and other things you enjoy." ) .font(.system(size: 14)) .foregroundStyle(.primary) @@ -121,7 +121,7 @@ struct AboutView: View { Text( "Computer Solitaire is a low-frills, ad-free solitaire game for your computer. " - + "Includes Klondike, Spider, FreeCell, and other things you enjoy." + + "It includes Klondike, Spider, FreeCell, and other things you enjoy." ) .font(.system(size: 11)) .foregroundStyle(.primary) From 35f94e0696350e74dac599985cdaa122e458515b Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Wed, 15 Jul 2026 22:01:38 -0700 Subject: [PATCH 3/3] Add release dates to changelog headings --- CHANGELOG.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e68fb29..9924251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.8.2 +## 0.8.2 - 2026-07-15 ### Added @@ -17,7 +17,7 @@ **Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.8.1...v0.8.2 -## 0.8.1 +## 0.8.1 - 2026-07-14 ### Fixed @@ -30,7 +30,7 @@ **Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.8.0...v0.8.1 -## 0.8.0 +## 0.8.0 - 2026-07-14 ### Added @@ -77,7 +77,7 @@ Each variant includes its own solver-backed hints, persistence, rules and scorin **Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.7.0...v0.8.0 -## 0.7.0 +## 0.7.0 - 2026-07-12 ### Added @@ -101,7 +101,7 @@ Each variant includes its own solver-backed hints, persistence, rules and scorin **Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.6.0...v0.7.0 -## 0.6.0 +## 0.6.0 - 2026-02-23 ### What's Changed @@ -111,7 +111,7 @@ Each variant includes its own solver-backed hints, persistence, rules and scorin **Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.5.0...v0.6.0 -## 0.5.0 +## 0.5.0 - 2026-02-21 ### What's Changed @@ -127,7 +127,7 @@ Each variant includes its own solver-backed hints, persistence, rules and scorin **Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.4.1...v0.5.0 -## 0.4.1 +## 0.4.1 - 2026-02-16 ### What's Changed @@ -135,7 +135,7 @@ Each variant includes its own solver-backed hints, persistence, rules and scorin **Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.4.0...v0.4.1 -## 0.4.0 +## 0.4.0 - 2026-02-16 ### What's Changed @@ -143,7 +143,7 @@ Each variant includes its own solver-backed hints, persistence, rules and scorin **Full Changelog**: https://github.com/austin-smith/ComputerSolitaire/compare/v0.3.0...v0.4.0 -## 0.3.0 +## 0.3.0 - 2026-02-16 ### What's Changed