diff --git a/.gitignore b/.gitignore
index e27d315..18d371b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,8 @@ DerivedData/
*.ipa
*.dSYM.zip
*.dSYM
+Archive/*
+Product/*
## Playgrounds
timeline.xctimeline
diff --git a/README.md b/README.md
index b38ea7b..581e8da 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,42 @@ Notable design decisions are recorded in [DECISIONS.md](./DECISIONS.md). The App
[`xcode-install`](https://github.com/xcpretty/xcode-install) and [fastlane/spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) both deserve credit for figuring out the hard parts of what makes this possible.
+## Releasing a new version
+
+Follow the steps below to build and release a new version of Xcodes.app. For any of the git steps, you can use your preferred tool, but please sign the tag.
+
+```sh
+# Update the version number in Xcode and commit the change, if necessary
+
+# Increment the build number
+scripts/increment_build_number.sh
+
+# Commit the change
+git add Xcodes/Resources/Info.plist
+git commit -asm "Increment build number"
+
+# Tag the latest commit
+# Replace $VERSION and $BUILD below with the latest real values
+git tag -asm "v$VERSION.b$BUILD" "v$VERSION.b$BUILD"
+
+# Push to origin
+git push --follow-tags
+
+# Build the app
+scripts/package_release.sh
+
+# Notarize the app
+scripts/notarize.sh "test@example.com" "@keychain:altool" MyOrg Product/Xcodes.zip
+
+# Go to https://github.com/RobotsAndPencils/XcodesApp/releases
+# Edit the latest draft release
+# Set its tag to the tag you just pushed
+# Set its title to a string with the format "$VERSION ($BUILD)"
+# Polish the draft release notes, if necessary
+# Attach the zip that was created in the Product directory to the release
+# Publish the release
+```
+
## Contact
diff --git a/Scripts/export_options.plist b/Scripts/export_options.plist
new file mode 100644
index 0000000..055f67c
--- /dev/null
+++ b/Scripts/export_options.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ method
+ developer-id
+
+
diff --git a/Scripts/increment_build_number.sh b/Scripts/increment_build_number.sh
new file mode 100755
index 0000000..f8ac44e
--- /dev/null
+++ b/Scripts/increment_build_number.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+#
+# Increment build number
+#
+# This will get the latest build number from git tags, add 1, then set it in the Info.plist.
+# Assumes that build numbers are monotonically increasing positive integers, across version numbers.
+# Tags must be named v$version_numberb$build_number, e.g. v1.2.3b456
+
+infoplist_file="$(pwd)/Xcodes/Resources/Info.plist"
+
+# Get latest tag hash matching pattern
+hash=$(git rev-list --tags="v" --max-count=1)
+# Get latest tag at hash that matches the same pattern as a prefix in order to support commits with multiple tags
+last_tag=$(git describe --tags --match "v*" "$hash")
+# Get build number from last component of tag name
+last_build_number=$(echo "$last_tag" | grep -o "b.*" | cut -c 2-)
+
+build_number=$(($last_build_number + 1))
+
+/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $build_number" "${infoplist_file}"
diff --git a/Scripts/notarize.sh b/Scripts/notarize.sh
new file mode 100755
index 0000000..89de6d4
--- /dev/null
+++ b/Scripts/notarize.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+#
+# Notarize
+#
+# Uploads to Apple's notarization service, polls until it completes, staples the ticket to the built app, then creates a new zip.
+#
+# Requires four arguments:
+# - Apple ID username
+# - Apple ID app-specific password (store this in your Keychain and use the @keychain:$NAME syntax to prevent your password from being added to your shell history)
+# - App Store Connect provider name
+# - Path to .app to upload
+#
+# Assumes that there's a .app beside the .zip with the same name so it can be stapled and re-zipped.
+#
+# E.g. notarize.sh "test@example.com" "@keychain:altool" MyOrg Xcodes.zip
+#
+# https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow
+# Adapted from https://github.com/keybase/client/blob/46f5df0aa64ff19198ba7b044bbb7cd907c0be9f/packaging/desktop/package_darwin.sh
+
+username="$1"
+password="$2"
+asc_provider="$3"
+file="$4"
+
+echo "Uploading to notarization service"
+
+uuid=$(xcrun altool \
+ --notarize-app \
+ --primary-bundle-id "com.robotsandpencils.XcodesApp.zip" \
+ --username "$username" \
+ --password "$password" \
+ --asc-provider "$asc_provider" \
+ --file "$file" 2>&1 | \
+ grep 'RequestUUID' | \
+ awk '{ print $3 }')
+
+echo "Successfully uploaded to notarization service, polling for result: $uuid"
+
+sleep 15
+ while :
+ do
+ fullstatus=$(xcrun altool \
+ --notarization-info "$uuid" \
+ --username "$username" \
+ --password "$password" \
+ --asc-provider "$asc_provider" 2>&1)
+ status=$(echo "$fullstatus" | grep 'Status\:' | awk '{ print $2 }')
+ if [ "$status" = "success" ]; then
+ echo "Notarization success"
+ exit 0
+ elif [ "$status" = "in" ]; then
+ echo "Notarization still in progress, sleeping for 15 seconds and trying again"
+ sleep 15
+ else
+ echo "Notarization failed, full status below"
+ echo "$fullstatus"
+ exit 1
+ fi
+ done
+
+# Remove .zip
+rm $file
+
+# Staple ticket to .app
+app_path="$(basename -s ".zip" "$file").app"
+xcrun stapler staple "$app_path"
+
+# Zip the stapled app for distribution
+zip -r "$file" "$app_path"
diff --git a/Scripts/package_release.sh b/Scripts/package_release.sh
new file mode 100755
index 0000000..d4fbb7d
--- /dev/null
+++ b/Scripts/package_release.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+#
+# Package release
+#
+# This will build and archive the app and then compress it in a .zip file at Product/Xcodes.zip
+# You must already have all required code signing assets installed on your computer
+
+PROJECT_NAME=Xcodes
+PROJECT_DIR=$(pwd)/$PROJECT_NAME/Resources
+SCRIPTS_DIR=$(pwd)/Scripts
+INFOPLIST_FILE="Info.plist"
+
+# Ensure a clean build
+rm -rf Archive/*
+rm -rf Product/*
+xcodebuild clean -project $PROJECT_NAME.xcodeproj -configuration Release -alltargets
+
+# Archive the app and export for release distribution
+xcodebuild archive -project $PROJECT_NAME.xcodeproj -scheme $PROJECT_NAME -archivePath Archive/$PROJECT_NAME.xcarchive
+xcodebuild -archivePath Archive/$PROJECT_NAME.xcarchive -exportArchive -exportPath Product/$PROJECT_NAME -exportOptionsPlist "${SCRIPTS_DIR}/export_options.plist"
+cp -r "Product/$PROJECT_NAME/$PROJECT_NAME.app" "Product/$PROJECT_NAME.app"
+
+# Create a ZIP archive suitable for altool.
+/usr/bin/ditto -c -k --keepParent "Product/$PROJECT_NAME.app" "Product/$PROJECT_NAME.zip"
diff --git a/Xcodes/Resources/Info.plist b/Xcodes/Resources/Info.plist
index 0d3bfc9..0a907aa 100644
--- a/Xcodes/Resources/Info.plist
+++ b/Xcodes/Resources/Info.plist
@@ -20,6 +20,8 @@
1.0.0
CFBundleVersion
1
+ CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT
+ $(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
@@ -33,9 +35,7 @@
SMPrivilegedExecutables
com.robotsandpencils.XcodesApp.Helper
- identifier "com.robotsandpencils.XcodesApp.Helper" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)"
+ identifier "com.robotsandpencils.XcodesApp.Helper" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)"
- CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT
- $(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)