123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- package main
- import (
- "context"
- "net/http"
- "os"
- "strconv"
- "strings"
- "time"
- "github.com/sagernet/asc-go/asc"
- "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"
- F "github.com/sagernet/sing/common/format"
- )
- 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(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)
- }
- default:
- log.Fatal("unknown action: ", os.Args[1])
- }
- }
- const (
- appID = "6673731168"
- groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
- )
- func createClient(expireDuration time.Duration) *asc.Client {
- privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
- if err != nil {
- log.Fatal(err)
- }
- tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey)
- if err != nil {
- log.Fatal(err)
- }
- return asc.NewClient(tokenConfig.Client())
- }
- func fetchMacOSVersion(ctx context.Context) error {
- client := createClient(time.Minute)
- 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 {
- tagVersion, err := build_shared.ReadTagVersion()
- if err != nil {
- return err
- }
- tag := tagVersion.VersionString()
- client := createClient(10 * time.Minute)
- log.Info(tag, " list build IDs")
- buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
- if err != nil {
- return err
- }
- buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
- return it.ID
- })
- var platforms []asc.Platform
- if len(os.Args) == 3 {
- switch os.Args[2] {
- case "ios":
- platforms = []asc.Platform{asc.PlatformIOS}
- case "macos":
- platforms = []asc.Platform{asc.PlatformMACOS}
- case "tvos":
- platforms = []asc.Platform{asc.PlatformTVOS}
- default:
- return E.New("unknown platform: ", os.Args[2])
- }
- } else {
- platforms = []asc.Platform{
- asc.PlatformIOS,
- asc.PlatformMACOS,
- asc.PlatformTVOS,
- }
- }
- for _, platform := range platforms {
- log.Info(string(platform), " list builds")
- for {
- builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
- FilterApp: []string{appID},
- FilterPreReleaseVersionPlatform: []string{string(platform)},
- })
- if err != nil {
- return err
- }
- build := builds.Data[0]
- if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute {
- log.Info(string(platform), " ", tag, " waiting for process")
- time.Sleep(15 * time.Second)
- continue
- }
- if *build.Attributes.ProcessingState != "VALID" {
- log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
- time.Sleep(15 * time.Second)
- continue
- }
- log.Info(string(platform), " ", tag, " list localizations")
- localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
- if err != nil {
- return err
- }
- localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
- return *it.Attributes.Locale == "en-US"
- })
- if localization.ID == "" {
- log.Fatal(string(platform), " ", tag, " no en-US localization found")
- }
- if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
- log.Info(string(platform), " ", tag, " update localization")
- _, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
- F.ToString("sing-box ", tagVersion.String()),
- ))
- if err != nil {
- return err
- }
- }
- log.Info(string(platform), " ", tag, " publish")
- response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
- if response != nil && response.StatusCode == http.StatusUnprocessableEntity {
- log.Info("waiting for process")
- time.Sleep(15 * time.Second)
- continue
- } else if err != nil {
- return err
- }
- log.Info(string(platform), " ", tag, " list submissions")
- betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
- FilterBuild: []string{build.ID},
- })
- if err != nil {
- return err
- }
- if len(betaSubmissions.Data) == 0 {
- log.Info(string(platform), " ", tag, " create submission")
- _, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
- if err != nil {
- if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
- log.Error(err)
- break
- }
- return err
- }
- }
- break
- }
- }
- 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(time.Minute)
- for {
- log.Info(platform, " list versions")
- versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
- FilterPlatform: []string{string(platform)},
- })
- if isRetryable(response) {
- continue
- } else 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(platform, " ", tag, " get submission")
- submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
- if response != nil && response.StatusCode == http.StatusNotFound {
- return nil
- }
- if isRetryable(response) {
- continue
- } else 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(time.Minute)
- 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")
- response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
- if err != nil {
- return err
- }
- if response.StatusCode != http.StatusNoContent {
- response.Write(os.Stderr)
- log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
- }
- } 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 == nil || 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:
- return err
- }
- }
- switch response.StatusCode {
- case http.StatusCreated:
- break fixSubmit
- default:
- return err
- }
- }
- }
- return nil
- }
- func publishAppStore(ctx context.Context) error {
- tag, err := build_shared.ReadTag()
- if err != nil {
- return err
- }
- client := createClient(time.Minute)
- 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
- }
- func isRetryable(response *asc.Response) bool {
- if response == nil {
- return false
- }
- switch response.StatusCode {
- case http.StatusInternalServerError, http.StatusUnprocessableEntity:
- return true
- default:
- return false
- }
- }
|