Add App Store review submission command

This commit is contained in:
世界
2026-09-20 22:24:38 +08:00
parent 394285b78b
commit 9fa1890aea
5 changed files with 459 additions and 232 deletions
+403 -225
View File
@@ -4,12 +4,14 @@ import (
"context"
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
"github.com/sagernet/asc-go/asc"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
@@ -32,18 +34,37 @@ func main() {
if err != nil {
log.Fatal(err)
}
case "list_builds":
if len(os.Args) < 3 {
log.Fatal("platform required: ios, macos, or tvos")
}
err := listBuilds(ctx, os.Args[2])
if err != nil {
log.Fatal(err)
}
case "remove_testflight":
if len(os.Args) < 5 {
log.Fatal("usage: remove_testflight <platform> <version> <build>")
}
err := removeTestflight(ctx, os.Args[2], os.Args[3], os.Args[4])
if err != nil {
log.Fatal(err)
}
case "cancel_app_store":
if len(os.Args) < 3 {
log.Fatal("platform required: ios, macos, or tvos")
}
err := cancelAppStore(ctx, os.Args[2])
if err != nil {
log.Fatal(err)
}
case "prepare_app_store":
err := prepareAppStore(ctx)
case "submit_app_store":
err := submitAppStore(ctx, os.Args[2:])
if err != nil {
log.Fatal(err)
}
case "publish_app_store":
err := publishAppStore(ctx)
err := publishAppStore(ctx, os.Args[2:])
if err != nil {
log.Fatal(err)
}
@@ -55,8 +76,35 @@ func main() {
const (
appID = "6785326793"
groupID = "39f9ebdc-05d4-421f-9595-dae71df227c4"
applePath = "../sing-box-for-apple"
appStorePromotionalText = "The universal proxy platform."
appStoreWhatsNew = "Fixes and improvements"
buildPollInterval = 15 * time.Second
)
var (
appStorePlatforms = []string{"ios", "tvos"}
platformDirectories = map[asc.Platform]string{
asc.PlatformIOS: "SFI",
asc.PlatformMACOS: "SFM",
asc.PlatformTVOS: "SFT",
}
)
func parsePlatform(name string) (asc.Platform, error) {
switch name {
case "ios":
return asc.PlatformIOS, nil
case "macos":
return asc.PlatformMACOS, nil
case "tvos":
return asc.PlatformTVOS, nil
default:
return "", E.New("unknown platform: ", name)
}
}
func createClient(expireDuration time.Duration) *asc.Client {
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
if err != nil {
@@ -70,16 +118,9 @@ func createClient(expireDuration time.Duration) *asc.Client {
}
func fetchNextProjectVersion(ctx context.Context, platformName string) error {
var platform asc.Platform
switch platformName {
case "ios":
platform = asc.PlatformIOS
case "macos":
platform = asc.PlatformMACOS
case "tvos":
platform = asc.PlatformTVOS
default:
return E.New("unknown platform: ", platformName)
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
query := &asc.ListBuildsQuery{
@@ -115,20 +156,55 @@ func fetchNextProjectVersion(ctx context.Context, platformName string) error {
return nil
}
func listBuilds(ctx context.Context, platformName string) error {
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
client := createClient(time.Minute)
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
if err != nil {
return err
}
betaBuildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
return it.ID
})
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
Include: []string{"preReleaseVersion"},
Sort: []string{"-uploadedDate"},
Limit: 10,
})
if err != nil {
return err
}
for _, build := range builds.Data {
var betaGroup string
if slices.Contains(betaBuildIDs, build.ID) {
betaGroup = " testflight"
}
var versionString string
if build.Relationships != nil && build.Relationships.PreReleaseVersion != nil && build.Relationships.PreReleaseVersion.Data != nil {
for _, included := range builds.Included {
preReleaseVersion := included.PrereleaseVersion()
if preReleaseVersion != nil && preReleaseVersion.ID == build.Relationships.PreReleaseVersion.Data.ID {
versionString = *preReleaseVersion.Attributes.Version
}
}
}
os.Stdout.WriteString(F.ToString(versionString, " (", *build.Attributes.Version, ") ", *build.Attributes.ProcessingState, " ", build.Attributes.UploadedDate.Time.Format(time.RFC3339), betaGroup, "\n"))
}
return nil
}
func publishTestflight(ctx context.Context) error {
if len(os.Args) < 3 {
return E.New("platform required: ios, macos, or tvos")
}
var platform asc.Platform
switch os.Args[2] {
case "ios":
platform = asc.PlatformIOS
case "macos":
platform = asc.PlatformMACOS
case "tvos":
platform = asc.PlatformTVOS
default:
return E.New("unknown platform: ", os.Args[2])
platform, err := parsePlatform(os.Args[2])
if err != nil {
return err
}
tagVersion, err := build_shared.ReadTagVersion()
@@ -173,19 +249,19 @@ func publishTestflight(ctx context.Context) error {
}
if len(builds.Data) == 0 {
log.Info(string(platform), " ", testFlightVersion, " waiting for build upload")
time.Sleep(15 * time.Second)
time.Sleep(buildPollInterval)
continue
}
build := builds.Data[0]
log.Info(string(platform), " ", testFlightVersion, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
if projectVersion == "" && common.Contains(buildIDs, build.ID) {
log.Info(string(platform), " ", testFlightVersion, " build ", *build.Attributes.Version, " already published, waiting for new upload")
time.Sleep(15 * time.Second)
time.Sleep(buildPollInterval)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
log.Info(string(platform), " ", testFlightVersion, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
time.Sleep(buildPollInterval)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
@@ -210,7 +286,7 @@ func publishTestflight(ctx context.Context) error {
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
log.Info("waiting for process")
time.Sleep(15 * time.Second)
time.Sleep(buildPollInterval)
continue
} else if err != nil {
return err
@@ -238,227 +314,341 @@ func publishTestflight(ctx context.Context) error {
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()
func removeTestflight(ctx context.Context, platformName string, versionString string, buildNumber string) error {
platform, err := parsePlatform(platformName)
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
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
FilterPreReleaseVersionVersion: []string{versionString},
FilterVersion: []string{buildNumber},
})
if err != nil {
return err
}
if len(builds.Data) != 1 {
return E.New(string(platform), " ", versionString, " (", buildNumber, ") matched ", len(builds.Data), " builds")
}
build := builds.Data[0]
log.Info(string(platform), " ", versionString, " (", buildNumber, ") remove from beta group")
_, err = client.Builds.RemoveAccessForBetaGroupsFromBuild(ctx, build.ID, []string{groupID})
if err != nil {
return err
}
return nil
}
func prepareAppStore(ctx context.Context) error {
func cancelAppStore(ctx context.Context, platformName string) error {
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
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)},
})
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
FilterVersionString: []string{tag},
})
if err != nil {
return err
}
if len(versions.Data) == 0 {
return nil
}
versionID := versions.Data[0].ID
log.Info(string(platform), " ", tag, " list review submissions")
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
FilterApp: []string{appID},
FilterPlatform: []string{string(platform)},
FilterState: []string{
string(asc.ReviewSubmissionStateReadyForReview),
string(asc.ReviewSubmissionStateWaitingForReview),
string(asc.ReviewSubmissionStateInReview),
},
})
if err != nil {
return err
}
for _, submission := range submissions.Data {
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, submission.ID, versionID)
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 !containsVersion {
continue
}
log.Info(string(platform), " ", tag, " cancel review submission ", submission.ID)
_, _, err = client.Submission.UpdateReviewSubmission(ctx, submission.ID, nil, common.Ptr(true))
if err != nil {
return err
}
if len(builds.Data) == 0 {
log.Fatal(string(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()
func reviewSubmissionContainsVersion(ctx context.Context, client *asc.Client, submissionID string, versionID string) (bool, error) {
items, _, err := client.Submission.ListItemsForReviewSubmission(ctx, submissionID, nil)
if err != nil {
return false, err
}
return slices.ContainsFunc(items.Data, func(it asc.ReviewSubmissionItem) bool {
return it.Relationships != nil && it.Relationships.AppStoreVersion != nil &&
it.Relationships.AppStoreVersion.Data != nil && it.Relationships.AppStoreVersion.Data.ID == versionID
}), nil
}
func submitAppStore(ctx context.Context, platformNames []string) error {
if len(platformNames) == 0 {
platformNames = appStorePlatforms
}
client := createClient(20 * time.Minute)
for _, platformName := range platformNames {
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
versionString, err := build_shared.ReadAppleMarketingVersion(applePath, platformDirectories[platform])
if err != nil {
return err
}
err = submitAppStoreVersion(ctx, client, platform, versionString)
if err != nil {
return err
}
}
return nil
}
func submitAppStoreVersion(ctx context.Context, client *asc.Client, platform asc.Platform, versionString string) error {
log.Info(string(platform), " ", versionString, " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
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)},
localVersion := badversion.Parse(versionString)
var version asc.AppStoreVersion
for _, remoteVersion := range versions.Data {
remoteVersionString := *remoteVersion.Attributes.VersionString
if remoteVersionString == versionString {
version = remoteVersion
continue
}
if !badversion.Parse(remoteVersionString).LessThan(localVersion) {
return E.New(string(platform), " ", versionString, " is not newer than App Store version ", remoteVersionString)
}
}
if version.ID != "" {
state := *version.Attributes.AppStoreState
switch state {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected,
asc.AppStoreVersionStateMetadataRejected,
asc.AppStoreVersionStateInvalidBinary:
log.Info(string(platform), " ", versionString, " found version in state ", string(state))
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
return nil
case asc.AppStoreVersionStatePendingDeveloperRelease:
log.Info(string(platform), " ", versionString, " approved, run publish_app_store to release")
return nil
default:
return E.New(string(platform), " ", versionString, " already released: ", string(state))
}
} else {
log.Info(string(platform), " ", versionString, " create version")
created, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
Platform: platform,
ReleaseType: common.Ptr("MANUAL"),
VersionString: versionString,
}, appID, nil)
if err != nil {
return err
}
version = created.Data
}
log.Info(string(platform), " ", versionString, " list localizations")
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), " ", versionString, " create en-US localization")
_, _, err = client.Apps.CreateAppStoreVersionLocalization(ctx, asc.AppStoreVersionLocalizationCreateRequestAttributes{
Locale: "en-US",
PromotionalText: common.Ptr(appStorePromotionalText),
WhatsNew: common.Ptr(appStoreWhatsNew),
}, version.ID)
if err != nil {
return err
}
} else if localization.Attributes.PromotionalText == nil || *localization.Attributes.PromotionalText != appStorePromotionalText ||
localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew != appStoreWhatsNew {
log.Info(string(platform), " ", versionString, " update en-US localization")
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
PromotionalText: common.Ptr(appStorePromotionalText),
WhatsNew: common.Ptr(appStoreWhatsNew),
})
if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
}
var build asc.Build
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
FilterPreReleaseVersionVersion: []string{versionString},
Sort: []string{"-uploadedDate"},
Limit: 1,
})
switch *version.Attributes.AppStoreState {
if err != nil {
return err
}
if len(builds.Data) == 0 {
log.Info(string(platform), " ", versionString, " waiting for build upload")
time.Sleep(buildPollInterval)
continue
}
build = builds.Data[0]
processingState := *build.Attributes.ProcessingState
if processingState == "VALID" {
break
}
if processingState == "PROCESSING" {
log.Info(string(platform), " ", versionString, " waiting for build ", *build.Attributes.Version, ": ", processingState)
time.Sleep(buildPollInterval)
continue
}
return E.New(string(platform), " ", versionString, " build ", *build.Attributes.Version, " ", processingState)
}
log.Info(string(platform), " ", versionString, " found build ", *build.Attributes.Version)
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != build.ID {
log.Info(string(platform), " ", versionString, " attach build ", *build.Attributes.Version)
_, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, common.Ptr(build.ID))
if err != nil {
return err
}
}
log.Info(string(platform), " ", versionString, " list review submissions")
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
FilterApp: []string{appID},
FilterPlatform: []string{string(platform)},
FilterState: []string{
string(asc.ReviewSubmissionStateReadyForReview),
string(asc.ReviewSubmissionStateWaitingForReview),
string(asc.ReviewSubmissionStateInReview),
string(asc.ReviewSubmissionStateUnresolvedIssues),
},
})
if err != nil {
return err
}
var (
submission asc.ReviewSubmission
submissionHasItem bool
)
for _, remoteSubmission := range submissions.Data {
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, remoteSubmission.ID, version.ID)
if err != nil {
return err
}
state := *remoteSubmission.Attributes.State
switch state {
case asc.ReviewSubmissionStateReadyForReview:
submission = remoteSubmission
submissionHasItem = containsVersion
case asc.ReviewSubmissionStateWaitingForReview, asc.ReviewSubmissionStateInReview:
if containsVersion {
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
return nil
}
return E.New(string(platform), " review submission ", remoteSubmission.ID, " with another version is ", string(state))
default:
return E.New(string(platform), " review submission ", remoteSubmission.ID, " is ", string(state))
}
}
if submission.ID == "" {
log.Info(string(platform), " ", versionString, " create review submission")
created, _, err := client.Submission.CreateReviewSubmission(ctx, appID, common.Ptr(platform))
if err != nil {
return err
}
submission = created.Data
}
if !submissionHasItem {
log.Info(string(platform), " ", versionString, " add version to review submission ", submission.ID)
_, _, err = client.Submission.CreateReviewSubmissionItemForAppStoreVersion(ctx, submission.ID, version.ID)
if err != nil {
return err
}
}
log.Info(string(platform), " ", versionString, " submit review submission ", submission.ID)
_, _, err = client.Submission.UpdateReviewSubmission(ctx, submission.ID, common.Ptr(true), nil)
if err != nil {
return err
}
return nil
}
func publishAppStore(ctx context.Context, platformNames []string) error {
if len(platformNames) == 0 {
platformNames = appStorePlatforms
}
client := createClient(time.Minute)
for _, platformName := range platformNames {
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
versionString, err := build_shared.ReadAppleMarketingVersion(applePath, platformDirectories[platform])
if err != nil {
return err
}
log.Info(string(platform), " ", versionString, " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
FilterVersionString: []string{versionString},
})
if err != nil {
return err
}
if len(versions.Data) == 0 {
return E.New(string(platform), " ", versionString, " not found")
}
version := versions.Data[0]
state := *version.Attributes.AppStoreState
switch state {
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")
return E.New(string(platform), " ", versionString, " not submitted")
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
log.Warn(string(platform), " ", versionString, " waiting for review")
continue
case asc.AppStoreVersionStatePendingDeveloperRelease:
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
return E.New(string(platform), " ", versionString, " unknown state ", string(state))
}
log.Info(string(platform), " ", versionString, " release")
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
if err != nil {
return err
@@ -466,15 +656,3 @@ func publishAppStore(ctx context.Context) error {
}
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
}
}
+47
View File
@@ -0,0 +1,47 @@
package build_shared
import (
"os"
"path/filepath"
"strings"
E "github.com/sagernet/sing/common/exceptions"
"howett.net/plist"
)
func ReadAppleMarketingVersion(applePath string, directory string) (string, error) {
projectFile, err := os.Open(filepath.Join(applePath, "sing-box.xcodeproj", "project.pbxproj"))
if err != nil {
return "", err
}
defer projectFile.Close()
var project map[string]any
err = plist.NewDecoder(projectFile).Decode(&project)
if err != nil {
return "", err
}
var marketingVersion string
for _, object := range project["objects"].(map[string]any) {
buildSettings, isMap := object.(map[string]any)["buildSettings"].(map[string]any)
if !isMap {
continue
}
infoPlistFile, isString := buildSettings["INFOPLIST_FILE"].(string)
if !isString || !strings.HasPrefix(infoPlistFile, directory+"/") {
continue
}
version, isString := buildSettings["MARKETING_VERSION"].(string)
if !isString {
continue
}
if marketingVersion != "" && marketingVersion != version {
return "", E.New("inconsistent marketing versions in ", directory, ": ", marketingVersion, ", ", version)
}
marketingVersion = version
}
if marketingVersion == "" {
return "", E.New("marketing version not found in ", directory)
}
return marketingVersion, nil
}