mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-12-27 16:55:39 +08:00
446 lines
13 KiB
Go
446 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"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 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 {
|
|
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
|
|
}
|
|
}
|