diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4c92724..99e1746 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -44,12 +44,6 @@ 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:
@@ -84,7 +78,12 @@ jobs:
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
- name: Build macOS app archive
+ env:
+ RELEASE_TAG: ${{ github.ref_name }}
run: |
+ # CURRENT_PROJECT_VERSION override: the Sparkle feed compares
+ # CFBundleVersion, so released builds stamp the marketing version
+ # there. The project file itself keeps a plain build number.
xcodebuild archive \
-project ComputerSolitaire.xcodeproj \
-scheme ComputerSolitaire \
@@ -95,15 +94,45 @@ jobs:
CODE_SIGN_IDENTITY="Developer ID Application" \
DEVELOPMENT_TEAM="${{ secrets.APPLE_TEAM_ID }}" \
OTHER_CODE_SIGN_FLAGS="--keychain $KEYCHAIN_PATH" \
+ CURRENT_PROJECT_VERSION="${RELEASE_TAG#v}" \
ARCHS=arm64
+ # Exporting re-signs all nested code (Sparkle's Autoupdate, Updater.app,
+ # and XPC services ship ad-hoc signed) with the Developer ID identity and
+ # secure timestamps. Copying the app straight out of the archive leaves
+ # those ad-hoc signatures in place and notarization rejects them.
+ - name: Export Developer ID app
+ run: |
+ cat > build/ExportOptions.plist <<'EOF'
+
+
+
+
+ method
+ developer-id
+ signingStyle
+ manual
+ signingCertificate
+ Developer ID Application
+ teamID
+ TEAM_ID_PLACEHOLDER
+
+
+ EOF
+ sed -i '' "s/TEAM_ID_PLACEHOLDER/${{ secrets.APPLE_TEAM_ID }}/" build/ExportOptions.plist
+
+ xcodebuild -exportArchive \
+ -archivePath build/ComputerSolitaire.xcarchive \
+ -exportOptionsPlist build/ExportOptions.plist \
+ -exportPath build/export
+
- name: Create DMG
run: |
mkdir -p dmg-contents
- APP_PATH="$(find build/ComputerSolitaire.xcarchive/Products/Applications -maxdepth 1 -name '*.app' -print -quit)"
+ APP_PATH="$(find build/export -maxdepth 1 -name '*.app' -print -quit)"
if [[ -z "${APP_PATH}" ]]; then
- echo "No .app bundle found in archive output."
+ echo "No .app bundle found in export output."
exit 1
fi
@@ -146,11 +175,29 @@ jobs:
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
run: |
DMG_PATH="ComputerSolitaire-${{ github.ref_name }}-arm64.dmg"
- xcrun notarytool submit "$DMG_PATH" \
+
+ # notarytool's exit code reflects "submission completed", not the
+ # verdict — an Invalid result can still exit 0. Check the status
+ # explicitly and surface Apple's per-file log on failure.
+ SUBMIT_OUTPUT="$(xcrun notarytool submit "$DMG_PATH" \
--key "$NOTARY_API_KEY_PATH" \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER_ID" \
- --wait
+ --wait | tee /dev/stderr)"
+
+ SUBMISSION_ID="$(printf '%s\n' "$SUBMIT_OUTPUT" | awk '$1 == "id:" { print $2; exit }')"
+ STATUS="$(printf '%s\n' "$SUBMIT_OUTPUT" | awk '$1 == "status:" { status = $2 } END { print status }')"
+
+ if [[ "$STATUS" != "Accepted" ]]; then
+ echo "Notarization verdict: ${STATUS:-unknown} — fetching Apple's log for submission ${SUBMISSION_ID:-unknown}"
+ if [[ -n "$SUBMISSION_ID" ]]; then
+ xcrun notarytool log "$SUBMISSION_ID" \
+ --key "$NOTARY_API_KEY_PATH" \
+ --key-id "$APPLE_API_KEY_ID" \
+ --issuer "$APPLE_API_ISSUER_ID"
+ fi
+ exit 1
+ fi
- name: Staple notarization ticket
run: |
@@ -159,19 +206,36 @@ jobs:
xcrun stapler validate "$DMG_PATH"
- name: Verify signed artifact
+ env:
+ RELEASE_TAG: ${{ github.ref_name }}
run: |
- APP_PATH="$(find build/ComputerSolitaire.xcarchive/Products/Applications -maxdepth 1 -name '*.app' -print -quit)"
+ APP_PATH="$(find build/export -maxdepth 1 -name '*.app' -print -quit)"
if [[ -z "${APP_PATH}" ]]; then
- echo "No .app bundle found in archive output."
+ echo "No .app bundle found in export output."
exit 1
fi
- DMG_PATH="ComputerSolitaire-${{ github.ref_name }}-arm64.dmg"
+ DMG_PATH="ComputerSolitaire-$RELEASE_TAG-arm64.dmg"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
spctl --assess --type execute --verbose=4 "$APP_PATH"
codesign --verify --verbose=2 "$DMG_PATH"
xcrun stapler validate "$DMG_PATH"
+ # Sparkle's nested helpers must carry the Developer ID signature,
+ # not the ad-hoc one they ship with.
+ SPARKLE_BIN="$APP_PATH/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
+ if codesign -dvv "$SPARKLE_BIN" 2>&1 | grep -q "Signature=adhoc"; then
+ echo "Sparkle helper is still ad-hoc signed; export did not re-sign nested code."
+ exit 1
+ fi
+
+ # The Sparkle feed keys on the marketing version.
+ BUNDLE_VERSION="$(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' "$APP_PATH/Contents/Info.plist")"
+ if [[ "v$BUNDLE_VERSION" != "$RELEASE_TAG" ]]; then
+ echo "CFBundleVersion ($BUNDLE_VERSION) does not match the release tag ($RELEASE_TAG)."
+ exit 1
+ fi
+
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
diff --git a/ComputerSolitaire.xcodeproj/project.pbxproj b/ComputerSolitaire.xcodeproj/project.pbxproj
index a633f3f..ccff99f 100644
--- a/ComputerSolitaire.xcodeproj/project.pbxproj
+++ b/ComputerSolitaire.xcodeproj/project.pbxproj
@@ -29,9 +29,9 @@
/* 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; };
+ 4B75C2212F1A250000761A12 /* Computer Solitaire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Computer Solitaire.app"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -96,14 +96,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
- 4B75C2232F1A250000761A12 /* Frameworks */ = {
+ 4B75C1F32F1A233600761A12 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 4B75C1F32F1A233600761A12 /* Frameworks */ = {
+ 4B75C2232F1A250000761A12 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -162,29 +162,6 @@
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" */;
@@ -232,6 +209,29 @@
productReference = 4B75C1F12F1A233600761A12 /* ComputerSolitaireUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
+ 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";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -245,9 +245,6 @@
4B75C1D02F1A233400761A12 = {
CreatedOnToolsVersion = 26.2;
};
- 4B75C2202F1A250000761A12 = {
- CreatedOnToolsVersion = 26.2;
- };
4B75C1E92F1A233500761A12 = {
CreatedOnToolsVersion = 26.2;
TestTargetID = 4B75C1D02F1A233400761A12;
@@ -256,6 +253,9 @@
CreatedOnToolsVersion = 26.2;
TestTargetID = 4B75C1D02F1A233400761A12;
};
+ 4B75C2202F1A250000761A12 = {
+ CreatedOnToolsVersion = 26.2;
+ };
};
};
buildConfigurationList = 4B75C1CC2F1A233400761A12 /* Build configuration list for PBXProject "ComputerSolitaire" */;
@@ -291,21 +291,21 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
- 4B75C2242F1A250000761A12 /* Resources */ = {
+ 4B75C1E82F1A233500761A12 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 4B75C1E82F1A233500761A12 /* Resources */ = {
+ 4B75C1F52F1A233600761A12 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 4B75C1F52F1A233600761A12 /* Resources */ = {
+ 4B75C2242F1A250000761A12 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -322,21 +322,21 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
- 4B75C2222F1A250000761A12 /* Sources */ = {
+ 4B75C1E72F1A233500761A12 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 4B75C1E72F1A233500761A12 /* Sources */ = {
+ 4B75C1F42F1A233600761A12 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
- 4B75C1F42F1A233600761A12 /* Sources */ = {
+ 4B75C2222F1A250000761A12 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -495,7 +495,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 0.8.3;
+ CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = T778QMSML9;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -559,7 +559,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 0.8.3;
+ CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = T778QMSML9;
ENABLE_NS_ASSERTIONS = NO;
@@ -581,74 +581,6 @@
};
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 = {
@@ -723,6 +655,74 @@
};
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;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -744,15 +744,6 @@
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 = (
@@ -771,6 +762,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ 4B75C2252F1A250000761A12 /* Build configuration list for PBXNativeTarget "ComputerSolitaireAppStore" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 4B75C2262F1A250000761A12 /* Debug */,
+ 4B75C2272F1A250000761A12 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
diff --git a/ComputerSolitaireTests/Shared/ProjectVersionConsistencyTests.swift b/ComputerSolitaireTests/Shared/ProjectVersionConsistencyTests.swift
new file mode 100644
index 0000000..f8da7f8
--- /dev/null
+++ b/ComputerSolitaireTests/Shared/ProjectVersionConsistencyTests.swift
@@ -0,0 +1,47 @@
+import XCTest
+
+/// Guards the version settings in project.pbxproj. The two app targets each
+/// expose their own Version and Build fields in Xcode, so a pane edit can
+/// silently fork the values apart; these tests fail the suite the moment the
+/// project file disagrees with itself.
+final class ProjectVersionConsistencyTests: XCTestCase {
+ func testMarketingVersionIsIdenticalEverywhere() throws {
+ let values = try values(forSetting: "MARKETING_VERSION")
+ XCTAssertFalse(values.isEmpty, "project.pbxproj defines no MARKETING_VERSION")
+ XCTAssertEqual(
+ values.count, 1,
+ "MARKETING_VERSION has diverged across targets: \(values.sorted())"
+ )
+ }
+
+ func testBuildNumberIsIdenticalEverywhere() throws {
+ let values = try values(forSetting: "CURRENT_PROJECT_VERSION")
+ XCTAssertFalse(values.isEmpty, "project.pbxproj defines no CURRENT_PROJECT_VERSION")
+ XCTAssertEqual(
+ values.count, 1,
+ "CURRENT_PROJECT_VERSION has diverged across targets: \(values.sorted())"
+ )
+ }
+
+ /// Values of every unconditional `NAME = value;` line. Conditional
+ /// variants such as `"NAME[sdk=macosx*]"` are quoted, so they never
+ /// start a trimmed line with the bare setting name.
+ private func values(forSetting name: String) throws -> Set {
+ var values: Set = []
+ for line in try projectFileContents().split(separator: "\n") {
+ let trimmed = line.trimmingCharacters(in: .whitespaces)
+ guard trimmed.hasPrefix("\(name) = "), trimmed.hasSuffix(";") else { continue }
+ values.insert(String(trimmed.dropFirst("\(name) = ".count).dropLast()))
+ }
+ return values
+ }
+
+ private func projectFileContents() throws -> String {
+ let projectFile = URL(fileURLWithPath: #filePath)
+ .deletingLastPathComponent() // Shared
+ .deletingLastPathComponent() // ComputerSolitaireTests
+ .deletingLastPathComponent() // repository root
+ .appendingPathComponent("ComputerSolitaire.xcodeproj/project.pbxproj")
+ return try String(contentsOf: projectFile, encoding: .utf8)
+ }
+}
diff --git a/scripts/tag-version.sh b/scripts/tag-version.sh
index eb10e82..45acc88 100755
--- a/scripts/tag-version.sh
+++ b/scripts/tag-version.sh
@@ -97,13 +97,6 @@ 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