diff --git a/.gon.hcl b/.gon.hcl
index dfc1781..af92701 100644
--- a/.gon.hcl
+++ b/.gon.hcl
@@ -4,6 +4,7 @@ bundle_id = "com.mitchellh.gon"
apple_id {
username = "mitchell.hashimoto@gmail.com"
password = "@env:AC_PASSWORD"
+ provider = "UL304B4VGY"
}
sign {
diff --git a/README.md b/README.md
index b2e2cec..47cab5a 100644
--- a/README.md
+++ b/README.md
@@ -149,6 +149,7 @@ bundle_id = "com.mitchellh.example.terraform"
apple_id {
username = "mitchell@example.com"
password = "@env:AC_PASSWORD"
+ provider = "UL304B4VGY"
}
sign {
diff --git a/cmd/gon/item.go b/cmd/gon/item.go
index fda3da0..9227a13 100644
--- a/cmd/gon/item.go
+++ b/cmd/gon/item.go
@@ -2,13 +2,11 @@ package main
import (
"context"
- "fmt"
"os"
"sync"
"github.com/fatih/color"
"github.com/hashicorp/go-hclog"
- "github.com/hashicorp/go-multierror"
"github.com/mitchellh/gon/internal/config"
"github.com/mitchellh/gon/notarize"
@@ -66,19 +64,18 @@ func (i *item) notarize(ctx context.Context, opts *processOptions) error {
}
// Start notarization
- info, err := notarize.Notarize(ctx, ¬arize.Options{
- File: i.Path,
- BundleId: bundleId,
- Username: opts.Config.AppleId.Username,
- Password: opts.Config.AppleId.Password,
- Provider: opts.Config.AppleId.Provider,
- Logger: opts.Logger.Named("notarize"),
- Status: &statusHuman{Prefix: opts.Prefix, Lock: lock},
- UploadLock: opts.UploadLock,
+ _, _, err := notarize.Notarize(ctx, ¬arize.Options{
+ File: i.Path,
+ DeveloperId: opts.Config.AppleId.Username,
+ Password: opts.Config.AppleId.Password,
+ Provider: opts.Config.AppleId.Provider,
+ Logger: opts.Logger.Named("notarize"),
+ Status: &statusHuman{Prefix: opts.Prefix, Lock: lock},
+ UploadLock: opts.UploadLock,
})
// Save the error state. We don't save the notarization result yet
- // because we don't know it for sure until we download the log file.
+ // because we don't know it for sure until we retrieve the log information.
i.State.NotarizeError = err
// If we had an error, we mention immediate we have an error.
@@ -88,73 +85,6 @@ func (i *item) notarize(ctx context.Context, opts *processOptions) error {
lock.Unlock()
}
- // If we have a log file, download it. We do this whether we have an error
- // or not because the log file can contain more details about the error.
- if info != nil && info.LogFileURL != "" {
- opts.Logger.Info(
- "downloading log file for notarization",
- "request_uuid", info.RequestUUID,
- "url", info.LogFileURL,
- )
-
- log, logerr := notarize.DownloadLog(info.LogFileURL)
- opts.Logger.Debug("log file downloaded", "log", log, "err", logerr)
- if logerr != nil {
- opts.Logger.Warn(
- "error downloading log file, this isn't a fatal error",
- "err", err,
- )
-
- // If we already failed notarization, just return that error
- if err := i.State.NotarizeError; err != nil {
- return err
- }
-
- // If it appears we succeeded notification, we make a new error.
- // We can't say notarization is successful without downloading this
- // file because warnings will cause notarization to not work
- // when loaded.
- lock.Lock()
- color.New(color.FgRed).Fprintf(os.Stdout,
- " %sError downloading log file to verify notarization.\n",
- opts.Prefix,
- )
- lock.Unlock()
-
- return fmt.Errorf(
- "Error downloading log file to verify notarization success: %s\n\n"+
- "You can download the log file manually at: %s",
- logerr, info.LogFileURL,
- )
- }
-
- // If we have any issues then it is a failed notarization. Notarization
- // can "succeed" with warnings, but when you attempt to use/open a file
- // Gatekeeper rejects it. So we currently reject any and all issues.
- if len(log.Issues) > 0 {
- var err error
-
- lock.Lock()
- color.New(color.FgRed).Fprintf(os.Stdout,
- " %s%d issues during notarization:\n",
- opts.Prefix, len(log.Issues))
- for idx, issue := range log.Issues {
- color.New(color.FgRed).Fprintf(os.Stdout,
- " %sIssue #%d (%s) for path %q: %s\n",
- opts.Prefix, idx+1, issue.Severity, issue.Path, issue.Message)
-
- // Append the error so we can return it
- err = multierror.Append(err, fmt.Errorf(
- "%s for path %q: %s",
- issue.Severity, issue.Path, issue.Message,
- ))
- }
- lock.Unlock()
-
- return err
- }
- }
-
// If we aren't notarized, then return
if err := i.State.NotarizeError; err != nil {
return err
diff --git a/cmd/gon/status_human.go b/cmd/gon/status_human.go
index 8b484ee..09d0284 100644
--- a/cmd/gon/status_human.go
+++ b/cmd/gon/status_human.go
@@ -18,7 +18,8 @@ type statusHuman struct {
Prefix string
Lock *sync.Mutex
- lastStatus string
+ lastInfoStatus string
+ lastLogStatus string
}
func (s *statusHuman) Submitting() {
@@ -37,13 +38,23 @@ func (s *statusHuman) Submitted(uuid string) {
os.Stdout, " %sWaiting for results from Apple. This can take minutes to hours.\n", s.Prefix)
}
-func (s *statusHuman) Status(info notarize.Info) {
+func (s *statusHuman) InfoStatus(info notarize.Info) {
s.Lock.Lock()
defer s.Lock.Unlock()
- if info.Status != s.lastStatus {
- s.lastStatus = info.Status
- color.New().Fprintf(os.Stdout, " %sStatus: %s\n", s.Prefix, info.Status)
+ if info.Status != s.lastInfoStatus {
+ s.lastInfoStatus = info.Status
+ color.New().Fprintf(os.Stdout, " %sInfoStatus: %s\n", s.Prefix, info.Status)
+ }
+}
+
+func (s *statusHuman) LogStatus(log notarize.Log) {
+ s.Lock.Lock()
+ defer s.Lock.Unlock()
+
+ if log.Status != s.lastLogStatus {
+ s.lastLogStatus = log.Status
+ color.New().Fprintf(os.Stdout, " %sLogStatus: %s\n", s.Prefix, log.Status)
}
}
diff --git a/go.mod b/go.mod
index ee32cc5..79a5574 100644
--- a/go.mod
+++ b/go.mod
@@ -5,10 +5,8 @@ go 1.13
require (
github.com/davecgh/go-spew v1.1.1
github.com/fatih/color v1.7.0
- github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2
github.com/hashicorp/go-multierror v1.0.0
- github.com/hashicorp/go-retryablehttp v0.6.3
github.com/hashicorp/hcl/v2 v2.0.0
github.com/sebdah/goldie v1.0.0
github.com/stretchr/testify v1.3.0
diff --git a/go.sum b/go.sum
index 48d8031..fe2093c 100644
--- a/go.sum
+++ b/go.sum
@@ -26,18 +26,11 @@ github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd/go.mod h1:qkLSc0A5EX
github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
-github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2 h1:STV8OvzphW1vlhPFxcG8d6OIilzBSKRAoWFJt+Onu10=
github.com/hashicorp/go-hclog v0.9.3-0.20191025211905-234833755cb2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-retryablehttp v0.6.3 h1:tuulM+WnToeqa05z83YLmKabZxrySOmJAd4mJ+s2Nfg=
-github.com/hashicorp/go-retryablehttp v0.6.3/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8=
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
diff --git a/notarize/info.go b/notarize/info.go
index f23e32a..4c3f518 100644
--- a/notarize/info.go
+++ b/notarize/info.go
@@ -7,7 +7,6 @@ import (
"io"
"os/exec"
"path/filepath"
- "time"
"github.com/hashicorp/go-hclog"
"howett.net/plist"
@@ -23,33 +22,19 @@ type Info struct {
// RequestUUID is the UUID provided by Apple after submitting the
// notarization request. This can be used to look up notarization information
// using the Apple tooling.
- RequestUUID string `plist:"RequestUUID"`
+ RequestUUID string `plist:"id"`
// Date is the date and time of submission
- Date time.Time `plist:"Date"`
+ Date string `plist:"createdDate"`
- // Hash is the encoded hash value for the submitted file. This is provided
- // by Apple. This is not decoded into a richer type like hash/sha256 because
- // it doesn't seem to be guaranteed by Apple anywhere what format this is in.
- Hash string `plist:"Hash"`
-
- // LogFileURL is a URL to a log file for more details.
- LogFileURL string `plist:"LogFileURL"`
+ // Name is th file uploaded for submission.
+ Name string `plist:"name"`
// Status the status of the notarization.
- //
- // StatusMessage is a human-friendly message associated with a status.
- Status string `plist:"Status"`
- StatusMessage string `plist:"Status Message"`
-}
-
-// infoResult is the structure of the plist emitted directly from
-// --notarization-info
-type infoResult struct {
- Info *Info `plist:"notarization-info"`
+ Status string `plist:"status"`
- // Errors is the list of errors that occurred while uploading
- Errors Errors `plist:"product-errors"`
+ // StatusMessage is a human-friendly message associated with a status.
+ StatusMessage string `plist:"message"`
}
// info requests the information about a notarization and returns
@@ -78,12 +63,13 @@ func info(ctx context.Context, uuid string, opts *Options) (*Info, error) {
cmd.Args = []string{
filepath.Base(cmd.Path),
- "altool",
- "--notarization-info",
+ "notarytool",
+ "info",
uuid,
- "-u", opts.Username,
- "-p", opts.Password,
- "--output-format", "xml",
+ "--apple-id", opts.DeveloperId,
+ "--password", opts.Password,
+ "--team-id", opts.Provider,
+ "--output-format", "plist",
}
// We store all output in out for logging and in case there is an error
@@ -109,23 +95,18 @@ func info(ctx context.Context, uuid string, opts *Options) (*Info, error) {
// If we have any output, try to decode that since even in the case of
// an error it will output some information.
- var result infoResult
+ var result Info
if out.Len() > 0 {
if _, perr := plist.Unmarshal(out.Bytes(), &result); perr != nil {
return nil, fmt.Errorf("failed to decode notarization submission output: %w", perr)
}
}
- // If there are errors in the result, then show that error
- if len(result.Errors) > 0 {
- return nil, result.Errors
- }
-
// Now we check the error for actually running the process
if err != nil {
return nil, fmt.Errorf("error checking on notarization status:\n\n%s", combined.String())
}
- logger.Info("notarization info", "uuid", uuid, "info", result.Info)
- return result.Info, nil
+ logger.Info("notarization info", "uuid", uuid, "info", result)
+ return &result, nil
}
diff --git a/notarize/info_test.go b/notarize/info_test.go
index 37b040b..bf7b72b 100644
--- a/notarize/info_test.go
+++ b/notarize/info_test.go
@@ -11,56 +11,75 @@ import (
)
func init() {
- childCommands["info-success"] = testCmdInfoSuccess
+ childCommands["info-accepted"] = testCmdInfoAcceptedSubmission
+ childCommands["info-invalid"] = testCmdInfoInvalidSubmission
}
-func TestInfo_success(t *testing.T) {
+func TestInfo_accepted(t *testing.T) {
info, err := info(context.Background(), "foo", &Options{
Logger: hclog.L(),
- BaseCmd: childCmd(t, "info-success"),
+ BaseCmd: childCmd(t, "info-accepted"),
})
require := require.New(t)
require.NoError(err)
- require.Equal(info.RequestUUID, "edc8e846-d6ce-444d-9eef-499aa444da1c")
- require.Equal(info.Hash, "644d0af906ae26c87037cd6e9073382d5b0461b39e7f23c7bb69a35debacedd4")
- require.Equal(info.LogFileURL, "https://osxapps-ssl.itunes.apple.com/itunes-assets/Enigma123/v4/29/f2/81/29f28128-e2be-158a-f421-1e19692dd935/developer_log.json?accessKey=1572864491_3132212434837665280_4XLMw7lZxMfKdHhgnlPkueVue9woI2MjQ6VEc8R0cxJrL9GGcTQSiE0C9Cu5o6o%2B3JtYGSqGWdvc3mJHbS0NBRZkHT%2BbwbdMGPT8poYk7TTkfHUIcW5aBz0aFO7RB6mSWVuZWOFT0dZ4VS%2Bep2LUP2KTDtDwiGQbTULu9VgZ1oY%3D")
- require.Equal(info.Status, "success")
- require.Equal(info.StatusMessage, "Package Approved")
+ require.Equal(info.RequestUUID, "32684f68-d63e-49ba-9234-25eeec84b369")
+ require.Equal(info.Status, "Accepted")
+ require.Equal(info.StatusMessage, "Successfully received submission info")
}
-// testCmdInfoSuccess mimicks a successful submission.
-func testCmdInfoSuccess() int {
+func TestInfo_invalid(t *testing.T) {
+ info, err := info(context.Background(), "foo", &Options{
+ Logger: hclog.L(),
+ BaseCmd: childCmd(t, "info-invalid"),
+ })
+
+ require := require.New(t)
+ require.NoError(err)
+ require.Equal(info.RequestUUID, "cfd69166-8e2f-1397-8636-ec06f98e3597")
+ require.Equal(info.Status, "Invalid")
+}
+
+// testCmdInfoAcceptedSubmission mimicks an accepted submission.
+func testCmdInfoAcceptedSubmission() int {
+ fmt.Println(strings.TrimSpace(`
+
+
+
+
+ createdDate
+ 2023-08-01T08:22:19.939Z
+ id
+ 32684f68-d63e-49ba-9234-25eeec84b369
+ message
+ Successfully received submission info
+ name
+ binary.zip
+ status
+ Accepted
+
+
+`))
+ return 0
+}
+
+// testCmdInfoInvalidSubmission mimicks an invalid submission.
+func testCmdInfoInvalidSubmission() int {
fmt.Println(strings.TrimSpace(`
- notarization-info
-
- Date
- 2019-11-02T02:17:12Z
- Hash
- 644d0af906ae26c87037cd6e9073382d5b0461b39e7f23c7bb69a35debacedd4
- LogFileURL
- https://osxapps-ssl.itunes.apple.com/itunes-assets/Enigma123/v4/29/f2/81/29f28128-e2be-158a-f421-1e19692dd935/developer_log.json?accessKey=1572864491_3132212434837665280_4XLMw7lZxMfKdHhgnlPkueVue9woI2MjQ6VEc8R0cxJrL9GGcTQSiE0C9Cu5o6o%2B3JtYGSqGWdvc3mJHbS0NBRZkHT%2BbwbdMGPT8poYk7TTkfHUIcW5aBz0aFO7RB6mSWVuZWOFT0dZ4VS%2Bep2LUP2KTDtDwiGQbTULu9VgZ1oY%3D
- RequestUUID
- edc8e846-d6ce-444d-9eef-499aa444da1c
- Status
- success
- Status Code
- 0
- Status Message
- Package Approved
-
- os-version
- 10.15.1
- success-message
- No errors getting notarization info.
- tool-path
- /Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/Frameworks/AppStoreService.framework
- tool-version
- 4.00.1181
+ createdDate
+ 2023-08-01T08:12:11.193Z
+ id
+ cfd69166-8e2f-1397-8636-ec06f98e3597
+ message
+ Successfully received submission info
+ name
+ binary.zip
+ status
+ Invalid
`))
diff --git a/notarize/log.go b/notarize/log.go
index c9325a1..c2ac6cc 100644
--- a/notarize/log.go
+++ b/notarize/log.go
@@ -1,20 +1,18 @@
package notarize
import (
+ "bytes"
+ "context"
"encoding/json"
"fmt"
"io"
+ "os/exec"
+ "path/filepath"
- "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-hclog"
- "github.com/hashicorp/go-retryablehttp"
)
-// Log is the structure that is available when downloading the log file
-// that the notarization service creates.
-//
-// This may not be complete with all fields. I only included fields that
-// I saw and even then only the more useful ones.
+// Log Retrieves notarization log for a single completed submission
type Log struct {
JobId string `json:"jobId"`
Status string `json:"status"`
@@ -42,40 +40,77 @@ type LogTicketContent struct {
Arch string `json:"arch"`
}
-// These are the log severities that may exist.
-const (
- LogSeverityError = "error"
- LogSeverityWarning = "warning"
-)
+// log requests the information about a notarization and returns
+// the updated information.
+func log(ctx context.Context, uuid string, opts *Options) (*Log, error) {
+ logger := opts.Logger
+ if logger == nil {
+ logger = hclog.NewNullLogger()
+ }
+
+ // Build our command
+ var cmd exec.Cmd
+ if opts.BaseCmd != nil {
+ cmd = *opts.BaseCmd
+ }
-// ParseLog parses a log from the given reader, such as an HTTP response.
-func ParseLog(r io.Reader) (*Log, error) {
- // Protect against this since it is common with HTTP responses.
- if r == nil {
- return nil, fmt.Errorf("nil reader given to ParseLog")
+ // We only set the path if it isn't set. This lets the options set the
+ // path to the codesigning binary that we use.
+ if cmd.Path == "" {
+ path, err := exec.LookPath("xcrun")
+ if err != nil {
+ return nil, err
+ }
+ cmd.Path = path
}
+ cmd.Args = []string{
+ filepath.Base(cmd.Path),
+ "notarytool",
+ "log",
+ uuid,
+ "--apple-id", opts.DeveloperId,
+ "--password", opts.Password,
+ "--team-id", opts.Provider,
+ }
+
+ // We store all output in out for logging and in case there is an error
+ var out, combined bytes.Buffer
+ cmd.Stdout = io.MultiWriter(&out, &combined)
+ cmd.Stderr = &combined
+
+ // Log what we're going to execute
+ logger.Info("requesting notarization log",
+ "uuid", uuid,
+ "command_path", cmd.Path,
+ "command_args", cmd.Args,
+ )
+
+ // Execute
+ err := cmd.Run()
+
+ // Log the result
+ logger.Info("notarization log command finished",
+ "output", out.String(),
+ "err", err,
+ )
+
+ // If we have any output, try to decode that since even in the case of
+ // an error it will output some information.
var result Log
- return &result, json.NewDecoder(r).Decode(&result)
-}
+ // return &result, json.NewDecoder().Decode(&result)
+ if out.Len() > 0 {
+ if derr := json.Unmarshal(out.Bytes(), &result); derr != nil {
+ return nil, fmt.Errorf("failed to decode notarization submission output: %w", derr)
-// DownloadLog downloads a log file and parses it using a default HTTP client.
-// If you want more fine-grained control over the download, download it
-// using your own client and use ParseLog.
-func DownloadLog(path string) (*Log, error) {
- // Build our HTTP client
- client := retryablehttp.NewClient()
- client.HTTPClient = cleanhttp.DefaultClient()
- client.Logger = hclog.NewNullLogger()
-
- // Get it!
- resp, err := client.Get(path)
- if err != nil {
- return nil, err
+ }
}
- if resp.Body != nil {
- defer resp.Body.Close()
+
+ // Now we check the error for actually running the process
+ if err != nil {
+ return nil, fmt.Errorf("error checking on notarization status:\n\n%s", combined.String())
}
- return ParseLog(resp.Body)
+ logger.Info("notarization log", "uuid", uuid, "info", result)
+ return &result, nil
}
diff --git a/notarize/log_test.go b/notarize/log_test.go
index bc7c2bc..261b3bc 100644
--- a/notarize/log_test.go
+++ b/notarize/log_test.go
@@ -1,44 +1,116 @@
package notarize
import (
- "os"
- "path/filepath"
+ "context"
+ "fmt"
+ "strings"
"testing"
- "github.com/davecgh/go-spew/spew"
- "github.com/sebdah/goldie"
+ "github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
)
func init() {
- goldie.FixtureDir = "testdata"
- spew.Config.DisablePointerAddresses = true
+ childCommands["log-accepted"] = testCmdLogValidSubmission
+ childCommands["log-invalid"] = testCmdLogInvalidSubmission
}
-func TestParseFile(t *testing.T) {
- f, err := os.Open("testdata")
- require.NoError(t, err)
- defer f.Close()
+func TestLog_accepted(t *testing.T) {
+ log, err := log(context.Background(), "foo", &Options{
+ Logger: hclog.L(),
+ BaseCmd: childCmd(t, "log-accepted"),
+ })
- fis, err := f.Readdir(-1)
- require.NoError(t, err)
- for _, fi := range fis {
- if fi.IsDir() {
- continue
- }
+ require := require.New(t)
+ require.NoError(err)
+ require.Equal(log.JobId, "3382aa04-e417-46a0-b1b4-42eebf85906c")
+ require.Equal(log.Status, "Accepted")
+ require.Equal(log.StatusSummary, "Ready for distribution")
+ require.Equal(len(log.Issues), 0)
+ require.Equal(len(log.TicketContents), 1)
+}
- if filepath.Ext(fi.Name()) == ".golden" {
- continue
- }
+func TestLog_invalid(t *testing.T) {
+ log, err := log(context.Background(), "foo", &Options{
+ Logger: hclog.L(),
+ BaseCmd: childCmd(t, "log-invalid"),
+ })
+
+ require := require.New(t)
+ require.NoError(err)
+ require.Equal(log.JobId, "4ba7c420-7444-44bc-a190-1bd4bad97b13")
+ require.Equal(log.Status, "Invalid")
+ require.Equal(log.StatusSummary, "Archive contains critical validation errors")
+ require.Equal(len(log.TicketContents), 0)
+ require.Equal(len(log.Issues), 3)
+}
- t.Run(fi.Name(), func(t *testing.T) {
- f, err := os.Open(filepath.Join("testdata", fi.Name()))
- require.NoError(t, err)
- defer f.Close()
+// testCmdLogValidSubmission mimicks an accepted submission.
+func testCmdLogValidSubmission() int {
+ fmt.Println(strings.TrimSpace(`
+{
+ "logFormatVersion": 1,
+ "jobId": "3382aa04-e417-46a0-b1b4-42eebf85906c",
+ "status": "Accepted",
+ "statusSummary": "Ready for distribution",
+ "statusCode": 0,
+ "archiveFilename": "gon.zip",
+ "uploadDate": "2019-11-06T00:51:10Z",
+ "sha256": "1070be725b5b0c89b8dad699a9080a3bf5809fe68bfe8f84d6ff4a282d661fd1",
+ "ticketContents": [
+ {
+ "path": "gon.zip/foo",
+ "digestAlgorithm": "SHA-256",
+ "cdhash": "b7049085e21423f102d6119bca93d57ebd903289",
+ "arch": "x86_64"
+ }
+ ],
+ "issues": null
+}
+`))
+ return 0
+}
- log, err := ParseLog(f)
- require.NoError(t, err)
- goldie.Assert(t, fi.Name(), []byte(spew.Sdump(log)))
- })
- }
+// testCmdLogInvalidSubmission mimicks an invalid submission.
+func testCmdLogInvalidSubmission() int {
+ fmt.Println(strings.TrimSpace(`
+{
+ "logFormatVersion": 1,
+ "jobId": "4ba7c420-7444-44bc-a190-1bd4bad97b13",
+ "status": "Invalid",
+ "statusSummary": "Archive contains critical validation errors",
+ "statusCode": 4000,
+ "archiveFilename": "gon.zip",
+ "uploadDate": "2019-11-06T00:54:22Z",
+ "sha256": "c109f26d378fbf1efadc8987fdab79d2ce63155e8941823d4d11a907152e11a5",
+ "ticketContents": null,
+ "issues": [
+ {
+ "severity": "error",
+ "code": null,
+ "path": "gon.zip/foo",
+ "message": "The binary is not signed.",
+ "docUrl": null,
+ "architecture": "x86_64"
+ },
+ {
+ "severity": "error",
+ "code": null,
+ "path": "gon.zip/foo",
+ "message": "The signature does not include a secure timestamp.",
+ "docUrl": null,
+ "architecture": "x86_64"
+ },
+ {
+ "severity": "error",
+ "code": null,
+ "path": "gon.zip/foo",
+ "message": "The executable does not have the hardened runtime enabled.",
+ "docUrl": null,
+ "architecture": "x86_64"
+ }
+ ]
+}
+`))
+ return 0
}
diff --git a/notarize/notarize.go b/notarize/notarize.go
index c28162a..c235917 100644
--- a/notarize/notarize.go
+++ b/notarize/notarize.go
@@ -16,11 +16,8 @@ type Options struct {
// File is the file to notarize. This must be in zip, dmg, or pkg format.
File string
- // BundleId is the bundle ID for the package. Ex. "com.example.myapp"
- BundleId string
-
- // Username is your Apple Connect username.
- Username string
+ // DeveloperId is your Apple Developer Apple ID.
+ DeveloperId string
// Password is your Apple Connect password. This must be specified.
// This also supports `@keychain:` and `@env:` formats to
@@ -47,7 +44,7 @@ type Options struct {
// BaseCmd is the base command for executing app submission. This is
// used for tests to overwrite where the codesign binary is. If this isn't
- // specified then we use `xcrun altool` as the base.
+ // specified then we use `xcrun notarytool` as the base.
BaseCmd *exec.Cmd
}
@@ -61,7 +58,7 @@ type Options struct {
//
// If error is nil, then Info is guaranteed to be non-nil.
// If error is not nil, notarization failed and Info _may_ be non-nil.
-func Notarize(ctx context.Context, opts *Options) (*Info, error) {
+func Notarize(ctx context.Context, opts *Options) (*Info, *Log, error) {
logger := opts.Logger
if logger == nil {
logger = hclog.NewNullLogger()
@@ -83,7 +80,7 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) {
uuid, err := upload(ctx, opts)
lock.Unlock()
if err != nil {
- return nil, err
+ return nil, nil, err
}
status.Submitted(uuid)
@@ -91,10 +88,10 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) {
// _to even exist_. While we get an error requesting info with an error
// code of 1519 (UUID not found), then we are stuck in a queue. Sometimes
// this queue is hours long. We just have to wait.
- result := &Info{RequestUUID: uuid}
+ infoResult := &Info{RequestUUID: uuid}
for {
time.Sleep(10 * time.Second)
- _, err := info(ctx, result.RequestUUID, opts)
+ _, err := info(ctx, infoResult.RequestUUID, opts)
if err == nil {
break
}
@@ -106,7 +103,7 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) {
}
// A real error, just return that
- return result, err
+ return infoResult, nil, err
}
// Now that the UUID result has been found, we poll more quickly
@@ -115,9 +112,42 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) {
for {
// Update the info. It is possible for this to return a nil info
// and we dont' ever want to set result to nil so we have a check.
- newResult, err := info(ctx, result.RequestUUID, opts)
- if newResult != nil {
- result = newResult
+ newInfoResult, err := info(ctx, infoResult.RequestUUID, opts)
+ if newInfoResult != nil {
+ infoResult = newInfoResult
+ }
+
+ if err != nil {
+ // This code is the network became unavailable error. If this
+ // happens then we just log and retry.
+ if e, ok := err.(Errors); ok && e.ContainsCode(-19000) {
+ logger.Warn("error that network became unavailable, will retry")
+ goto RETRYINFO
+ }
+
+ return infoResult, nil, err
+ }
+
+ status.InfoStatus(*infoResult)
+
+ // If we reached a terminal state then exit
+ if infoResult.Status == "Accepted" || infoResult.Status == "Invalid" {
+ break
+ }
+
+ RETRYINFO:
+ // Sleep, we just do a constant poll every 5 seconds. I haven't yet
+ // found any rate limits to the service so this seems okay.
+ time.Sleep(5 * time.Second)
+ }
+
+ logResult := &Log{JobId: uuid}
+ for {
+ // Update the log. It is possible for this to return a nil log
+ // and we dont' ever want to set result to nil so we have a check.
+ newLogResult, err := log(ctx, logResult.JobId, opts)
+ if newLogResult != nil {
+ logResult = newLogResult
}
if err != nil {
@@ -125,20 +155,20 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) {
// happens then we just log and retry.
if e, ok := err.(Errors); ok && e.ContainsCode(-19000) {
logger.Warn("error that network became unavailable, will retry")
- goto RETRY
+ goto RETRYLOG
}
- return result, err
+ return infoResult, logResult, err
}
- status.Status(*result)
+ status.LogStatus(*logResult)
// If we reached a terminal state then exit
- if result.Status == "success" || result.Status == "invalid" {
+ if logResult.Status == "Accepted" || logResult.Status == "Invalid" {
break
}
- RETRY:
+ RETRYLOG:
// Sleep, we just do a constant poll every 5 seconds. I haven't yet
// found any rate limits to the service so this seems okay.
time.Sleep(5 * time.Second)
@@ -146,9 +176,9 @@ func Notarize(ctx context.Context, opts *Options) (*Info, error) {
// If we're in an invalid status then return an error
err = nil
- if result.Status == "invalid" {
- err = fmt.Errorf("package is invalid. To learn more download the logs at the URL: %s", result.LogFileURL)
+ if logResult.Status == "Invalid" && infoResult.Status == "Invalid" {
+ err = fmt.Errorf("package is invalid.")
}
- return result, err
+ return infoResult, logResult, err
}
diff --git a/notarize/status.go b/notarize/status.go
index bdb3b4d..060dd00 100644
--- a/notarize/status.go
+++ b/notarize/status.go
@@ -12,11 +12,14 @@ type Status interface {
// The arguments give you access to the requestUUID to query more information.
Submitted(requestUUID string)
- // Status is called as the status of the submitted package changes.
+ // InfoStatus is called as the status of the submitted package changes.
// The info argument contains additional information about the status.
// Note that some fields in the info argument may not be populated, please
// refer to the docs.
- Status(Info)
+ InfoStatus(Info)
+
+ // LogStatus is called as the status of the submitted package changes.
+ LogStatus(Log)
}
// noopStatus implements Status and does nothing.
@@ -24,7 +27,8 @@ type noopStatus struct{}
func (noopStatus) Submitting() {}
func (noopStatus) Submitted(string) {}
-func (noopStatus) Status(Info) {}
+func (noopStatus) InfoStatus(Info) {}
+func (noopStatus) LogStatus(Log) {}
// Assert that we always implement it
var _ Status = noopStatus{}
diff --git a/notarize/testdata/log_zip_failure.json b/notarize/testdata/log_zip_failure.json
deleted file mode 100644
index de6d2d1..0000000
--- a/notarize/testdata/log_zip_failure.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "logFormatVersion": 1,
- "jobId": "4ba7c420-7444-44bc-a190-1bd4bad97b13",
- "status": "Invalid",
- "statusSummary": "Archive contains critical validation errors",
- "statusCode": 4000,
- "archiveFilename": "gon.zip",
- "uploadDate": "2019-11-06T00:54:22Z",
- "sha256": "c109f26d378fbf1efadc8987fdab79d2ce63155e8941823d4d11a907152e11a5",
- "ticketContents": null,
- "issues": [
- {
- "severity": "error",
- "code": null,
- "path": "gon.zip/foo",
- "message": "The binary is not signed.",
- "docUrl": null,
- "architecture": "x86_64"
- },
- {
- "severity": "error",
- "code": null,
- "path": "gon.zip/foo",
- "message": "The signature does not include a secure timestamp.",
- "docUrl": null,
- "architecture": "x86_64"
- },
- {
- "severity": "error",
- "code": null,
- "path": "gon.zip/foo",
- "message": "The executable does not have the hardened runtime enabled.",
- "docUrl": null,
- "architecture": "x86_64"
- }
- ]
-}
diff --git a/notarize/testdata/log_zip_failure.json.golden b/notarize/testdata/log_zip_failure.json.golden
deleted file mode 100644
index 4391593..0000000
--- a/notarize/testdata/log_zip_failure.json.golden
+++ /dev/null
@@ -1,27 +0,0 @@
-(*notarize.Log)({
- JobId: (string) (len=36) "4ba7c420-7444-44bc-a190-1bd4bad97b13",
- Status: (string) (len=7) "Invalid",
- StatusSummary: (string) (len=43) "Archive contains critical validation errors",
- StatusCode: (int) 4000,
- ArchiveFilename: (string) (len=7) "gon.zip",
- UploadDate: (string) (len=20) "2019-11-06T00:54:22Z",
- SHA256: (string) (len=64) "c109f26d378fbf1efadc8987fdab79d2ce63155e8941823d4d11a907152e11a5",
- Issues: ([]notarize.LogIssue) (len=3 cap=4) {
- (notarize.LogIssue) {
- Severity: (string) (len=5) "error",
- Path: (string) (len=11) "gon.zip/foo",
- Message: (string) (len=25) "The binary is not signed."
- },
- (notarize.LogIssue) {
- Severity: (string) (len=5) "error",
- Path: (string) (len=11) "gon.zip/foo",
- Message: (string) (len=50) "The signature does not include a secure timestamp."
- },
- (notarize.LogIssue) {
- Severity: (string) (len=5) "error",
- Path: (string) (len=11) "gon.zip/foo",
- Message: (string) (len=58) "The executable does not have the hardened runtime enabled."
- }
- },
- TicketContents: ([]notarize.LogTicketContent)
-})
diff --git a/notarize/testdata/log_zip_success.json b/notarize/testdata/log_zip_success.json
deleted file mode 100644
index 79d0d30..0000000
--- a/notarize/testdata/log_zip_success.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "logFormatVersion": 1,
- "jobId": "3382aa04-e417-46a0-b1b4-42eebf85906c",
- "status": "Accepted",
- "statusSummary": "Ready for distribution",
- "statusCode": 0,
- "archiveFilename": "gon.zip",
- "uploadDate": "2019-11-06T00:51:10Z",
- "sha256": "1070be725b5b0c89b8dad699a9080a3bf5809fe68bfe8f84d6ff4a282d661fd1",
- "ticketContents": [
- {
- "path": "gon.zip/foo",
- "digestAlgorithm": "SHA-256",
- "cdhash": "b7049085e21423f102d6119bca93d57ebd903289",
- "arch": "x86_64"
- }
- ],
- "issues": null
-}
diff --git a/notarize/testdata/log_zip_success.json.golden b/notarize/testdata/log_zip_success.json.golden
deleted file mode 100644
index 60190c3..0000000
--- a/notarize/testdata/log_zip_success.json.golden
+++ /dev/null
@@ -1,18 +0,0 @@
-(*notarize.Log)({
- JobId: (string) (len=36) "3382aa04-e417-46a0-b1b4-42eebf85906c",
- Status: (string) (len=8) "Accepted",
- StatusSummary: (string) (len=22) "Ready for distribution",
- StatusCode: (int) 0,
- ArchiveFilename: (string) (len=7) "gon.zip",
- UploadDate: (string) (len=20) "2019-11-06T00:51:10Z",
- SHA256: (string) (len=64) "1070be725b5b0c89b8dad699a9080a3bf5809fe68bfe8f84d6ff4a282d661fd1",
- Issues: ([]notarize.LogIssue) ,
- TicketContents: ([]notarize.LogTicketContent) (len=1 cap=4) {
- (notarize.LogTicketContent) {
- Path: (string) (len=11) "gon.zip/foo",
- DigestAlgorithm: (string) (len=7) "SHA-256",
- CDHash: (string) (len=40) "b7049085e21423f102d6119bca93d57ebd903289",
- Arch: (string) (len=6) "x86_64"
- }
- }
-})
diff --git a/notarize/upload.go b/notarize/upload.go
index a755b2b..960a4cb 100644
--- a/notarize/upload.go
+++ b/notarize/upload.go
@@ -38,24 +38,14 @@ func upload(ctx context.Context, opts *Options) (string, error) {
cmd.Args = []string{
filepath.Base(cmd.Path),
- "altool",
- "--notarize-app",
- "--primary-bundle-id", opts.BundleId,
- "-u", opts.Username,
- "-p", opts.Password,
+ "notarytool",
+ "submit", opts.File,
+ "--apple-id", opts.DeveloperId,
+ "--password", opts.Password,
+ "--team-id", opts.Provider,
+ "--output-format", "plist",
}
- if opts.Provider != "" {
- cmd.Args = append(cmd.Args,
- "--asc-provider", opts.Provider,
- )
- }
-
- cmd.Args = append(cmd.Args,
- "-f", opts.File,
- "--output-format", "xml",
- )
-
// We store all output in out for logging and in case there is an error
var out, combined bytes.Buffer
cmd.Stdout = io.MultiWriter(&out, &combined)
@@ -86,36 +76,26 @@ func upload(ctx context.Context, opts *Options) (string, error) {
}
}
- // If there are errors in the result, then show that error
- if len(result.Errors) > 0 {
- return "", result.Errors
- }
-
// Now we check the error for actually running the process
if err != nil {
return "", fmt.Errorf("error submitting for notarization:\n\n%s", combined.String())
}
// We should have a request UUID set at this point since we checked for errors
- if result.Upload == nil || result.Upload.RequestUUID == "" {
+ if result.RequestUUID == "" {
return "", fmt.Errorf(
"notarization appeared to succeed, but we failed at parsing " +
"the request UUID. Please enable logging, try again, and report " +
"this as a bug.")
}
- logger.Info("notarization request submitted", "request_id", result.Upload.RequestUUID)
- return result.Upload.RequestUUID, nil
+ logger.Info("notarization request submitted", "request_id", result.RequestUUID)
+ return result.RequestUUID, nil
}
// uploadResult is the plist structure when the upload succeeds
type uploadResult struct {
// Upload is non-nil if there is a successful upload
- Upload *struct {
- RequestUUID string `plist:"RequestUUID"`
- } `plist:"notarization-upload"`
-
- // Errors is the list of errors that occurred while uploading
- Errors Errors `plist:"product-errors"`
+ RequestUUID string `plist:"id"`
}
diff --git a/notarize/upload_test.go b/notarize/upload_test.go
index 46d5716..28455fd 100644
--- a/notarize/upload_test.go
+++ b/notarize/upload_test.go
@@ -12,7 +12,6 @@ import (
func init() {
childCommands["upload-success"] = testCmdUploadSuccess
- childCommands["upload-errors"] = testCmdUploadErrors
childCommands["upload-exit-status"] = testCmdUploadExitStatus
}
@@ -23,17 +22,7 @@ func TestUpload_success(t *testing.T) {
})
require.NoError(t, err)
- require.Equal(t, uuid, "edc8e846-d6ce-444d-9eef-499aa444da1c")
-}
-
-func TestUpload_errors(t *testing.T) {
- uuid, err := upload(context.Background(), &Options{
- Logger: hclog.L(),
- BaseCmd: childCmd(t, "upload-errors"),
- })
-
- require.Error(t, err)
- require.Empty(t, uuid)
+ require.Equal(t, uuid, "cfd69166-8e2f-1397-8636-ec06f98e3597")
}
func TestUpload_exitStatus(t *testing.T) {
@@ -53,62 +42,15 @@ func testCmdUploadSuccess() int {
- notarization-upload
-
- RequestUUID
- edc8e846-d6ce-444d-9eef-499aa444da1c
-
- os-version
- 10.15.1
- success-message
- No errors uploading './terraform.zip'.
- tool-path
- /Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/Frameworks/AppStoreService.framework
- tool-version
- 4.00.1181
-
-
-`))
- return 0
-}
-
-// testCmdUploadErrors mimicks a successful submission.
-func testCmdUploadErrors() int {
- fmt.Println(strings.TrimSpace(`
-
-
-
-
- os-version
- 10.15.1
- product-errors
-
-
- code
- -18000
- message
- ERROR ITMS-90732: "The software asset has already been uploaded. The upload ID is 671a0eb9-01b0-4966-b485-78b9f370abff" at SoftwareAssets/EnigmaSoftwareAsset
- userInfo
-
- NSLocalizedDescription
- ERROR ITMS-90732: "The software asset has already been uploaded. The upload ID is 671a0eb9-01b0-4966-b485-78b9f370abff" at SoftwareAssets/EnigmaSoftwareAsset
- NSLocalizedFailureReason
- ERROR ITMS-90732: "The software asset has already been uploaded. The upload ID is 671a0eb9-01b0-4966-b485-78b9f370abff" at SoftwareAssets/EnigmaSoftwareAsset
- NSLocalizedRecoverySuggestion
- ERROR ITMS-90732: "The software asset has already been uploaded. The upload ID is 671a0eb9-01b0-4966-b485-78b9f370abff" at SoftwareAssets/EnigmaSoftwareAsset
-
-
-
- tool-path
- /Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/Frameworks/AppStoreService.framework
- tool-version
- 4.00.1181
+ id
+ cfd69166-8e2f-1397-8636-ec06f98e3597
+ message
+ Successfully uploaded file
+ path
+ /path/to/binary.zip
`))
-
- // Despite an error we return exit code 0 so we can test that we error
- // in the presence of errors in the output.
return 0
}