From fea20a4082cc8acdd77dcfb9de1bd17641c07aee Mon Sep 17 00:00:00 2001
From: Cam Saul <github@camsaul.com>
Date: Mon, 13 Jan 2020 17:06:34 -0800
Subject: [PATCH] Metabase Mac App build script / instructions updates [ci
 skip]

---
 OSX/Metabase/Metabase-Info.plist |   2 +-
 bin/Metabase/Util.pm             |  11 ++-
 bin/config.json                  |   8 +-
 bin/osx-release                  | 109 ++++++++++++++++++++++++--
 docs/developers-guide-osx.md     | 129 +++++++++++++++++++++----------
 5 files changed, 208 insertions(+), 51 deletions(-)

diff --git a/OSX/Metabase/Metabase-Info.plist b/OSX/Metabase/Metabase-Info.plist
index 6448265fff3..fdbbbe212d4 100644
--- a/OSX/Metabase/Metabase-Info.plist
+++ b/OSX/Metabase/Metabase-Info.plist
@@ -40,7 +40,7 @@
 	<key>SUEnableAutomaticChecks</key>
 	<true/>
 	<key>SUFeedURL</key>
-	<string>https://s3.amazonaws.com/downloads.metabase.com/appcast.xml</string>
+	<string>https://downloads.metabase.com/appcast.xml</string>
 	<key>SUPublicDSAKeyFile</key>
 	<string>dsa_pub.pem</string>
 </dict>
diff --git a/bin/Metabase/Util.pm b/bin/Metabase/Util.pm
index b7e07bbce87..72c0e10dc95 100644
--- a/bin/Metabase/Util.pm
+++ b/bin/Metabase/Util.pm
@@ -6,10 +6,12 @@ package Metabase::Util;
 use Cwd 'getcwd';
 use Exporter;
 use JSON;
+use Readonly;
 use Term::ANSIColor qw(:constants);
 
 our @ISA = qw(Exporter);
 our @EXPORT = qw(config
+                 config_or_die
                  announce
                  print_giant_success_banner
                  get_file_or_die
@@ -17,16 +19,21 @@ our @EXPORT = qw(config
                  OSX_ARTIFACTS_DIR
                  artifact);
 
-my $config_file = getcwd() . '/bin/config.json';
+Readonly my $config_file => getcwd() . '/bin/config.json';
 warn "Missing config file: $config_file\n" .
      "Please copy $config_file.template, and edit it as needed.\n"
      unless (-e $config_file);
-my $config = from_json(`cat $config_file`) if -e $config_file;
+Readonly my $config => from_json(`cat $config_file`) if -e $config_file;
 
 sub config {
     return $config ? $config->{ $_[0] } : '';
 }
 
+sub config_or_die {
+    my ($configKey) = @_;
+    return config($configKey) or die "Missing config.json property '$configKey'";
+}
+
 sub announce {
     print "\n\n" . GREEN . $_[0] . RESET . "\n\n";
 }
diff --git a/bin/config.json b/bin/config.json
index e6cd9b2b050..bb2a29d80f4 100644
--- a/bin/config.json
+++ b/bin/config.json
@@ -1,5 +1,7 @@
 {
-    "codesigningIdentity": "Developer ID Application: Metabase, Inc",
-    "awsProfile": "metabase",
-    "awsBucket": "downloads.metabase.com"
+  "codesigningIdentity": "Developer ID Application: Metabase, Inc",
+  "appStoreConnectProviderShortName": "BR27ZJK7WW",
+  "awsProfile": "metabase",
+  "awsBucket": "downloads.metabase.com",
+  "cloudFrontDistributionID": "E35CJLWZIZVG7K"
 }
diff --git a/bin/osx-release b/bin/osx-release
index 7f20030eaf6..eac9c57b94a 100755
--- a/bin/osx-release
+++ b/bin/osx-release
@@ -95,19 +95,21 @@ sub build {
 
 # Codesign Metabase.app
 sub codesign {
-    my $codesigning_cert_name = config('codesigningIdentity') or return;
+    Readonly my $codesigning_cert_name => config_or_die('codesigningIdentity');
 
     announce "Codesigning $app...";
 
     system('codesign', '--force', '--verify',
            '--sign', $codesigning_cert_name,
            '-r=designated => anchor trusted',
+           '--timestamp',
+           '--options', 'runtime',
            '--deep', get_file_or_die($app)) == 0 or die "Code signing failed: $!\n";
 }
 
 # Verify that Metabase.app was signed correctly
 sub verify_codesign {
-    return unless config('codesigningIdentity');
+    config_or_die('codesigningIdentity');
 
     announce "Verifying codesigning for $app...";
 
@@ -154,8 +156,8 @@ sub generate_appcast {
 
     remove_tree($appcast);
 
-    my $aws_bucket  = config('awsBucket')  or return;
-    my $signature   = generate_signature() or return;
+    Readonly my $aws_bucket  => config_or_die('awsBucket');
+    Readonly my $signature   => generate_signature() or die 'Failed to generate appcast signature';
 
     open(my $out, '>', $appcast) or die "Unable to write to $appcast: $!";
     print $out Text::Caml->new->render_file(get_file_or_die('bin/templates/appcast.xml.template'), {
@@ -296,6 +298,85 @@ sub create_dmg {
     remove_tree($temp_dmg, $dmg_source_dir);
 }
 
+# ------------------------------------------------------------ NOTORIZATION ------------------------------------------------------------
+
+sub getAppleID {
+    return $ENV{'METABASE_MAC_APP_BUILD_APPLE_ID'} or die 'Make sure you export the env var METABASE_MAC_APP_BUILD_APPLE_ID';
+}
+
+sub getAscProvider {
+    return config_or_die('appStoreConnectProviderShortName');
+}
+
+sub notarize_file {
+    my ($filename) = @_;
+
+    announce "Notarizing $filename...";
+
+    Readonly my $appleID     => getAppleID;
+    Readonly my $ascProvider => getAscProvider;
+
+    system('xcrun', 'altool', '--notarize-app',
+           '--primary-bundle-id', 'com.metabase.Metabase',
+           '--username', $appleID,
+           '--password', '@keychain:METABASE_MAC_APP_BUILD_PASSWORD',
+           '--asc-provider', $ascProvider,
+           '--file', $filename
+          ) == 0 or die $!;
+}
+
+sub wait_for_notarization {
+    announce "Waiting for notarization...";
+
+    Readonly my $appleID     => getAppleID;
+    Readonly my $ascProvider => getAscProvider;
+
+    my $status = `xcrun altool --notarization-history 0 -u "$appleID" -p "\@keychain:METABASE_MAC_APP_BUILD_PASSWORD" --asc-provider $ascProvider` or die $!;
+
+    print "$status\n";
+
+    if ($status =~ m/in progress/) {
+        print "Notarization is still in progress, waiting a few seconds and trying again...\n";
+        sleep 5;
+        wait_for_notarization();
+    } else {
+        announce "Notarization successful.";
+        return "Done";
+    }
+}
+
+sub staple_notorization {
+    my ($filename) = @_;
+
+    announce "Stapling notarization to $filename...";
+
+    system('xcrun', 'stapler', 'staple',
+           '-v', $filename) == 0 or die $1;
+
+    announce "Notarization stapled successfully.";
+}
+
+# Verify that an app is Signed & Notarized correctly. See https://help.apple.com/xcode/mac/current/#/dev1cc22a95c
+sub verify_notarization {
+    # e.g. /Applications/Metabase.app
+    my ($appFile) = @_;
+
+    announce "Verifying that $appFile is notarized correctly...";
+
+    system('spctl', '-a', '-v', $appFile) == 0 or die $!;
+
+    announce "Verification successful.";
+}
+
+
+sub notarize_files {
+    notarize_file(get_file_or_die($zipfile));
+    notarize_file(get_file_or_die($dmg));
+    wait_for_notarization();
+    staple_notorization(get_file_or_die($dmg));
+    verify_notarization(get_file_or_die($app));
+}
+
 
 # ------------------------------------------------------------ UPLOADING ------------------------------------------------------------
 
@@ -303,8 +384,8 @@ sub create_dmg {
 # Upload artifacts to AWS
 # Make sure to run `aws configure --profile metabase` first to set up your ~/.aws/config file correctly
 sub upload {
-    my $aws_profile = config('awsProfile') or return;
-    my $aws_bucket  = config('awsBucket')  or return;
+    Readonly my $aws_profile => config_or_die('awsProfile');
+    Readonly my $aws_bucket  => config_or_die('awsBucket');
 
     # Make a folder that contains the files we want to upload
     Readonly my $upload_dir => artifact('upload');
@@ -332,6 +413,20 @@ sub upload {
     announce "Upload finished."
 }
 
+sub create_cloudfront_invalidation {
+    announce "Creating CloudFront invalidation...";
+
+    system ('aws', 'configure',
+            'set', 'preview.cloudfront', 'true') == 0 or die $!;
+
+    system ('aws', 'cloudfront', 'create-invalidation',
+            '--profile', config_or_die('awsProfile'),
+            '--distribution-id', config_or_die('cloudFrontDistributionID'),
+            '--paths', '/appcast.xml') == 0 or die $!;
+
+    announce "CloudFront invalidation created successfully.";
+}
+
 
 # ------------------------------------------------------------ RUN ALL ------------------------------------------------------------
 
@@ -345,7 +440,9 @@ sub all {
     generate_appcast;
     edit_release_notes;
     create_dmg;
+    notarize_files;
     upload;
+    create_cloudfront_invalidation;
 }
 
 # Run all the commands specified in command line args, otherwise default to running 'all'
diff --git a/docs/developers-guide-osx.md b/docs/developers-guide-osx.md
index 946ff3c7c47..0596e9e57cc 100644
--- a/docs/developers-guide-osx.md
+++ b/docs/developers-guide-osx.md
@@ -3,64 +3,115 @@
 NOTE: These instructions are only for packaging a built Metabase uberjar into `Metabase.app`. They are not useful if your goal is to work on Metabase itself; for development, please see
 our [developers' guide](developers-guide.md).
 
-## Prereqs
+## First-Time Configuration
 
-1.  Install XCode.
+### Building
 
-1.  Install XCode command-line tools. In `Xcode` > `Preferences` > `Locations` select your current Xcode version in the `Command Line Tools` drop-down.
+The following steps need to be done before building the Mac App:
+
+1.  Install XCode.
 
-1.  Run `./bin/build` to build the latest version of the uberjar.
+1.  Add a JRE to the `OSX/Metabase/jre`
 
-1.  Next, you'll need to run the following commands before building the app:
+    You can download a copy of a JRE from https://adoptopenjdk.net/releases.html — make sure you download a JRE rather than JDK. Move the `Contents/Home` directory from the JRE archive into `OSX/Metabase/jre`. For example:
 
     ```bash
-      # Fetch and initialize git submodule
-      git submodule update --init
+    wget https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u232-b09/OpenJDK8U-jre_x64_mac_hotspot_8u232b09.tar.gz
+    tar -xzvf OpenJDK8U-jre_x64_mac_hotspot_8u232b09.tar.gz
+    mv jdk8u232-b09-jre/Contents/Home/ OSX/Metabase/jre
+    ```
 
-      # Install Perl modules used by ./bin/osx-setup and ./bin/osx-release
-      sudo cpan install File::Copy::Recursive Readonly String::Util Text::Caml JSON
+    You are fine to use whatever the latest JRE version available is. I have been using the HotSpot JRE instead of the OpenJ9 one but it ultimately shouldn't make a difference.    
+    
+1.  Copy Metabase uberjar to OSX resources dir
 
-      # Copy JRE and uberjar
-      ./bin/osx-setup
+    ```bash
+    cp /path/to/metabase.jar OSX/Resources/metabase.jar
     ```
+    
+    Every time you want to build a new version of the Mac App, you can simple update the bundled uberjar the same way.
+        
+At this point, you should try opening up the Xcode project and building the Mac App in Xcode by clicking the run button. The app should build and launch at this point. If it doesn't, ask Cam for help!
 
-`./bin/osx-setup` will copy over things like the JRE into the Mac App directory for you. You only need to do this once the first time you plan on building the Mac App.
-This also runs `./bin/build` to get the latest uberjar and copies it for you; if the script fails near the end, you can just copy the uberjar to `OSX/Resources/metabase.jar`.)
+### Releasing
 
-## Releasing
+The following steps are prereqs for releasing the Mac App:
 
-A handy Perl script called `./bin/osx-release` takes care of all of the details for you. Before you run it for the first time, you'll need to set up a few additional things:
 
-```bash
-# Install aws command-line client (if needed)
-brew install awscli
+1.  Install XCode command-line tools. In `Xcode` > `Preferences` > `Locations` select your current Xcode version in the `Command Line Tools` drop-down.
 
-# Configure AWS Credentials
-# You'll need credentials that give you permission to write the metabase-osx-releases S3 bucket.
-# You just need the access key ID and secret key; use the defaults for locale and other options.
-aws configure --profile metabase
+1.  Install CPAN modules
 
-# Obtain a copy of the private key used for signing the app (ask Cam)
-# and put a copy of it at ./dsa_priv.pem
-cp /path/to/private/key.pem OSX/dsa_priv.pem
-```
+    ```bash
+    sudo cpan
+    install force File::Copy::Recursive Readonly String::Util Text::Caml JSON
+    quit
+    ```
+
+    You can install [PerlBrew](https://perlbrew.pl/) if you want to install CPAN modules without having to use `sudo`.
 
-You'll need the `Apple Developer ID Application Certificate` in your computer's keychain.
-You'll need to generate a Certificate Signing Request from Keychain Access, and have Sameer go to [the Apple Developer Site](https://developer.apple.com/account/mac/certificate/)
-and generate one for you, then load the file on your computer.
+    Normally you shouldn't have to use `install force` to install the modules above, but `File::Copy::Recursive` seems fussy lately and has a failing test that prevents it from installing normally.
 
-Finally, you may need to open the project a single time in Xcode to make sure the appropriate "build schemes" are generated (these are not checked into CI).
-Run `open OSX/Metabase.xcodeproj` to open the project, which will automatically generate the appropriate schemes. This only needs to be done once.
+1.  Install AWS command-line client (if needed)
 
-After that, you are good to go:
-```bash
-# Build the latest version of the uberjar and copy it to the Mac App build directory
-# (You can skip this step if you just ran ./bin/osx-setup, because it does this step for you)
-./bin/build && cp target/uberjar/metabase.jar OSX/Resources/metabase.jar
+    ```bash
+    brew install awscli
+    ```
+    
+1.  Configure AWS Credentials for `metabase` profile (used to upload artifacts to S3)    
 
-# Bundle entire app, and upload to s3
-./bin/osx-release
-```
+    You'll need credentials that give you permission to write the metabase-osx-releases S3 bucket.
+    You just need the access key ID and secret key; use the defaults for locale and other options.
+    
+    ```bash
+    aws configure --profile metabase
+    ```
+    
+1.  Obtain a copy of the private key for signing app updates (ask Cam) and put a copy of it at `OSX/dsa_priv.pem`
+
+    ```bash
+    cp /path/to/private/key.pem OSX/dsa_priv.pem
+    ```
+    
+1.  Add `Apple Developer ID Application Certificate` to your computer's keychain.
+
+    You'll need to generate a Certificate Signing Request from Keychain Access, and have Sameer go to [the Apple Developer Site](https://developer.apple.com/account/mac/certificate/) and generate one for you, then load the file on your computer.
+    
+1.  Export your Apple ID for building the app as `METABASE_MAC_APP_BUILD_APPLE_ID`. (This Apple ID must be part of the Metabase org in the Apple developer site. Ask Cam or Sameer to add you if it isn't.)
+     
+    ```bash
+    #  Add this to .zshrc or .bashrc
+    export METABASE_MAC_APP_BUILD_APPLE_ID=my_email@whatever.com
+    ```        
+
+1.  Create an App-Specific password for the Apple ID in the previous step
+
+     Go to https://appleid.apple.com/account/manage then `Security` > `App-Specific Passwords` > `Generate Password`
+        
+    1.  Store the password in Keychain
+    
+        ```bash
+        xcrun altool \
+        --store-password-in-keychain-item "METABASE_MAC_APP_BUILD_PASSWORD" \
+        -u "$METABASE_MAC_APP_BUILD_APPLE_ID" \
+        -p <secret_password>
+        ```
+      
+## Building & Releasing the Mac App
+        
+After following the configuration steps above, to build and release the app you can use the `./bin/osx-release` script:
+
+1. Copy latest uberjar to the Mac App build directory
+
+    ```bash
+    cp path/to/metabase.jar OSX/Resources/metabase.jar
+    ```
+    
+1. Bundle entire app, and upload to s3
+
+    ```bash
+    ./bin/osx-release
+    ```
 
 ## Debugging ./bin/osx-release
 
-- 
GitLab