diff --git a/.gitignore b/.gitignore index 69e6304..197284d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .bitrise* _tmp .idea +steps-fastlane +.DS_Store diff --git a/bitrise.yml b/bitrise.yml index 178a58f..a60445c 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1,47 +1,67 @@ -format_version: 7 +format_version: "11" default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git workflows: - # ---------------------------------------------------------------- - # --- workflow to test this step ci: + before_run: + - audit-this-step + - go-tests + after_run: + - test + + go-tests: + steps: + - go-list: {} + - golint: {} + - errcheck: {} + - go-test: {} + + test: + before_run: + - auth-all + - cache + + auth-all: + before_run: + - auth-test-api-key-connection + - auth-test-apple-id-connection + - auth-test-apple-id-connection-globally-set-app-specific-password + - auth-test-api-key-input + + cache: envs: - SAMPLE_APP_URL: "https://github.com/bitrise-io/sample-apps-flutter-veggieseasons.git" - BRANCH: master - before-run: - - audit-this-step steps: - - go-list: - - golint: - - errcheck: - - go-test: - script: inputs: - content: rm -rf ./_tmp - change-workdir: title: Switch working dir to test/_tmp dir - run_if: true + run_if: "true" inputs: - path: ./_tmp - is_create_path: true - - script: + - git::https://github.com/bitrise-steplib/bitrise-step-simple-git-clone: inputs: - - content: |- - set -ex - git clone $SAMPLE_APP_URL -b $BRANCH . + - repository_url: $SAMPLE_APP_URL + - branch: $BRANCH + - clone_into_dir: "." - path::./: - title: Test if Fastlane receives session-based Apple Developer connection + title: Smoke test to check if Fastlane receives session-based Apple Developer connection inputs: - lane: test_fastlane_session - work_dir: ./ - verbose_log: "yes" - - certificate-and-profile-installer: + - connection: apple_id + - certificate-and-profile-installer: {} - path::./: title: Test building a Flutter project inputs: - lane: build - work_dir: ./ - verbose_log: "yes" + - connection: "off" - script: inputs: - content: |- @@ -56,51 +76,103 @@ workflows: exit 1 fi - fastlane-session-test: + auth-test-apple-id-connection: + before_run: + - _auth_prepare + steps: + - path::./: + inputs: + - lane: release + - work_dir: ./ + - verbose_log: "yes" + - connection: apple_id + - app_password: $FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD + + auth-test-apple-id-connection-globally-set-app-specific-password: + before_run: + - _auth_prepare + steps: + - path::./: + inputs: + - lane: release + - work_dir: ./ + - verbose_log: "yes" + - connection: apple_id + - app_password: "" + + auth-test-api-key-connection: + before_run: + - _auth_prepare + steps: + - path::./: + inputs: + - lane: release + - work_dir: ./ + - verbose_log: "yes" + - connection: api_key + + auth-test-api-key-input: + before_run: + - _auth_prepare + steps: + - path::./: + inputs: + - lane: release + - work_dir: ./ + - verbose_log: "yes" + - connection: "off" + - api_issuer: $AUTH_API_ISSUER + - api_key_path: $BITRISEIO_AUTH_API_KEY_PATH_URL + + _auth_prepare: envs: - SAMPLE_APP_URL: "https://github.com/bitrise-samples/sample-apps-fastlane-test.git" - - BITRISE_PROJECT_PATH: ./sample-apps-fastlane-test.xcodeproj - - BITRISE_SCHEME: sample-apps-fastlane-test + - BRANCH: master + - INFO_PLIST_PATH: ./sample-apps-fastlane-test/Info.plist steps: + - script: + title: Clean saved Fastlane session cookie + inputs: + - content: rm -rf ~/.fastlane - script: inputs: - content: rm -rf ./_tmp - change-workdir: title: Switch working dir to test/_tmp dir - run_if: true + run_if: "true" inputs: - path: ./_tmp - is_create_path: true - - script: - inputs: - - content: git clone $SAMPLE_APP_URL . - - ios-auto-provision@1.3.1: + - git::https://github.com/bitrise-steplib/bitrise-step-simple-git-clone: inputs: - - generate_profiles: 'yes' - - distribution_type: app-store - - path::./: - inputs: - - lane: release - - work_dir: ./ - - verbose_log: "yes" - - # ---------------------------------------------------------------- - # --- Utility workflows - vendor-update: - title: Vendor update - description: | - Used for updating the vendored dependencies - steps: + - repository_url: $SAMPLE_APP_URL + - branch: $BRANCH + - clone_into_dir: "." + - certificate-and-profile-installer: {} - script: - title: Vendor update inputs: - content: |- - #!/bin/bash - set -ex - go mod vendor + #!/usr/bin/env bash + CURRENT_BUILD_NUMBER=${BITRISE_BUILD_NUMBER} + BITRISE_BUILD_NUMBER=$((($(gdate +%Y%m%d%H%M%S)-20000000000000)*10000)) + if [[ -z "${BITRISE_BUILD_NUMBER}" ]]; then + echo "error: failed to calculated build number" + exit 1 + fi + if [[ -n "${CURRENT_BUILD_NUMBER}" ]]; then + BITRISE_BUILD_NUMBER=$((${BITRISE_BUILD_NUMBER}+${CURRENT_BUILD_NUMBER})) + fi + + envman add --key BITRISE_BUILD_NUMBER --value "${BITRISE_BUILD_NUMBER}" + - set-xcode-build-number: + title: Set Build Number + run_if: "true" + inputs: + - build_version: $BITRISE_BUILD_NUMBER + - plist_path: $INFO_PLIST_PATH + - build_version_offset: $BITRISE_BUILD_VERSION_OFFSET + - # ---------------------------------------------------------------- - # --- workflows to Share this step into a Step Library audit-this-step: title: Audit the step steps: diff --git a/credentials.go b/credentials.go new file mode 100644 index 0000000..6b0157e --- /dev/null +++ b/credentials.go @@ -0,0 +1,68 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth" +) + +// fastlaneAPIKey is used to serialize App Store Connect API Key into JSON for fastlane +// see: https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file +type fastlaneAPIKey struct { + KeyID string `json:"key_id"` + IssuerID string `json:"issuer_id"` + PrivateKey string `json:"key"` +} + +// FastlaneAuthParams converts Apple credentials to Fastlane env vars and arguments +func FastlaneAuthParams(authConfig appleauth.Credentials) (map[string]string, error) { + envs := make(map[string]string) + if authConfig.AppleID != nil { + // Set as environment variables + if authConfig.AppleID.Username != "" { + envs["FASTLANE_USER"] = authConfig.AppleID.Username + envs["DELIVER_USERNAME"] = authConfig.AppleID.Username + } + if authConfig.AppleID.Password != "" { + envs["FASTLANE_PASSWORD"] = authConfig.AppleID.Password + envs["DELIVER_PASSWORD"] = authConfig.AppleID.Password + } + if authConfig.AppleID.Session != "" { + envs["FASTLANE_SESSION"] = authConfig.AppleID.Session + } + if authConfig.AppleID.AppSpecificPassword != "" { + envs["FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD"] = authConfig.AppleID.AppSpecificPassword + } + } + + if authConfig.APIKey != nil { + fastlaneAPIKeyParams, err := json.Marshal(fastlaneAPIKey{ + IssuerID: authConfig.APIKey.IssuerID, + KeyID: authConfig.APIKey.KeyID, + PrivateKey: authConfig.APIKey.PrivateKey, + }) + if err != nil { + return envs, fmt.Errorf("failed to marshal Fastane API Key configuration: %v", err) + } + + tmpDir, err := pathutil.NormalizedOSTempDirPath("apiKey") + if err != nil { + return envs, err + } + fastlaneAuthFile := filepath.Join(tmpDir, "api_key.json") + if err := ioutil.WriteFile(fastlaneAuthFile, fastlaneAPIKeyParams, os.ModePerm); err != nil { + return envs, err + } + + envs["DELIVER_API_KEY_PATH"] = fastlaneAuthFile + // deliver: "Precheck cannot check In-app purchases with the App Store Connect API Key (yet). Exclude In-app purchases from precheck" + envs["PRECHECK_INCLUDE_IN_APP_PURCHASES"] = "false" + } + + return envs, nil +} diff --git a/go.mod b/go.mod index 6bd6556..387b9b0 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,11 @@ require ( github.com/bitrise-io/bitrise v0.0.0-20190829132137-518e23eca82d // indirect github.com/bitrise-io/bitrise-init v0.0.0-20190813135927-7ff42ba329cb github.com/bitrise-io/envman v0.0.0-20190813133714-27a300a1ed43 // indirect - github.com/bitrise-io/go-steputils v0.0.0-20190806143347-f540824d77df + github.com/bitrise-io/go-steputils v0.0.0-20201016102104-03ae3a6ded35 github.com/bitrise-io/go-utils v0.0.0-20201211082830-859032e9adf0 github.com/bitrise-io/stepman v0.0.0-20190813144014-10564a4888a6 // indirect github.com/bitrise-steplib/bitrise-step-android-unit-test v0.0.0-20190902203028-ff8e682d8645 - github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver v0.0.0-20210111151319-1426de4b985f + github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver v0.0.0-20210202154324-7c20293c7cf0 github.com/google/go-cmp v0.5.4 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kr/pretty v0.1.0 // indirect diff --git a/go.sum b/go.sum index 9061b8e..af3ee1b 100644 --- a/go.sum +++ b/go.sum @@ -6,16 +6,16 @@ github.com/bitrise-io/bitrise-init v0.0.0-20190813135927-7ff42ba329cb h1:e1cLjVT github.com/bitrise-io/bitrise-init v0.0.0-20190813135927-7ff42ba329cb/go.mod h1:6tVeJwc47w4nYwexFkYLRwQ2gUnfL568ySfMTLBsONo= github.com/bitrise-io/envman v0.0.0-20190813133714-27a300a1ed43 h1:BRhP0pS7NPK4iFKlQszhbHoAwD6m88GFys2w+bgwiN8= github.com/bitrise-io/envman v0.0.0-20190813133714-27a300a1ed43/go.mod h1:m8pTp1o3Sw9uzDxb1WRm5IBRnMau2iOvPMSnRCAhQNI= -github.com/bitrise-io/go-steputils v0.0.0-20190806143347-f540824d77df h1:UaDw6nAsI2jlGjNqhdMUbk0xhJJ+iQZ1buEXHRKrtU8= -github.com/bitrise-io/go-steputils v0.0.0-20190806143347-f540824d77df/go.mod h1:GXgBV3Frd3qcnsg+NryQTyx1CHjZHr/2w7Bx4WAcB4o= +github.com/bitrise-io/go-steputils v0.0.0-20201016102104-03ae3a6ded35 h1:iKtx/RxSrA9xcjZ17W3yu3jMzwowNB6nxLF/1BwVYQ8= +github.com/bitrise-io/go-steputils v0.0.0-20201016102104-03ae3a6ded35/go.mod h1:GXgBV3Frd3qcnsg+NryQTyx1CHjZHr/2w7Bx4WAcB4o= github.com/bitrise-io/go-utils v0.0.0-20201211082830-859032e9adf0 h1:HR5o2gHKo0rAMfKogclAg4KOnEX8XniC7A3JFuEAXjY= github.com/bitrise-io/go-utils v0.0.0-20201211082830-859032e9adf0/go.mod h1:tTEsKvbz1LbzuN/KpVFHXnLtcAPdEgIdM41s0lL407s= github.com/bitrise-io/stepman v0.0.0-20190813144014-10564a4888a6 h1:/GnB2kEaO/6KSMfGpmntvohfacADpWFcf8iCPLsZqh4= github.com/bitrise-io/stepman v0.0.0-20190813144014-10564a4888a6/go.mod h1:hGCjd8leP411yt5QkQi+VBNWGxIZ4H02LNIVeKBeMUk= github.com/bitrise-steplib/bitrise-step-android-unit-test v0.0.0-20190902203028-ff8e682d8645 h1:9molXzIAxnKStwV78lt7MSgUQwxIWl4+r9/oYTQA7no= github.com/bitrise-steplib/bitrise-step-android-unit-test v0.0.0-20190902203028-ff8e682d8645/go.mod h1:0yqqJw+MqwsfHKq4pL90IoSYskLF91oDCyyZYnIehWA= -github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver v0.0.0-20210111151319-1426de4b985f h1:50OrOc2xqKRXBsC3lfryzm75/QHhltDR2HAIWtHjvJk= -github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver v0.0.0-20210111151319-1426de4b985f/go.mod h1:mm5WIhfyTnf89pNO+4ylSsUAbEh8JGSpV3F7V0XWB4U= +github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver v0.0.0-20210202154324-7c20293c7cf0 h1:DKE9KQEbdO4iGpDQF1uiaYtgFOyu/lH1srg3ufAMURI= +github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver v0.0.0-20210202154324-7c20293c7cf0/go.mod h1:mG5kKjSyK3sZNp7e5QpFBAtxJRWeA+4PSMh3ZfwggNs= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -37,6 +37,8 @@ github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIH github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -62,3 +64,5 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 7f60cf7..bf1566f 100644 --- a/main.go +++ b/main.go @@ -16,22 +16,59 @@ import ( "github.com/bitrise-io/go-utils/command" "github.com/bitrise-io/go-utils/command/gems" "github.com/bitrise-io/go-utils/command/rubycommand" + "github.com/bitrise-io/go-utils/errorutil" "github.com/bitrise-io/go-utils/fileutil" "github.com/bitrise-io/go-utils/log" "github.com/bitrise-io/go-utils/pathutil" + "github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth" "github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice" "github.com/kballard/go-shellquote" ) // Config contains inputs parsed from environment variables type Config struct { - WorkDir string `env:"work_dir,dir"` - Lane string `env:"lane,required"` - UpdateFastlane bool `env:"update_fastlane,opt[true,false]"` - VerboseLog bool `env:"verbose_log,opt[yes,no]"` - EnableCache bool `env:"enable_cache,opt[yes,no]"` + WorkDir string `env:"work_dir,dir"` + Lane string `env:"lane,required"` + + BitriseConnection string `env:"connection,opt[automatic,api_key,apple_id,off]"` + AppleID string `env:"apple_id"` + Password stepconf.Secret `env:"password"` + AppSpecificPassword stepconf.Secret `env:"app_password"` + APIKeyPath stepconf.Secret `env:"api_key_path"` + APIIssuer string `env:"api_issuer"` + + UpdateFastlane bool `env:"update_fastlane,opt[true,false]"` + VerboseLog bool `env:"verbose_log,opt[yes,no]"` + EnableCache bool `env:"enable_cache,opt[yes,no]"` GemHome string `env:"GEM_HOME"` + + // Used to get Bitrise Apple Developer Portal Connection + BuildURL string `env:"BITRISE_BUILD_URL"` + BuildAPIToken stepconf.Secret `env:"BITRISE_BUILD_API_TOKEN"` +} + +func parseAuthSources(bitriseConnection string) ([]appleauth.Source, error) { + switch bitriseConnection { + case "automatic": + return []appleauth.Source{ + &appleauth.ConnectionAPIKeySource{}, + &appleauth.ConnectionAppleIDFastlaneSource{}, + &appleauth.InputAPIKeySource{}, + &appleauth.InputAppleIDFastlaneSource{}, + }, nil + case "api_key": + return []appleauth.Source{&appleauth.ConnectionAPIKeySource{}}, nil + case "apple_id": + return []appleauth.Source{&appleauth.ConnectionAppleIDFastlaneSource{}}, nil + case "off": + return []appleauth.Source{ + &appleauth.InputAPIKeySource{}, + &appleauth.InputAppleIDFastlaneSource{}, + }, nil + default: + return nil, fmt.Errorf("invalid connection input: %s", bitriseConnection) + } } func failf(format string, v ...interface{}) { @@ -57,31 +94,38 @@ func fastlaneDebugInfo(workDir string, useBundler bool, bundlerVersion gems.Vers log.Debugf("$ %s", cmd.PrintableCommandArgs()) if err := cmd.Run(); err != nil { - return "", fmt.Errorf("Fastlane command: (%s) failed", cmd.PrintableCommandArgs()) + if errorutil.IsExitStatusError(err) { + return "", fmt.Errorf("Fastlane command (%s) failed, output: %s", cmd.PrintableCommandArgs(), outBuffer.String()) + } + return "", fmt.Errorf("Fastlane command (%s) failed: %v", cmd.PrintableCommandArgs(), err) } return outBuffer.String(), nil } +func functionName(i interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() +} + +const notConnected = `Connected Apple Developer Portal Account not found. +Most likely because there is no Apple Developer Portal Account connected to the build. +Read more: https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/` + func handleSessionDataError(err error) { if err == nil { return } - if networkErr, ok := err.(devportalservice.NetworkError); ok && networkErr.Status == http.StatusNotFound { - log.Debugf("") - log.Debugf("Connected Apple Developer Portal Account not found") - log.Debugf("Most likely because there is no Apple Developer Portal Account connected to the build, or the build is running locally.") - log.Debugf("Read more: https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/") - } else { + if networkErr, ok := err.(devportalservice.NetworkError); ok && networkErr.Status == http.StatusUnauthorized { fmt.Println() - log.Errorf("Failed to activate Bitrise Apple Developer Portal connection: %s", err) - log.Warnf("Read more: https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/") + log.Warnf("%s", "Unauthorized to query Connected Apple Developer Portal Account. This happens by design, with a public app's PR build, to protect secrets.") + + return } -} -func functionName(i interface{}) string { - return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() + fmt.Println() + log.Errorf("Failed to activate Bitrise Apple Developer Portal connection: %s", err) + log.Warnf("Read more: https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/") } func main() { @@ -94,6 +138,22 @@ func main() { log.SetEnableDebugLog(config.VerboseLog) fmt.Println() + // Validate inputs + authInputs := appleauth.Inputs{ + Username: config.AppleID, + Password: string(config.Password), + AppSpecificPassword: string(config.AppSpecificPassword), + APIIssuer: config.APIIssuer, + APIKeyPath: string(config.APIKeyPath), + } + if err := authInputs.Validate(); err != nil { + failf("Issue with authentication related inputs: %v", err) + } + authSources, err := parseAuthSources(config.BitriseConnection) + if err != nil { + failf("Invalid Input: %v", err) + } + if strings.TrimSpace(config.GemHome) != "" { log.Warnf("Custom value (%s) is set for GEM_HOME environment variable. This can lead to errors as gem lookup path may not contain GEM_HOME.") } @@ -127,33 +187,33 @@ func main() { } } - // - // Fastlane session - fastlaneSession := "" - buildURL, buildAPIToken := os.Getenv("BITRISE_BUILD_URL"), os.Getenv("BITRISE_BUILD_API_TOKEN") - if buildURL != "" && buildAPIToken != "" { - var provider devportalservice.AppleDeveloperConnectionProvider - provider = devportalservice.NewBitriseClient(http.DefaultClient) - - conn, err := provider.GetAppleDeveloperConnection(buildURL, buildAPIToken) + // Select and fetch Apple authenication source + var devportalConnectionProvider *devportalservice.BitriseClient + if config.BuildURL != "" && config.BuildAPIToken != "" { + devportalConnectionProvider = devportalservice.NewBitriseClient(http.DefaultClient, config.BuildURL, string(config.BuildAPIToken)) + } else { + fmt.Println() + log.Warnf("Connected Apple Developer Portal Account not found. Step is not running on bitrise.io: BITRISE_BUILD_URL and BITRISE_BUILD_API_TOKEN envs are not set") + } + var conn *devportalservice.AppleDeveloperConnection + if config.BitriseConnection != "off" && devportalConnectionProvider != nil { + var err error + conn, err = devportalConnectionProvider.GetAppleDeveloperConnection() if err != nil { handleSessionDataError(err) } - if conn != nil && conn.AppleID != "" { + if conn != nil && (conn.APIKeyConnection == nil && conn.AppleIDConnection == nil) { fmt.Println() - log.Infof("Connected session-based Apple Developer Portal Account found") - - if expiry := conn.Expiry(); expiry != nil && conn.Expired() { - log.Warnf("Connection expired on %s", expiry.String()) - } else if session, err := conn.FastlaneLoginSession(); err != nil { - handleSessionDataError(err) - } else { - fastlaneSession = session - } + log.Warnf("%s", notConnected) + } + } + + authConfig, err := appleauth.Select(conn, authSources, authInputs) + if err != nil { + if _, ok := err.(*appleauth.MissingAuthConfigError); !ok { + failf("Could not configure Apple Service authentication: %v", err) } - } else { - log.Warnf("Step is not running on bitrise.io: BITRISE_BUILD_URL and BITRISE_BUILD_API_TOKEN envs are not set") } // Split lane option @@ -262,6 +322,24 @@ func main() { fmt.Println() log.Infof("Run Fastlane") + var envs []string + authEnvs, err := FastlaneAuthParams(authConfig) + if err != nil { + failf("Failed to set up Fastlane authentication paramteres: %v", err) + } + var globallySetAuthEnvs []string + for envKey, envValue := range authEnvs { + if _, set := os.LookupEnv(envKey); set { + globallySetAuthEnvs = append(globallySetAuthEnvs, envKey) + } + + envs = append(envs, fmt.Sprintf("%s=%s", envKey, envValue)) + } + if len(globallySetAuthEnvs) != 0 { + log.Warnf("Fastlane authentication-related environment varibale(s) (%s) are set, overriding.", globallySetAuthEnvs) + log.Infof("To stop overriding authentication-related environment variables, please set Bitrise Apple Developer Connection input to 'off' and leave authentication-related inputs empty.") + } + fastlaneCmd := []string{"fastlane"} fastlaneCmd = append(fastlaneCmd, laneOptions...) if useBundler { @@ -275,11 +353,6 @@ func main() { failf("Failed to create command model, error: %s", err) } - envs := []string{} - if fastlaneSession != "" { - envs = append(envs, "FASTLANE_SESSION="+fastlaneSession) - } - cmd.SetStdout(os.Stdout).SetStderr(os.Stderr) cmd.SetDir(workDir) diff --git a/step.yml b/step.yml index 2d87567..7e97420 100644 --- a/step.yml +++ b/step.yml @@ -58,6 +58,63 @@ inputs: * If the Fastfile path is `./here/is/my/fastlane/Fastfile` * Then the Fastfile's directory is `./here/is/my/fastlane` * So the Working Directory should be `./here/is/my` + - connection: "automatic" + opts: + title: Bitrise Apple Developer Connection + summary: The Apple Service authentication method the Step uses. By default, any enabled Bitrise Apple Developer connection is used and other authentication-related Step inputs are ignored. + description: |- + The input determines the method used for Apple Service authentication. By default, any enabled Bitrise Apple Developer connection is used and other authentication-related Step inputs are ignored. + + There are two types of Apple Developer connection you can enable on Bitrise: one is based on an API key of the App Store Connect API, the other is the session-based authentication with an Apple ID. You can choose which type of Bitrise Apple Developer connection to use or you can tell the Step to only use the Step inputs for authentication: + - `automatic`: Use any enabled Apple Developer connection, either based on Apple ID authentication or API key authentication. Step inputs are only used as a fallback. API key authentication has priority over Apple ID authentication in both cases. + - `api_key`: Use the Apple Developer connection based on API key authentication. Authentication-related Step inputs are ignored. + - `apple_id`: Use the Apple Developer connection based on Apple ID authentication and **Application-specific password** Step input. Other authentication-related Step inputs are ignored. + - `off`: Do not use any already configured Apple Developer Connection. Only authentication-related Step inputs are considered. + is_required: true + value_options: + - "automatic" + - "api_key" + - "apple_id" + - "off" + - api_key_path: "" + opts: + title: "API Key: URL" + summary: Path to local or remote file + description: |- + Specify the path in an URL format where your API key is stored. + For example: `https://URL/TO/AuthKey_something.p8` or `file:///PATH/TO/AuthKey_something.p8`. + + + You can upload your key on the **Generic File Storage** tab in the Workflow Editor and set the Environment Variable for the file here. + + + For example: `$BITRISEIO_MYKEY_URL` + - api_issuer: "" + opts: + title: "API Key: Issuer ID" + description: |- + Issuer ID. Required if **API Key: URL** (`api_key_path`) is specified. + - apple_id: "" + opts: + title: "Apple ID: Email" + summary: Email for Apple ID login. + description: Email for Apple ID login. + is_sensitive: true + - password: "" + opts: + title: "Apple ID: Password" + description: Password for the specified Apple ID. + is_sensitive: true + - app_password: "" + opts: + title: "Apple ID: Application-specific password" + summary: Required if using Apple ID + description: |- + An application-specific password for the Apple ID. + **NOTE:** Application-specific passwords can be created on the + [AppleID Website](https://appleid.apple.com). It can be used to + bypass two-factor authentication. + is_sensitive: true - update_fastlane: "true" opts: title: "Should update fastlane gem before run?" diff --git a/steps-fastlane b/steps-fastlane new file mode 100755 index 0000000..fa9d6e9 Binary files /dev/null and b/steps-fastlane differ diff --git a/vendor/github.com/bitrise-io/go-steputils/stepconf/stepconf.go b/vendor/github.com/bitrise-io/go-steputils/stepconf/stepconf.go index 027095b..f891923 100644 --- a/vendor/github.com/bitrise-io/go-steputils/stepconf/stepconf.go +++ b/vendor/github.com/bitrise-io/go-steputils/stepconf/stepconf.go @@ -56,7 +56,19 @@ func (s Secret) String() string { // Print the name of the struct with Title case in blue color with followed by a newline, // then print all fields formatted as '- field name: field value` separated by newline. func Print(config interface{}) { - fmt.Printf(toString(config)) + fmt.Print(toString(config)) +} + +func valueString(v reflect.Value) string { + if v.Kind() != reflect.Ptr { + return fmt.Sprintf("%v", v.Interface()) + } + + if !v.IsNil() { + return fmt.Sprintf("%v", v.Elem().Interface()) + } + + return "" } // returns the name of the struct with Title case in blue color followed by a newline, @@ -75,7 +87,7 @@ func toString(config interface{}) string { str := fmt.Sprintf(colorstring.Bluef("%s:\n", strings.Title(t.Name()))) for i := 0; i < t.NumField(); i++ { - str += fmt.Sprintf("- %s: %v\n", t.Field(i).Name, v.Field(i).Interface()) + str += fmt.Sprintf("- %s: %s\n", t.Field(i).Name, valueString(v.Field(i))) } return str @@ -137,6 +149,14 @@ func setField(field reflect.Value, value, constraint string) error { return nil } + if field.Kind() == reflect.Ptr { + // If field is a pointer type, then set its value to be a pointer to a new zero value, matching field underlying type. + var dePtrdType = field.Type().Elem() // get the type field can point to + var newPtrType = reflect.New(dePtrdType) // create new ptr address for type with non-nil zero value + field.Set(newPtrType) // assign value to pointer + field = field.Elem() + } + switch field.Kind() { case reflect.String: field.SetString(value) diff --git a/vendor/github.com/bitrise-io/go-utils/errorutil/errorutil.go b/vendor/github.com/bitrise-io/go-utils/errorutil/errorutil.go new file mode 100644 index 0000000..dd5f8d2 --- /dev/null +++ b/vendor/github.com/bitrise-io/go-utils/errorutil/errorutil.go @@ -0,0 +1,32 @@ +// Package errorutil ... +package errorutil + +import ( + "os/exec" + "regexp" +) + +func exitCode(err error) int { + if exitError, ok := err.(*exec.ExitError); ok { + return exitError.ProcessState.ExitCode() + } + return -1 +} + +// IsExitStatusError ... +func IsExitStatusError(err error) bool { + return exitCode(err) != -1 +} + +// IsExitStatusErrorStr ... +func IsExitStatusErrorStr(errString string) bool { + // https://golang.org/src/os/exec_posix.go?s=2421:2459#L87 + // example exit status error string: exit status 1 + var rex = regexp.MustCompile(`^exit status [0-9]{1,3}$`) + return rex.MatchString(errString) +} + +// CmdExitCodeFromError ... +func CmdExitCodeFromError(err error) (int, error) { + return exitCode(err), err +} diff --git a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/auth_source.go b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/auth_source.go new file mode 100644 index 0000000..71d11aa --- /dev/null +++ b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/auth_source.go @@ -0,0 +1,176 @@ +package appleauth + +import ( + "fmt" + + "github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice" +) + +// Source returns a specific kind (Apple ID/API Key) Apple authentication data from a specific source (Bitrise Apple Developer Connection, Step inputs) +type Source interface { + Fetch(connection *devportalservice.AppleDeveloperConnection, inputs Inputs) (*Credentials, error) + Description() string +} + +// ConnectionAPIKeySource provides API Key from Bitrise Apple Developer Connection +type ConnectionAPIKeySource struct{} + +// InputAPIKeySource provides API Key from Step inputs +type InputAPIKeySource struct{} + +// ConnectionAppleIDSource provides Apple ID from Bitrise Apple Developer Connection +type ConnectionAppleIDSource struct{} + +// InputAppleIDSource provides Apple ID from Step inputs +type InputAppleIDSource struct{} + +// ConnectionAppleIDFastlaneSource provides Apple ID from Bitrise Apple Developer Connection, includes Fastlane specific session +type ConnectionAppleIDFastlaneSource struct{} + +// InputAppleIDFastlaneSource provides Apple ID from Step inputs, includes Fastlane specific session +type InputAppleIDFastlaneSource struct{} + +// Description ... +func (*ConnectionAPIKeySource) Description() string { + return "Bitrise Apple Developer Connection with API key found" +} + +// Fetch ... +func (*ConnectionAPIKeySource) Fetch(conn *devportalservice.AppleDeveloperConnection, inputs Inputs) (*Credentials, error) { + if conn == nil || conn.APIKeyConnection == nil { // Not configured + return nil, nil + } + + return &Credentials{ + APIKey: conn.APIKeyConnection, + }, nil +} + +// + +// Description ... +func (*InputAPIKeySource) Description() string { + return "Inputs with API key authentication found" +} + +// Fetch ... +func (*InputAPIKeySource) Fetch(conn *devportalservice.AppleDeveloperConnection, inputs Inputs) (*Credentials, error) { + if inputs.APIKeyPath == "" { // Not configured + return nil, nil + } + + privateKey, keyID, err := fetchPrivateKey(inputs.APIKeyPath) + if err != nil { + return nil, fmt.Errorf("could not fetch private key (%s) specified as input: %v", inputs.APIKeyPath, err) + } + if len(privateKey) == 0 { + return nil, fmt.Errorf("private key (%s) is empty", inputs.APIKeyPath) + } + + return &Credentials{ + APIKey: &devportalservice.APIKeyConnection{ + IssuerID: inputs.APIIssuer, + KeyID: keyID, + PrivateKey: string(privateKey), + }, + }, nil +} + +// + +// Description ... +func (*ConnectionAppleIDSource) Description() string { + return "Bitrise Apple Developer Connection with Apple ID found." +} + +// Fetch ... +func (*ConnectionAppleIDSource) Fetch(conn *devportalservice.AppleDeveloperConnection, inputs Inputs) (*Credentials, error) { + if conn == nil || conn.AppleIDConnection == nil { // No Apple ID configured + return nil, nil + } + + return &Credentials{ + AppleID: &AppleID{ + Username: conn.AppleIDConnection.AppleID, + Password: conn.AppleIDConnection.Password, + Session: "", + AppSpecificPassword: inputs.AppSpecificPassword, + }, + }, nil +} + +// + +// Description ... +func (*InputAppleIDSource) Description() string { + return "Inputs with Apple ID authentication found." +} + +// Fetch ... +func (*InputAppleIDSource) Fetch(conn *devportalservice.AppleDeveloperConnection, inputs Inputs) (*Credentials, error) { + if inputs.Username == "" { // Not configured + return nil, nil + } + + return &Credentials{ + AppleID: &AppleID{ + Username: inputs.Username, + Password: inputs.Password, + AppSpecificPassword: inputs.AppSpecificPassword, + }, + }, nil +} + +// + +// Description ... +func (*ConnectionAppleIDFastlaneSource) Description() string { + return "Bitrise Apple Developer Connection with Apple ID found." +} + +// Fetch ... +func (*ConnectionAppleIDFastlaneSource) Fetch(conn *devportalservice.AppleDeveloperConnection, inputs Inputs) (*Credentials, error) { + if conn == nil || conn.AppleIDConnection == nil { // No Apple ID configured + return nil, nil + } + + appleIDConn := conn.AppleIDConnection + if expiry := appleIDConn.Expiry(); expiry != nil && appleIDConn.Expired() { + return nil, fmt.Errorf("2FA session saved in Bitrise Developer Connection is expired, was valid until %s", expiry.String()) + } + session, err := appleIDConn.FastlaneLoginSession() + if err != nil { + return nil, fmt.Errorf("could not prepare Fastlane session cookie object: %v", err) + } + + return &Credentials{ + AppleID: &AppleID{ + Username: conn.AppleIDConnection.AppleID, + Password: conn.AppleIDConnection.Password, + Session: session, + AppSpecificPassword: inputs.AppSpecificPassword, + }, + }, nil +} + +// + +// Description ... +func (*InputAppleIDFastlaneSource) Description() string { + return "Inputs with Apple ID authentication found. This method does not support TFA enabled Apple IDs." +} + +// Fetch ... +func (*InputAppleIDFastlaneSource) Fetch(conn *devportalservice.AppleDeveloperConnection, inputs Inputs) (*Credentials, error) { + if inputs.Username == "" { // Not configured + return nil, nil + } + + return &Credentials{ + AppleID: &AppleID{ + Username: inputs.Username, + Password: inputs.Password, + AppSpecificPassword: inputs.AppSpecificPassword, + }, + }, nil +} diff --git a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/fetch.go b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/fetch.go new file mode 100644 index 0000000..3858946 --- /dev/null +++ b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/fetch.go @@ -0,0 +1,60 @@ +package appleauth + +import ( + "fmt" + + "github.com/bitrise-io/go-utils/log" + "github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice" +) + +// Credentials contains either Apple ID or APIKey auth info +type Credentials struct { + AppleID *AppleID + APIKey *devportalservice.APIKeyConnection +} + +// AppleID contains Apple ID auth info +// +// Without 2FA: +// Required: username, password +// With 2FA: +// Required: username, password, appSpecificPassword +// session (Only for Fastlane, set as FASTLANE_SESSION) +// +// As Fastlane spaceship uses: +// - iTMSTransporter: it requires Username + Password (or App-specific password with 2FA) +// - TunesAPI: it requires Username + Password (+ 2FA session with 2FA) +type AppleID struct { + Username, Password string + Session, AppSpecificPassword string +} + +// MissingAuthConfigError is returned in case no usable Apple App Store Connect / Developer Portal authenticaion is found +type MissingAuthConfigError struct { +} + +func (*MissingAuthConfigError) Error() string { + return "no credentials provided" +} + +// Select return valid Apple ID or API Key based authentication data, from the provided Bitrise Apple Developer Connection or Inputs +// authSources: required, array of checked sources (in order, the first set one will be used) +// for example: []AppleAuthSource{&SourceConnectionAPIKey{}, &SourceConnectionAppleID{}, &SourceInputAPIKey{}, &SourceInputAppleID{}} +// inputs: optional, user provided inputs that are not centrally managed (by setting up connections) +func Select(conn *devportalservice.AppleDeveloperConnection, authSources []Source, inputs Inputs) (Credentials, error) { + for _, source := range authSources { + auth, err := source.Fetch(conn, inputs) + if err != nil { + return Credentials{}, err + } + + if auth != nil { + fmt.Println() + log.Infof("%s", source.Description()) + + return *auth, nil + } + } + + return Credentials{}, &MissingAuthConfigError{} +} diff --git a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/inputs.go b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/inputs.go new file mode 100644 index 0000000..34fbaea --- /dev/null +++ b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/inputs.go @@ -0,0 +1,57 @@ +package appleauth + +import ( + "fmt" + "strings" + + "github.com/bitrise-io/go-utils/log" +) + +// Inputs is Apple Service authentication configuration provided by end user +type Inputs struct { + // Apple ID + Username, Password, AppSpecificPassword string + // API key (JWT) + APIIssuer, APIKeyPath string +} + +// Validate trims extra spaces and checks input grouping +func (cfg *Inputs) Validate() error { + cfg.APIIssuer = strings.TrimSpace(cfg.APIIssuer) + cfg.APIKeyPath = strings.TrimSpace(cfg.APIKeyPath) + cfg.Username = strings.TrimSpace(cfg.Username) + cfg.AppSpecificPassword = strings.TrimSpace(cfg.AppSpecificPassword) + var ( + isAPIKeyAuthType = (cfg.APIKeyPath != "" || cfg.APIIssuer != "") + isAppleIDAuthType = (cfg.AppSpecificPassword != "" || cfg.Username != "" || cfg.Password != "") + ) + + switch { + case isAppleIDAuthType && isAPIKeyAuthType: + log.Warnf("Either provide Apple ID, Password (and App-specific password if available) OR API Key Path and API Issuer") + return fmt.Errorf("both Apple ID and API key related configuration provided, but only one of them expected") + + case isAppleIDAuthType: + if cfg.AppSpecificPassword != "" { + // App Specific Password provided, assuming 2FA is enabled. + // In this case 2FA session is required, configured Bitrise account connection required, this contains username+password + break + } + if cfg.Username == "" { + return fmt.Errorf("no Apple Service Apple ID provided") + } + if cfg.Password == "" { + return fmt.Errorf("no Apple Service Password provided") + } + + case isAPIKeyAuthType: + if cfg.APIIssuer == "" { + return fmt.Errorf("no Apple Service API Issuer provided") + } + if cfg.APIKeyPath == "" { + return fmt.Errorf("no Apple Service API Key Path provided") + } + } + + return nil +} diff --git a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/key_helper.go b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/key_helper.go new file mode 100644 index 0000000..4d7e9c2 --- /dev/null +++ b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth/key_helper.go @@ -0,0 +1,71 @@ +package appleauth + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "path/filepath" + "regexp" + + "github.com/bitrise-io/go-utils/log" +) + +func fetchPrivateKey(privateKeyURL string) ([]byte, string, error) { + fileURL, err := url.Parse(privateKeyURL) + if err != nil { + return nil, "", err + } + + key, err := copyOrDownloadFile(fileURL) + if err != nil { + return nil, "", err + } + + return key, getKeyID(fileURL), nil +} + +func copyOrDownloadFile(u *url.URL) ([]byte, error) { + // if file -> copy + if u.Scheme == "file" { + b, err := ioutil.ReadFile(u.Path) + if err != nil { + return nil, err + } + + return b, err + } + + // otherwise download + resp, err := http.Get(u.String()) + if err != nil { + return nil, err + } + defer func() { + if err := resp.Body.Close(); err != nil { + log.Errorf("Failed to close file: %s", err) + } + }() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("request failed with status %d", resp.StatusCode) + } + + contentBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %s", err) + } + + return contentBytes, nil +} + +func getKeyID(u *url.URL) string { + var keyID = "Bitrise" // as default if no ID found in file name + + // get the ID of the key from the file + if matches := regexp.MustCompile(`AuthKey_(.+)\.p8`).FindStringSubmatch(filepath.Base(u.Path)); len(matches) == 2 { + keyID = matches[1] + } + + return keyID +} diff --git a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/devportalservice.go b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/devportalservice.go index 024337e..6ecc851 100644 --- a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/devportalservice.go +++ b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/devportalservice.go @@ -10,73 +10,127 @@ import ( "text/template" "time" + "github.com/bitrise-io/go-utils/fileutil" "github.com/bitrise-io/go-utils/log" ) -const ( - bitriseBuildURLKey = "BITRISE_BUILD_URL" - bitriseBuildAPITokenKey = "BITRISE_BUILD_API_TOKEN" -) - type httpClient interface { Do(req *http.Request) (*http.Response, error) } // AppleDeveloperConnectionProvider ... type AppleDeveloperConnectionProvider interface { - GetAppleDeveloperConnection(buildURL, buildAPIToken string) (*AppleDeveloperConnection, error) + GetAppleDeveloperConnection() (*AppleDeveloperConnection, error) } // BitriseClient implements AppleDeveloperConnectionProvider through the Bitrise.io API. type BitriseClient struct { - httpClient httpClient + httpClient httpClient + buildURL, buildAPIToken string + + readBytesFromFile func(pth string) ([]byte, error) } // NewBitriseClient creates a new instance of BitriseClient. -func NewBitriseClient(client httpClient) *BitriseClient { +func NewBitriseClient(client httpClient, buildURL, buildAPIToken string) *BitriseClient { return &BitriseClient{ - httpClient: client, + httpClient: client, + buildURL: buildURL, + buildAPIToken: buildAPIToken, + readBytesFromFile: fileutil.ReadBytesFromFile, } } const appleDeveloperConnectionPath = "apple_developer_portal_data.json" -// GetAppleDeveloperConnection fetches the Bitrise.io session-based Apple Developer connection. -func (c *BitriseClient) GetAppleDeveloperConnection(buildURL, buildAPIToken string) (*AppleDeveloperConnection, error) { - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", buildURL, appleDeveloperConnectionPath), nil) +func privateKeyWithHeader(privateKey string) string { + if strings.HasPrefix(privateKey, "-----BEGIN PRIVATE KEY----") { + return privateKey + } + + return fmt.Sprint( + "-----BEGIN PRIVATE KEY-----\n", + privateKey, + "\n-----END PRIVATE KEY-----", + ) +} + +// GetAppleDeveloperConnection fetches the Bitrise.io Apple Developer connection. +func (c *BitriseClient) GetAppleDeveloperConnection() (*AppleDeveloperConnection, error) { + var rawCreds []byte + var err error + + if strings.HasPrefix(c.buildURL, "file://") { + rawCreds, err = c.readBytesFromFile(strings.TrimPrefix(c.buildURL, "file://")) + } else { + rawCreds, err = c.download() + } if err != nil { - return nil, err + return nil, fmt.Errorf("failed to fetch authentication credentials: %v", err) + } + + type data struct { + *AppleIDConnection + *APIKeyConnection + TestDevices []TestDevice `json:"test_devices"` + } + var d data + if err := json.Unmarshal([]byte(rawCreds), &d); err != nil { + return nil, fmt.Errorf("failed to unmarshal authentication credentials from response (%s): %s", rawCreds, err) + } + + if d.APIKeyConnection != nil { + if d.APIKeyConnection.IssuerID == "" { + return nil, fmt.Errorf("invalid authentication credentials, empty issuer_id in response (%s)", rawCreds) + } + if d.APIKeyConnection.KeyID == "" { + return nil, fmt.Errorf("invalid authentication credentials, empty key_id in response (%s)", rawCreds) + } + if d.APIKeyConnection.PrivateKey == "" { + return nil, fmt.Errorf("invalid authentication credentials, empty private_key in response (%s)", rawCreds) + } + + d.APIKeyConnection.PrivateKey = privateKeyWithHeader(d.APIKeyConnection.PrivateKey) } - req.Header.Add("BUILD_API_TOKEN", buildAPIToken) + + return &AppleDeveloperConnection{ + AppleIDConnection: d.AppleIDConnection, + APIKeyConnection: d.APIKeyConnection, + TestDevices: d.TestDevices, + }, nil +} + +func (c *BitriseClient) download() ([]byte, error) { + url := fmt.Sprintf("%s/%s", c.buildURL, appleDeveloperConnectionPath) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request for URL (%s): %s", url, err) + } + req.Header.Add("BUILD_API_TOKEN", c.buildAPIToken) resp, err := c.httpClient.Do(req) if err != nil { // On error, any Response can be ignored - return nil, fmt.Errorf("failed to perform request, error: %s", err) + return nil, fmt.Errorf("failed to perform request: %s", err) } // The client must close the response body when finished with it defer func() { if cerr := resp.Body.Close(); cerr != nil { - log.Warnf("Failed to close response body, error: %s", cerr) + log.Warnf("Failed to close response body: %s", cerr) } }() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body, error: %s", err) + return nil, fmt.Errorf("failed to read response body: %s", err) } if resp.StatusCode != http.StatusOK { - return nil, NetworkError{Status: resp.StatusCode, Body: string(body)} + return nil, NetworkError{Status: resp.StatusCode} } - var connection AppleDeveloperConnection - if err := json.Unmarshal([]byte(body), &connection); err != nil { - return nil, fmt.Errorf("failed to unmarshal response (%s), error: %s", body, err) - } - - return &connection, nil + return body, nil } type cookie struct { @@ -91,27 +145,52 @@ type cookie struct { ForDomain *bool `json:"for_domain,omitempty"` } -// AppleDeveloperConnection represents a Bitrise.io session-based Apple Developer connection. -// https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/ -type AppleDeveloperConnection struct { +// AppleIDConnection represents a Bitrise.io Apple ID-based Apple Developer connection. +type AppleIDConnection struct { AppleID string `json:"apple_id"` Password string `json:"password"` ConnectionExpiryDate string `json:"connection_expiry_date"` SessionCookies map[string][]cookie `json:"session_cookies"` } -// Expiry returns the expiration of the Bitrise session-based Apple Developer connection. -func (c *AppleDeveloperConnection) Expiry() *time.Time { +// APIKeyConnection represents a Bitrise.io API key-based Apple Developer connection. +type APIKeyConnection struct { + KeyID string `json:"key_id"` + IssuerID string `json:"issuer_id"` + PrivateKey string `json:"private_key"` +} + +// TestDevice ... +type TestDevice struct { + ID int `json:"id"` + UserID int `json:"user_id"` + DeviceID string `json:"device_identifier"` + Title string `json:"title"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + DeviceType string `json:"device_type"` +} + +// AppleDeveloperConnection represents a Bitrise.io Apple Developer connection. +// https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/ +type AppleDeveloperConnection struct { + AppleIDConnection *AppleIDConnection + APIKeyConnection *APIKeyConnection + TestDevices []TestDevice `json:"test_devices"` +} + +// Expiry returns the expiration of the Bitrise Apple ID-based Apple Developer connection. +func (c *AppleIDConnection) Expiry() *time.Time { t, err := time.Parse(time.RFC3339, c.ConnectionExpiryDate) if err != nil { - log.Warnf("Could not parse session-based connection expiry date: %s", err) + log.Warnf("Could not parse Apple ID session expiry date: %s", err) return nil } return &t } -// Expired returns whether the Bitrise session-based Apple Developer connection is expired. -func (c *AppleDeveloperConnection) Expired() bool { +// Expired returns whether the Bitrise Apple ID-based Apple Developer connection is expired. +func (c *AppleIDConnection) Expired() bool { expiry := c.Expiry() if expiry == nil { return false @@ -121,7 +200,7 @@ func (c *AppleDeveloperConnection) Expired() bool { // FastlaneLoginSession returns the Apple ID login session in a ruby/object:HTTP::Cookie format. // The session can be used as a value for FASTLANE_SESSION environment variable: https://docs.fastlane.tools/best-practices/continuous-integration/#two-step-or-two-factor-auth. -func (c *AppleDeveloperConnection) FastlaneLoginSession() (string, error) { +func (c *AppleIDConnection) FastlaneLoginSession() (string, error) { var rubyCookies []string for _, cookie := range c.SessionCookies["https://idmsa.apple.com"] { if rubyCookies == nil { diff --git a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/devportalservice_testdata.go b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/devportalservice_testdata.go index c0f37f0..e189837 100644 --- a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/devportalservice_testdata.go +++ b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/devportalservice_testdata.go @@ -24,24 +24,34 @@ const testDevicesResponseBody = `{ } ` -const testAppleDevConnSession = `--- -- !ruby/object:HTTP::Cookie - name: DES58b0eba556d80ed2b98707e15ffafd344 - value: HSARMTKNSRVTWFlaFrGQTmfmFBwJuiX/aaaaaaaaa+A7FbJa4V8MmWijnJknnX06ME0KrI9V8vFg==SRVT - domain: idmsa.apple.com - for_domain: true - path: "/" - -- !ruby/object:HTTP::Cookie - name: myacinfo - value: DAWTKNV26a0a6db3ae43acd203d0d03e8bc45000cd4bdc668e90953f22ca3b36eaab0e18634660a10cf28cc65d8ddf633c017de09477dfb18c8a3d6961f96cbbf064be616e80cee62d3d7f39a485bf826377c5b5dbbfc4a97dcdb462052db73a3a1d9b4a325d5bdd496190b3088878cecce17e4d6db9230e0575cfbe7a8754d1de0c937080ef84569b6e4a75237c2ec01cf07db060a11d92e7220707dd00a2a565ee9e06074d8efa6a1b7f83db3e1b2acdafb5fc0708443e77e6d71e168ae2a83b848122264b2da5cadfd9e451f9fe3f6eebc71904d4bc36acc528cc2a844d4f2eb527649a69523756ec9955457f704c28a3b6b9f97d6df900bd60044d5bc50408260f096954f03c53c16ac40a796dc439b859f882a50390b1c7517a9f4479fb1ce9ba2db241d6b8f2eb127c46ef96e0ccccccccc - domain: apple.com - for_domain: true - path: "/" +var testDevices = []TestDevice{ + { + ID: 24, + UserID: 4, + DeviceID: "asdf12345ad9b298cb9a9f28555c49573d8bc322", + Title: "iPhone 6", + CreatedAt: "2015-03-13T16:16:13.665Z", + UpdatedAt: "2015-03-13T16:16:13.665Z", + DeviceType: "ios", + }, + { + ID: 28, + UserID: 4, + DeviceID: "asdf12341e73b76df6e99d0d713133c3e078418f", + Title: "iPad mini 2 (Wi-Fi)", + CreatedAt: "2015-03-19T13:25:43.487Z", + UpdatedAt: "2015-03-19T13:25:43.487Z", + DeviceType: "ios", + }, +} -` +var testConnectionOnlyDevices = AppleDeveloperConnection{ + AppleIDConnection: nil, + APIKeyConnection: nil, + TestDevices: testDevices, +} -const testAppleDevConnDataJSON = `{ +const testAppleIDConnectionResponseBody = `{ "apple_id": "example@example.io", "password": "highSecurityPassword", "connection_expiry_date": "2019-04-06T12:04:59.000Z", @@ -66,22 +76,27 @@ const testAppleDevConnDataJSON = `{ "httponly": true } ] - }, - "test_devices": [ - { - "id": 8414, - "user_id": 52411, - "device_identifier": "1b78ac4bad2e8911139287ac5dd152fbe86eb2b9", - "title": "iPhone 7", - "created_at": "2018-08-30T09:09:36.332Z", - "updated_at": "2018-08-30T09:09:36.332Z", - "device_type": "ios" - } - ], - "default_team_id": null + } }` -var testAppleDevConnData = AppleDeveloperConnection{ +const testFastlaneSession = `--- +- !ruby/object:HTTP::Cookie + name: DES58b0eba556d80ed2b98707e15ffafd344 + value: HSARMTKNSRVTWFlaFrGQTmfmFBwJuiX/aaaaaaaaa+A7FbJa4V8MmWijnJknnX06ME0KrI9V8vFg==SRVT + domain: idmsa.apple.com + for_domain: true + path: "/" + +- !ruby/object:HTTP::Cookie + name: myacinfo + value: DAWTKNV26a0a6db3ae43acd203d0d03e8bc45000cd4bdc668e90953f22ca3b36eaab0e18634660a10cf28cc65d8ddf633c017de09477dfb18c8a3d6961f96cbbf064be616e80cee62d3d7f39a485bf826377c5b5dbbfc4a97dcdb462052db73a3a1d9b4a325d5bdd496190b3088878cecce17e4d6db9230e0575cfbe7a8754d1de0c937080ef84569b6e4a75237c2ec01cf07db060a11d92e7220707dd00a2a565ee9e06074d8efa6a1b7f83db3e1b2acdafb5fc0708443e77e6d71e168ae2a83b848122264b2da5cadfd9e451f9fe3f6eebc71904d4bc36acc528cc2a844d4f2eb527649a69523756ec9955457f704c28a3b6b9f97d6df900bd60044d5bc50408260f096954f03c53c16ac40a796dc439b859f882a50390b1c7517a9f4479fb1ce9ba2db241d6b8f2eb127c46ef96e0ccccccccc + domain: apple.com + for_domain: true + path: "/" + +` + +var testAppleIDConnection = AppleIDConnection{ AppleID: "example@example.io", Password: "highSecurityPassword", ConnectionExpiryDate: "2019-04-06T12:04:59.000Z", @@ -108,3 +123,85 @@ var testAppleDevConnData = AppleDeveloperConnection{ }, }, } + +var testConnectionWithAppleIDConnection = AppleDeveloperConnection{ + AppleIDConnection: &testAppleIDConnection, + APIKeyConnection: nil, + TestDevices: nil, +} + +const testAPIKeyConnectionResponseBody = `{ + "key_id": "ASDF4H9LNQ", + "issuer_id": "asdf1234-7325-47e3-e053-5b8c7c11a4d1", + "private_key": "-----BEGIN PRIVATE KEY-----\nASdf1234MBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg9O4G/HVLgSqc2i7x\nasDF12346UNzKCEwOfQ1ixC0G9agCgYIKoZIzj0DAQehRANCAARcJQItGFcefLRc\naSDf1234ka9BMpRjjr3NWyCWl817HCdXXckuc22RjnKxRnYMBBDv8zPDX0k9TbST\nacgZ04Gg\n-----END PRIVATE KEY-----" +}` + +var testAPIKeyConnection = APIKeyConnection{ + KeyID: "ASDF4H9LNQ", + IssuerID: "asdf1234-7325-47e3-e053-5b8c7c11a4d1", + PrivateKey: "-----BEGIN PRIVATE KEY-----\nASdf1234MBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg9O4G/HVLgSqc2i7x\nasDF12346UNzKCEwOfQ1ixC0G9agCgYIKoZIzj0DAQehRANCAARcJQItGFcefLRc\naSDf1234ka9BMpRjjr3NWyCWl817HCdXXckuc22RjnKxRnYMBBDv8zPDX0k9TbST\nacgZ04Gg\n-----END PRIVATE KEY-----", +} + +var testConnectionWithAPIKeyConnection = AppleDeveloperConnection{ + AppleIDConnection: nil, + APIKeyConnection: &testAPIKeyConnection, + TestDevices: nil, +} + +const testAppleIDAndAPIKeyConnectionResponseBody = `{ + "apple_id": "example@example.io", + "password": "highSecurityPassword", + "connection_expiry_date": "2019-04-06T12:04:59.000Z", + "session_cookies": { + "https://idmsa.apple.com": [ + { + "name": "DES58b0eba556d80ed2b98707e15ffafd344", + "path": "/", + "value": "HSARMTKNSRVTWFlaFrGQTmfmFBwJuiX/aaaaaaaaa+A7FbJa4V8MmWijnJknnX06ME0KrI9V8vFg==SRVT", + "domain": "idmsa.apple.com", + "secure": true, + "expires": "2019-04-06T12:04:59Z", + "max_age": 2592000, + "httponly": true + }, + { + "name": "myacinfo", + "path": "/", + "value": "DAWTKNV26a0a6db3ae43acd203d0d03e8bc45000cd4bdc668e90953f22ca3b36eaab0e18634660a10cf28cc65d8ddf633c017de09477dfb18c8a3d6961f96cbbf064be616e80cee62d3d7f39a485bf826377c5b5dbbfc4a97dcdb462052db73a3a1d9b4a325d5bdd496190b3088878cecce17e4d6db9230e0575cfbe7a8754d1de0c937080ef84569b6e4a75237c2ec01cf07db060a11d92e7220707dd00a2a565ee9e06074d8efa6a1b7f83db3e1b2acdafb5fc0708443e77e6d71e168ae2a83b848122264b2da5cadfd9e451f9fe3f6eebc71904d4bc36acc528cc2a844d4f2eb527649a69523756ec9955457f704c28a3b6b9f97d6df900bd60044d5bc50408260f096954f03c53c16ac40a796dc439b859f882a50390b1c7517a9f4479fb1ce9ba2db241d6b8f2eb127c46ef96e0ccccccccc", + "domain": "apple.com", + "secure": true, + "httponly": true + } + ] + }, + "key_id": "ASDF4H9LNQ", + "issuer_id": "asdf1234-7325-47e3-e053-5b8c7c11a4d1", + "private_key": "-----BEGIN PRIVATE KEY-----\nASdf1234MBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg9O4G/HVLgSqc2i7x\nasDF12346UNzKCEwOfQ1ixC0G9agCgYIKoZIzj0DAQehRANCAARcJQItGFcefLRc\naSDf1234ka9BMpRjjr3NWyCWl817HCdXXckuc22RjnKxRnYMBBDv8zPDX0k9TbST\nacgZ04Gg\n-----END PRIVATE KEY-----", + "test_devices":[ + { + "id":24, + "user_id":4, + "device_identifier":"asdf12345ad9b298cb9a9f28555c49573d8bc322", + "title":"iPhone 6", + "created_at":"2015-03-13T16:16:13.665Z", + "updated_at":"2015-03-13T16:16:13.665Z", + "device_type":"ios" + }, + { + "id":28, + "user_id":4, + "device_identifier":"asdf12341e73b76df6e99d0d713133c3e078418f", + "title":"iPad mini 2 (Wi-Fi)", + "created_at":"2015-03-19T13:25:43.487Z", + "updated_at":"2015-03-19T13:25:43.487Z", + "device_type":"ios" + } + ] +} +` + +var testConnectionWithAppleIDAndAPIKeyConnection = AppleDeveloperConnection{ + AppleIDConnection: &testAppleIDConnection, + APIKeyConnection: &testAPIKeyConnection, + TestDevices: testDevices, +} diff --git a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/errors.go b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/errors.go index b487980..c6243a7 100644 --- a/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/errors.go +++ b/vendor/github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice/errors.go @@ -5,9 +5,8 @@ import "fmt" // NetworkError represents a networking issue. type NetworkError struct { Status int - Body string } func (e NetworkError) Error() string { - return fmt.Sprintf("response %d %s", e.Status, e.Body) + return fmt.Sprintf("network request failed with status %d", e.Status) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 4b8a6c3..bcb99d6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -9,7 +9,7 @@ github.com/bitrise-io/bitrise-init/steps github.com/bitrise-io/bitrise-init/utility # github.com/bitrise-io/envman v0.0.0-20190813133714-27a300a1ed43 github.com/bitrise-io/envman/models -# github.com/bitrise-io/go-steputils v0.0.0-20190806143347-f540824d77df +# github.com/bitrise-io/go-steputils v0.0.0-20201016102104-03ae3a6ded35 github.com/bitrise-io/go-steputils/cache github.com/bitrise-io/go-steputils/stepconf github.com/bitrise-io/go-steputils/tools @@ -18,6 +18,7 @@ github.com/bitrise-io/go-utils/colorstring github.com/bitrise-io/go-utils/command github.com/bitrise-io/go-utils/command/gems github.com/bitrise-io/go-utils/command/rubycommand +github.com/bitrise-io/go-utils/errorutil github.com/bitrise-io/go-utils/fileutil github.com/bitrise-io/go-utils/log github.com/bitrise-io/go-utils/parseutil @@ -28,7 +29,8 @@ github.com/bitrise-io/go-utils/sliceutil github.com/bitrise-io/stepman/models # github.com/bitrise-steplib/bitrise-step-android-unit-test v0.0.0-20190902203028-ff8e682d8645 github.com/bitrise-steplib/bitrise-step-android-unit-test/cache -# github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver v0.0.0-20210111151319-1426de4b985f +# github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver v0.0.0-20210202154324-7c20293c7cf0 +github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/appleauth github.com/bitrise-steplib/steps-deploy-to-itunesconnect-deliver/devportalservice # github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote