Browse Source

release: Add app store connect actions

世界 1 year ago
parent
commit
52f3a4226c
3 changed files with 266 additions and 20 deletions
  1. 1 0
      .github/workflows/build.yml
  2. 6 0
      Makefile
  3. 259 20
      cmd/internal/app_store_connect/main.go

+ 1 - 0
.github/workflows/build.yml

@@ -478,6 +478,7 @@ jobs:
       - name: Upload to App Store Connect
         if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch'
         run: |-
+          go run -v ./cmd/internal/app_store_connect cancel_app_store ${{ matrix.platform }}
           cd clients/apple
           xcodebuild -exportArchive \
             -archivePath "${{ matrix.archive }}" \

+ 6 - 0
Makefile

@@ -192,6 +192,12 @@ release_apple_beta: update_apple_version release_ios release_macos release_tvos
 publish_testflight:
 	go run -v ./cmd/internal/app_store_connect publish_testflight
 
+prepare_app_store:
+	go run -v ./cmd/internal/app_store_connect prepare_app_store
+
+publish_app_store:
+	go run -v ./cmd/internal/app_store_connect publish_app_store
+
 test:
 	@go test -v ./... && \
 	cd test && \

+ 259 - 20
cmd/internal/app_store_connect/main.go

@@ -2,10 +2,12 @@ package main
 
 import (
 	"context"
+	"net/http"
 	"os"
 	"strconv"
 	"time"
 
+	"github.com/sagernet/sing-box/cmd/internal/build_shared"
 	"github.com/sagernet/sing-box/log"
 	"github.com/sagernet/sing/common"
 	E "github.com/sagernet/sing/common/exceptions"
@@ -15,14 +17,30 @@ import (
 )
 
 func main() {
+	ctx := context.Background()
 	switch os.Args[1] {
+	case "next_macos_project_version":
+		err := fetchMacOSVersion(ctx)
+		if err != nil {
+			log.Fatal(err)
+		}
 	case "publish_testflight":
-		err := publishTestflight(context.Background())
+		err := publishTestflight(ctx)
 		if err != nil {
 			log.Fatal(err)
 		}
-	case "next_macos_project_version":
-		err := fetchMacOSVersion(context.Background())
+	case "cancel_app_store":
+		err := cancelAppStore(ctx, os.Args[2])
+		if err != nil {
+			log.Fatal(err)
+		}
+	case "prepare_app_store":
+		err := prepareAppStore(ctx)
+		if err != nil {
+			log.Fatal(err)
+		}
+	case "publish_app_store":
+		err := publishAppStore(ctx)
 		if err != nil {
 			log.Fatal(err)
 		}
@@ -48,6 +66,39 @@ func createClient() *asc.Client {
 	return asc.NewClient(tokenConfig.Client())
 }
 
+func fetchMacOSVersion(ctx context.Context) error {
+	client := createClient()
+	versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
+		FilterPlatform: []string{"MAC_OS"},
+	})
+	if err != nil {
+		return err
+	}
+	var versionID string
+findVersion:
+	for _, version := range versions.Data {
+		switch *version.Attributes.AppStoreState {
+		case asc.AppStoreVersionStateReadyForSale,
+			asc.AppStoreVersionStatePendingDeveloperRelease:
+			versionID = version.ID
+			break findVersion
+		}
+	}
+	if versionID == "" {
+		return E.New("no version found")
+	}
+	latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
+	if err != nil {
+		return err
+	}
+	versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
+	if err != nil {
+		return E.Cause(err, "parse version code")
+	}
+	os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
+	return nil
+}
+
 func publishTestflight(ctx context.Context) error {
 	client := createClient()
 	var buildsToPublish []asc.Build
@@ -74,35 +125,223 @@ func publishTestflight(ctx context.Context) error {
 	return nil
 }
 
-func fetchMacOSVersion(ctx context.Context) error {
+func cancelAppStore(ctx context.Context, platform string) error {
+	switch platform {
+	case "ios":
+		platform = string(asc.PlatformIOS)
+	case "macos":
+		platform = string(asc.PlatformMACOS)
+	case "tvos":
+		platform = string(asc.PlatformTVOS)
+	}
+	tag, err := build_shared.ReadTag()
+	if err != nil {
+		return err
+	}
 	client := createClient()
+	log.Info(platform, " list versions")
 	versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
-		FilterPlatform: []string{"MAC_OS"},
+		FilterPlatform: []string{string(platform)},
 	})
 	if err != nil {
 		return err
 	}
-	var versionID string
-findVersion:
-	for _, version := range versions.Data {
-		switch *version.Attributes.AppStoreState {
-		case asc.AppStoreVersionStateReadyForSale,
-			asc.AppStoreVersionStatePendingDeveloperRelease:
-			versionID = version.ID
-			break findVersion
-		}
+	version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
+		return *it.Attributes.VersionString == tag
+	})
+	if version.ID == "" {
+		return nil
 	}
-	if versionID == "" {
-		return E.New("no version found")
+	log.Info(string(platform), " ", tag, " get submission")
+	submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
+	if response != nil && response.StatusCode == http.StatusNotFound {
+		return nil
 	}
-	latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
 	if err != nil {
 		return err
 	}
-	versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
+	log.Info(platform, " ", tag, " delete submission")
+	_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
 	if err != nil {
-		return E.Cause(err, "parse version code")
+		return err
+	}
+	return nil
+}
+
+func prepareAppStore(ctx context.Context) error {
+	tag, err := build_shared.ReadTag()
+	if err != nil {
+		return err
+	}
+	client := createClient()
+	for _, platform := range []asc.Platform{
+		asc.PlatformIOS,
+		asc.PlatformMACOS,
+		asc.PlatformTVOS,
+	} {
+		log.Info(string(platform), " list versions")
+		versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
+			FilterPlatform: []string{string(platform)},
+		})
+		if err != nil {
+			return err
+		}
+		version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
+			return *it.Attributes.VersionString == tag
+		})
+		log.Info(string(platform), " ", tag, " list builds")
+		builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
+			FilterApp:                       []string{appID},
+			FilterPreReleaseVersionPlatform: []string{string(platform)},
+		})
+		if err != nil {
+			return err
+		}
+		if len(builds.Data) == 0 {
+			log.Fatal(platform, " ", tag, " no build found")
+		}
+		buildID := common.Ptr(builds.Data[0].ID)
+		if version.ID == "" {
+			log.Info(string(platform), " ", tag, " create version")
+			newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
+				Platform:      platform,
+				VersionString: tag,
+			}, appID, buildID)
+			if err != nil {
+				return err
+			}
+			version = newVersion.Data
+
+		} else {
+			log.Info(string(platform), " ", tag, " check build")
+			currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
+			if err != nil {
+				return err
+			}
+			if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
+				switch *version.Attributes.AppStoreState {
+				case asc.AppStoreVersionStatePrepareForSubmission,
+					asc.AppStoreVersionStateRejected,
+					asc.AppStoreVersionStateDeveloperRejected:
+				case asc.AppStoreVersionStateWaitingForReview,
+					asc.AppStoreVersionStateInReview,
+					asc.AppStoreVersionStatePendingDeveloperRelease:
+					submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
+					if err != nil {
+						return err
+					}
+					if submission != nil {
+						log.Info(string(platform), " ", tag, " delete submission")
+						_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
+						if err != nil {
+							return err
+						}
+						time.Sleep(5 * time.Second)
+					}
+				default:
+					log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
+				}
+				log.Info(string(platform), " ", tag, " update build")
+				_, _, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
+				if err != nil {
+					return err
+				}
+			} else {
+				switch *version.Attributes.AppStoreState {
+				case asc.AppStoreVersionStatePrepareForSubmission,
+					asc.AppStoreVersionStateRejected,
+					asc.AppStoreVersionStateDeveloperRejected:
+				case asc.AppStoreVersionStateWaitingForReview,
+					asc.AppStoreVersionStateInReview,
+					asc.AppStoreVersionStatePendingDeveloperRelease:
+					continue
+				default:
+					log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
+				}
+			}
+		}
+		log.Info(string(platform), " ", tag, " list localization")
+		localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
+		if err != nil {
+			return err
+		}
+		localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
+			return *it.Attributes.Locale == "en-US"
+		})
+		if localization.ID == "" {
+			log.Info(string(platform), " ", tag, " no en-US localization found")
+		}
+		if localization.Attributes.WhatsNew == nil && *localization.Attributes.WhatsNew == "" {
+			log.Info(string(platform), " ", tag, " update localization")
+			_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
+				PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
+				WhatsNew:        common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
+			})
+			if err != nil {
+				return err
+			}
+		}
+		log.Info(string(platform), " ", tag, " create submission")
+	fixSubmit:
+		for {
+			_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
+			if err != nil {
+				switch response.StatusCode {
+				case http.StatusInternalServerError:
+					continue
+				default:
+					response.Write(os.Stderr)
+					log.Info(string(platform), " ", tag, " unexpected response: ", response.Status)
+				}
+			}
+			switch response.StatusCode {
+			case http.StatusCreated:
+				break fixSubmit
+			default:
+				response.Write(os.Stderr)
+				log.Info(string(platform), " ", tag, " unexpected response: ", response.Status)
+			}
+		}
+	}
+	return nil
+}
+
+func publishAppStore(ctx context.Context) error {
+	tag, err := build_shared.ReadTag()
+	if err != nil {
+		return err
+	}
+	client := createClient()
+	for _, platform := range []asc.Platform{
+		asc.PlatformIOS,
+		asc.PlatformMACOS,
+		asc.PlatformTVOS,
+	} {
+		log.Info(string(platform), " list versions")
+		versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
+			FilterPlatform: []string{string(platform)},
+		})
+		if err != nil {
+			return err
+		}
+		version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
+			return *it.Attributes.VersionString == tag
+		})
+		switch *version.Attributes.AppStoreState {
+		case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
+			log.Fatal(string(platform), " ", tag, " not submitted")
+		case asc.AppStoreVersionStateWaitingForReview,
+			asc.AppStoreVersionStateInReview:
+			log.Warn(string(platform), " ", tag, " waiting for review")
+			continue
+		case asc.AppStoreVersionStatePendingDeveloperRelease:
+		default:
+			log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
+		}
+		_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
+		if err != nil {
+			return err
+		}
 	}
-	os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
 	return nil
 }