diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..822ec88 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,50 @@ +name: build + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + schedule: + - cron: '0 9 * * *' + workflow_dispatch: + +jobs: + + macos-build: + + runs-on: inseven-macos-14 + + steps: + + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Install dependencies + run: scripts/install-dependencies.sh + + - name: Build and test + env: + APPLE_DISTRIBUTION_CERTIFICATE_BASE64: ${{ secrets.PERSONAL_APPLE_DISTRIBUTION_CERTIFICATE_BASE64 }} + APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD: ${{ secrets.PERSONAL_APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD }} + MACOS_DEVELOPER_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.PERSONAL_MACOS_DEVELOPER_INSTALLER_CERTIFICATE_BASE64 }} + MACOS_DEVELOPER_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.PERSONAL_MACOS_DEVELOPER_INSTALLER_CERTIFICATE_PASSWORD }} + + APPLE_API_KEY_BASE64: ${{ secrets.PERSONAL_APPLE_API_KEY_BASE64 }} + APPLE_API_KEY_ISSUER_ID: ${{ secrets.PERSONAL_APPLE_API_KEY_ISSUER_ID }} + APPLE_API_KEY_ID: ${{ secrets.PERSONAL_APPLE_API_KEY_ID }} + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE: ${{ github.ref == 'refs/heads/main' }} + + run: | + scripts/build.sh + + - name: Archive the binary + uses: actions/upload-artifact@v4 + with: + path: build/build-*.zip + if-no-files-found: error diff --git a/.gitmodules b/.gitmodules index e6acfdd..e4a644c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,9 @@ [submodule "interact"] path = interact url = git@github.com:inseven/interact.git +[submodule "scripts/changes"] + path = scripts/changes + url = https://github.com/jbmorley/changes.git +[submodule "scripts/build-tools"] + path = scripts/build-tools + url = https://github.com/jbmorley/build-tools.git diff --git a/ExportOptions.plist b/ExportOptions.plist new file mode 100644 index 0000000..e724516 --- /dev/null +++ b/ExportOptions.plist @@ -0,0 +1,27 @@ + + + + + destination + export + installerSigningCertificate + C44BC9291462AE6729496E2CAC18CE475688F8CD + manageAppVersionAndBuildNumber + + method + app-store + provisioningProfiles + + uk.co.jbmorley.thoughts.apps.appstore + Thoughts Mac App Store Profile + + signingCertificate + 91DADFE184A1526FA94D4513D5B4C75E1DB3B252 + signingStyle + manual + teamID + QS82QFHKWB + uploadSymbols + + + diff --git a/Thoughts.xcodeproj/project.pbxproj b/Thoughts.xcodeproj/project.pbxproj index 077b429..1b7a1f2 100644 --- a/Thoughts.xcodeproj/project.pbxproj +++ b/Thoughts.xcodeproj/project.pbxproj @@ -512,6 +512,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Thoughts/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( @@ -532,15 +533,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Thoughts/Thoughts.entitlements; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Thoughts/Preview Content\""; - DEVELOPMENT_TEAM = QS82QFHKWB; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = QS82QFHKWB; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Thoughts/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( @@ -550,6 +554,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = uk.co.jbmorley.thoughts.apps.appstore; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Thoughts Mac App Store Profile"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; diff --git a/Thoughts/Info.plist b/Thoughts/Info.plist index 57dbbb8..a80edd5 100644 --- a/Thoughts/Info.plist +++ b/Thoughts/Info.plist @@ -15,5 +15,7 @@ + ITSAppUsesNonExemptEncryption + diff --git a/Thoughts_Mac_App_Store_Profile.provisionprofile b/Thoughts_Mac_App_Store_Profile.provisionprofile new file mode 100644 index 0000000..ac1cbba Binary files /dev/null and b/Thoughts_Mac_App_Store_Profile.provisionprofile differ diff --git a/scripts/build-tools b/scripts/build-tools new file mode 160000 index 0000000..c0f02d8 --- /dev/null +++ b/scripts/build-tools @@ -0,0 +1 @@ +Subproject commit c0f02d8da114357ce680a632355f295327565db5 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..22018e1 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,173 @@ +#!/bin/bash + +# Copyright (c) 2024 Jason Morley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +set -e +set -o pipefail +set -x +set -u + +SCRIPTS_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +ROOT_DIRECTORY="${SCRIPTS_DIRECTORY}/.." +BUILD_DIRECTORY="${ROOT_DIRECTORY}/build" +TEMPORARY_DIRECTORY="${ROOT_DIRECTORY}/temp" + +KEYCHAIN_PATH="${TEMPORARY_DIRECTORY}/temporary.keychain" +ARCHIVE_PATH="${BUILD_DIRECTORY}/Thoughts.xcarchive" +ENV_PATH="${ROOT_DIRECTORY}/.env" + +RELEASE_SCRIPT_PATH="${SCRIPTS_DIRECTORY}/release.sh" + +IOS_XCODE_PATH=${IOS_XCODE_PATH:-/Applications/Xcode.app} +MACOS_XCODE_PATH=${MACOS_XCODE_PATH:-/Applications/Xcode.app} + +source "${SCRIPTS_DIRECTORY}/environment.sh" + +# Check that the GitHub command is available on the path. +which gh || (echo "GitHub cli (gh) not available on the path." && exit 1) + +# Process the command line arguments. +POSITIONAL=() +RELEASE=${RELEASE:-false} +while [[ $# -gt 0 ]] +do + key="$1" + case $key in + -r|--release) + RELEASE=true + shift + ;; + *) + POSITIONAL+=("$1") + shift + ;; + esac +done + +# Generate a random string to secure the local keychain. +export TEMPORARY_KEYCHAIN_PASSWORD=`openssl rand -base64 14` + +# Source the .env file if it exists to make local development easier. +if [ -f "$ENV_PATH" ] ; then + echo "Sourcing .env..." + source "$ENV_PATH" +fi + +function xcode_project { + xcodebuild \ + -project Thoughts.xcodeproj "$@" +} + +function build_scheme { + # Disable code signing for the build server. + xcode_project \ + -scheme "$1" \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO "${@:2}" +} + +cd "$ROOT_DIRECTORY" + +# Select the correct Xcode. +sudo xcode-select --switch "$MACOS_XCODE_PATH" + +# List the available schemes. +xcode_project -list + +# Clean up the build directory. +if [ -d "$BUILD_DIRECTORY" ] ; then + rm -r "$BUILD_DIRECTORY" +fi +mkdir -p "$BUILD_DIRECTORY" + +# Create the a new keychain. +if [ -d "$TEMPORARY_DIRECTORY" ] ; then + rm -rf "$TEMPORARY_DIRECTORY" +fi +mkdir -p "$TEMPORARY_DIRECTORY" +echo "$TEMPORARY_KEYCHAIN_PASSWORD" | build-tools create-keychain "$KEYCHAIN_PATH" --password + +function cleanup { + + # Cleanup the temporary files, keychain and keys. + cd "$ROOT_DIRECTORY" + build-tools delete-keychain "$KEYCHAIN_PATH" + rm -rf "$TEMPORARY_DIRECTORY" + rm -rf ~/.appstoreconnect/private_keys +} + +trap cleanup EXIT + +# Determine the version and build number. +VERSION_NUMBER=`changes version` +BUILD_NUMBER=`build-number.swift` + +# Import the certificates into our dedicated keychain. +echo "$APPLE_DISTRIBUTION_CERTIFICATE_PASSWORD" | build-tools import-base64-certificate --password "$KEYCHAIN_PATH" "$APPLE_DISTRIBUTION_CERTIFICATE_BASE64" +echo "$MACOS_DEVELOPER_INSTALLER_CERTIFICATE_PASSWORD" | build-tools import-base64-certificate --password "$KEYCHAIN_PATH" "$MACOS_DEVELOPER_INSTALLER_CERTIFICATE_BASE64" + +# Install the provisioning profiles. +build-tools install-provisioning-profile "Thoughts_Mac_App_Store_Profile.provisionprofile" + +# Build and archive the macOS project. +sudo xcode-select --switch "$MACOS_XCODE_PATH" +xcode_project \ + -scheme "Thoughts" \ + -config Release \ + -archivePath "$ARCHIVE_PATH" \ + OTHER_CODE_SIGN_FLAGS="--keychain=\"${KEYCHAIN_PATH}\"" \ + CURRENT_PROJECT_VERSION=$BUILD_NUMBER \ + MARKETING_VERSION=$VERSION_NUMBER \ + clean archive +xcodebuild \ + -archivePath "$ARCHIVE_PATH" \ + -exportArchive \ + -exportPath "$BUILD_DIRECTORY" \ + -exportOptionsPlist "ExportOptions.plist" + +APP_BASENAME="Thoughts.app" +APP_PATH="$BUILD_DIRECTORY/$APP_BASENAME" +PKG_PATH="$BUILD_DIRECTORY/Thoughts.pkg" + +# Install the private key. +mkdir -p ~/.appstoreconnect/private_keys/ +echo -n "$APPLE_API_KEY_BASE64" | base64 --decode -o ~/".appstoreconnect/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8" + +# Archive the build directory. +ZIP_BASENAME="build-${VERSION_NUMBER}-${BUILD_NUMBER}.zip" +ZIP_PATH="${BUILD_DIRECTORY}/${ZIP_BASENAME}" +pushd "${BUILD_DIRECTORY}" +zip -r "${ZIP_BASENAME}" . +popd + +if $RELEASE ; then + + changes \ + release \ + --skip-if-empty \ + --pre-release \ + --push \ + --exec "${RELEASE_SCRIPT_PATH}" \ + "${PKG_PATH}" "${ZIP_PATH}" + +fi diff --git a/scripts/changes b/scripts/changes new file mode 160000 index 0000000..f68484b --- /dev/null +++ b/scripts/changes @@ -0,0 +1 @@ +Subproject commit f68484bb984f324acca17e69e15f0bed94ee6cbd diff --git a/scripts/environment.sh b/scripts/environment.sh new file mode 100755 index 0000000..51caabe --- /dev/null +++ b/scripts/environment.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Copyright (c) 2024 Jason Morley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +SCRIPTS_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +ROOT_DIRECTORY="${SCRIPTS_DIRECTORY}/.." + +export PYTHONUSERBASE="${ROOT_DIRECTORY}/.local/python" +mkdir -p "$PYTHONUSERBASE" +export PATH="${PYTHONUSERBASE}/bin":$PATH + +export PATH=$PATH:"${SCRIPTS_DIRECTORY}/changes" +export PATH=$PATH:"${SCRIPTS_DIRECTORY}/build-tools" +export PATH=$PATH:"${ROOT_DIRECTORY}/diligence/scripts" diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh new file mode 100755 index 0000000..755e2a6 --- /dev/null +++ b/scripts/install-dependencies.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Copyright (c) 2024 Jason Morley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +set -e +set -o pipefail +set -x +set -u + +SCRIPTS_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +ROOT_DIRECTORY="${SCRIPTS_DIRECTORY}/.." +CHANGES_DIRECTORY="${SCRIPTS_DIRECTORY}/changes" +BUILD_TOOLS_DIRECTORY="${SCRIPTS_DIRECTORY}/build-tools" + +ENVIRONMENT_PATH="${SCRIPTS_DIRECTORY}/environment.sh" + +if [ -d "${ROOT_DIRECTORY}/.local" ] ; then + rm -r "${ROOT_DIRECTORY}/.local" +fi +source "${ENVIRONMENT_PATH}" + +# Install the Python dependencies +pip3 install --user pipenv +PIPENV_PIPFILE="$CHANGES_DIRECTORY/Pipfile" pipenv install +PIPENV_PIPFILE="$BUILD_TOOLS_DIRECTORY/Pipfile" pipenv install diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..712f371 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Copyright (c) 2016-2024 Jason Morley +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +set -e +set -o pipefail +set -x + +# This script expects the macOS PKG as the first argument, and any additional files to be attached to the GitHub release +# to be passed as subsequent arguments. + +# Upload the macOS build. +xcrun altool --upload-app \ + -f "$1" \ + --primary-bundle-id "uk.co.jbmorley.thoughts.apps.appstore" \ + --apiKey "$APPLE_API_KEY_ID" \ + --apiIssuer "$APPLE_API_KEY_ISSUER_ID" \ + --type macos + +# Actually make the release. +FLAGS=() +if $CHANGES_INITIAL_DEVELOPMENT ; then + FLAGS+=("--prerelease") +elif $CHANGES_PRE_RELEASE ; then + FLAGS+=("--prerelease") +fi +gh release create "$CHANGES_TAG" --title "$CHANGES_QUALIFIED_TITLE" --notes-file "$CHANGES_NOTES_FILE" "${FLAGS[@]}" + +# Upload the attachments. +for attachment in "$@" +do + gh release upload "$CHANGES_TAG" "$attachment" +done