diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 256d9ede..a21f962f 100644 --- a/.github/workflows/build.yml +++ b/.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 }}" \ diff --git a/Makefile b/Makefile index 50db1082..11cecd17 100644 --- a/Makefile +++ b/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 && \ diff --git a/cmd/internal/app_store_connect/main.go b/cmd/internal/app_store_connect/main.go index 1304b768..32e6dcd7 100644 --- a/cmd/internal/app_store_connect/main.go +++ b/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 "publish_testflight": - err := publishTestflight(context.Background()) + case "next_macos_project_version": + err := fetchMacOSVersion(ctx) if err != nil { log.Fatal(err) } - case "next_macos_project_version": - err := fetchMacOSVersion(context.Background()) + case "publish_testflight": + err := publishTestflight(ctx) + if err != nil { + log.Fatal(err) + } + 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,32 +66,6 @@ func createClient() *asc.Client { return asc.NewClient(tokenConfig.Client()) } -func publishTestflight(ctx context.Context) error { - client := createClient() - var buildsToPublish []asc.Build - for _, platform := range []string{ - "IOS", - "MAC_OS", - "TV_OS", - } { - builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{ - FilterApp: []string{appID}, - FilterPreReleaseVersionPlatform: []string{platform}, - }) - if err != nil { - return err - } - buildsToPublish = append(buildsToPublish, builds.Data[0]) - } - _, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, common.Map(buildsToPublish, func(it asc.Build) string { - return it.ID - })) - if err != nil { - return err - } - return nil -} - func fetchMacOSVersion(ctx context.Context) error { client := createClient() versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{ @@ -106,3 +98,250 @@ findVersion: os.Stdout.WriteString(F.ToString(versionInt+1, "\n")) return nil } + +func publishTestflight(ctx context.Context) error { + client := createClient() + var buildsToPublish []asc.Build + for _, platform := range []string{ + "IOS", + "MAC_OS", + "TV_OS", + } { + builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{ + FilterApp: []string{appID}, + FilterPreReleaseVersionPlatform: []string{platform}, + }) + if err != nil { + return err + } + buildsToPublish = append(buildsToPublish, builds.Data[0]) + } + _, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, common.Map(buildsToPublish, func(it asc.Build) string { + return it.ID + })) + if err != nil { + return err + } + return nil +} + +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{string(platform)}, + }) + if err != nil { + return err + } + version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool { + return *it.Attributes.VersionString == tag + }) + if version.ID == "" { + return nil + } + 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 + } + if err != nil { + return err + } + log.Info(platform, " ", tag, " delete submission") + _, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID) + if err != nil { + 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 + } + } + return nil +}