| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445 | package mainimport (	"context"	"net/http"	"os"	"strconv"	"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 stringfindVersion:	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 {					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	}}
 |