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