main.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. package main
  2. import (
  3. "context"
  4. "net/http"
  5. "os"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/sagernet/asc-go/asc"
  10. "github.com/sagernet/sing-box/cmd/internal/build_shared"
  11. "github.com/sagernet/sing-box/log"
  12. "github.com/sagernet/sing/common"
  13. E "github.com/sagernet/sing/common/exceptions"
  14. F "github.com/sagernet/sing/common/format"
  15. )
  16. func main() {
  17. ctx := context.Background()
  18. switch os.Args[1] {
  19. case "next_macos_project_version":
  20. err := fetchMacOSVersion(ctx)
  21. if err != nil {
  22. log.Fatal(err)
  23. }
  24. case "publish_testflight":
  25. err := publishTestflight(ctx)
  26. if err != nil {
  27. log.Fatal(err)
  28. }
  29. case "cancel_app_store":
  30. err := cancelAppStore(ctx, os.Args[2])
  31. if err != nil {
  32. log.Fatal(err)
  33. }
  34. case "prepare_app_store":
  35. err := prepareAppStore(ctx)
  36. if err != nil {
  37. log.Fatal(err)
  38. }
  39. case "publish_app_store":
  40. err := publishAppStore(ctx)
  41. if err != nil {
  42. log.Fatal(err)
  43. }
  44. default:
  45. log.Fatal("unknown action: ", os.Args[1])
  46. }
  47. }
  48. const (
  49. appID = "6673731168"
  50. groupID = "5c5f3b78-b7a0-40c0-bcad-e6ef87bbefda"
  51. )
  52. func createClient(expireDuration time.Duration) *asc.Client {
  53. privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
  54. if err != nil {
  55. log.Fatal(err)
  56. }
  57. tokenConfig, err := asc.NewTokenConfig(os.Getenv("ASC_KEY_ID"), os.Getenv("ASC_KEY_ISSUER_ID"), expireDuration, privateKey)
  58. if err != nil {
  59. log.Fatal(err)
  60. }
  61. return asc.NewClient(tokenConfig.Client())
  62. }
  63. func fetchMacOSVersion(ctx context.Context) error {
  64. client := createClient(time.Minute)
  65. versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
  66. FilterPlatform: []string{"MAC_OS"},
  67. })
  68. if err != nil {
  69. return err
  70. }
  71. var versionID string
  72. findVersion:
  73. for _, version := range versions.Data {
  74. switch *version.Attributes.AppStoreState {
  75. case asc.AppStoreVersionStateReadyForSale,
  76. asc.AppStoreVersionStatePendingDeveloperRelease:
  77. versionID = version.ID
  78. break findVersion
  79. }
  80. }
  81. if versionID == "" {
  82. return E.New("no version found")
  83. }
  84. latestBuild, _, err := client.Builds.GetBuildForAppStoreVersion(ctx, versionID, &asc.GetBuildForAppStoreVersionQuery{})
  85. if err != nil {
  86. return err
  87. }
  88. versionInt, err := strconv.Atoi(*latestBuild.Data.Attributes.Version)
  89. if err != nil {
  90. return E.Cause(err, "parse version code")
  91. }
  92. os.Stdout.WriteString(F.ToString(versionInt+1, "\n"))
  93. return nil
  94. }
  95. func publishTestflight(ctx context.Context) error {
  96. tagVersion, err := build_shared.ReadTagVersion()
  97. if err != nil {
  98. return err
  99. }
  100. tag := tagVersion.VersionString()
  101. client := createClient(20 * time.Minute)
  102. log.Info(tag, " list build IDs")
  103. buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
  104. if err != nil {
  105. return err
  106. }
  107. buildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
  108. return it.ID
  109. })
  110. var platforms []asc.Platform
  111. if len(os.Args) == 3 {
  112. switch os.Args[2] {
  113. case "ios":
  114. platforms = []asc.Platform{asc.PlatformIOS}
  115. case "macos":
  116. platforms = []asc.Platform{asc.PlatformMACOS}
  117. case "tvos":
  118. platforms = []asc.Platform{asc.PlatformTVOS}
  119. default:
  120. return E.New("unknown platform: ", os.Args[2])
  121. }
  122. } else {
  123. platforms = []asc.Platform{
  124. asc.PlatformIOS,
  125. asc.PlatformMACOS,
  126. asc.PlatformTVOS,
  127. }
  128. }
  129. waitingForProcess := false
  130. for _, platform := range platforms {
  131. log.Info(string(platform), " list builds")
  132. for {
  133. builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
  134. FilterApp: []string{appID},
  135. FilterPreReleaseVersionPlatform: []string{string(platform)},
  136. })
  137. if err != nil {
  138. return err
  139. }
  140. build := builds.Data[0]
  141. if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
  142. log.Info(string(platform), " ", tag, " waiting for process")
  143. time.Sleep(15 * time.Second)
  144. continue
  145. }
  146. if *build.Attributes.ProcessingState != "VALID" {
  147. waitingForProcess = true
  148. log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
  149. time.Sleep(15 * time.Second)
  150. continue
  151. }
  152. log.Info(string(platform), " ", tag, " list localizations")
  153. localizations, _, err := client.TestFlight.ListBetaBuildLocalizationsForBuild(ctx, build.ID, nil)
  154. if err != nil {
  155. return err
  156. }
  157. localization := common.Find(localizations.Data, func(it asc.BetaBuildLocalization) bool {
  158. return *it.Attributes.Locale == "en-US"
  159. })
  160. if localization.ID == "" {
  161. log.Fatal(string(platform), " ", tag, " no en-US localization found")
  162. }
  163. if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
  164. log.Info(string(platform), " ", tag, " update localization")
  165. _, _, err = client.TestFlight.UpdateBetaBuildLocalization(ctx, localization.ID, common.Ptr(
  166. F.ToString("sing-box ", tagVersion.String()),
  167. ))
  168. if err != nil {
  169. return err
  170. }
  171. }
  172. log.Info(string(platform), " ", tag, " publish")
  173. response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
  174. if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
  175. log.Info("waiting for process")
  176. time.Sleep(15 * time.Second)
  177. continue
  178. } else if err != nil {
  179. return err
  180. }
  181. log.Info(string(platform), " ", tag, " list submissions")
  182. betaSubmissions, _, err := client.TestFlight.ListBetaAppReviewSubmissions(ctx, &asc.ListBetaAppReviewSubmissionsQuery{
  183. FilterBuild: []string{build.ID},
  184. })
  185. if err != nil {
  186. return err
  187. }
  188. if len(betaSubmissions.Data) == 0 {
  189. log.Info(string(platform), " ", tag, " create submission")
  190. _, _, err = client.TestFlight.CreateBetaAppReviewSubmission(ctx, build.ID)
  191. if err != nil {
  192. if strings.Contains(err.Error(), "ANOTHER_BUILD_IN_REVIEW") {
  193. log.Error(err)
  194. break
  195. }
  196. return err
  197. }
  198. }
  199. break
  200. }
  201. }
  202. return nil
  203. }
  204. func cancelAppStore(ctx context.Context, platform string) error {
  205. switch platform {
  206. case "ios":
  207. platform = string(asc.PlatformIOS)
  208. case "macos":
  209. platform = string(asc.PlatformMACOS)
  210. case "tvos":
  211. platform = string(asc.PlatformTVOS)
  212. }
  213. tag, err := build_shared.ReadTag()
  214. if err != nil {
  215. return err
  216. }
  217. client := createClient(time.Minute)
  218. for {
  219. log.Info(platform, " list versions")
  220. versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
  221. FilterPlatform: []string{string(platform)},
  222. })
  223. if isRetryable(response) {
  224. continue
  225. } else if err != nil {
  226. return err
  227. }
  228. version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
  229. return *it.Attributes.VersionString == tag
  230. })
  231. if version.ID == "" {
  232. return nil
  233. }
  234. log.Info(platform, " ", tag, " get submission")
  235. submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
  236. if response != nil && response.StatusCode == http.StatusNotFound {
  237. return nil
  238. }
  239. if isRetryable(response) {
  240. continue
  241. } else if err != nil {
  242. return err
  243. }
  244. log.Info(platform, " ", tag, " delete submission")
  245. _, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
  246. if err != nil {
  247. return err
  248. }
  249. return nil
  250. }
  251. }
  252. func prepareAppStore(ctx context.Context) error {
  253. tag, err := build_shared.ReadTag()
  254. if err != nil {
  255. return err
  256. }
  257. client := createClient(time.Minute)
  258. for _, platform := range []asc.Platform{
  259. asc.PlatformIOS,
  260. asc.PlatformMACOS,
  261. asc.PlatformTVOS,
  262. } {
  263. log.Info(string(platform), " list versions")
  264. versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
  265. FilterPlatform: []string{string(platform)},
  266. })
  267. if err != nil {
  268. return err
  269. }
  270. version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
  271. return *it.Attributes.VersionString == tag
  272. })
  273. log.Info(string(platform), " ", tag, " list builds")
  274. builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
  275. FilterApp: []string{appID},
  276. FilterPreReleaseVersionPlatform: []string{string(platform)},
  277. })
  278. if err != nil {
  279. return err
  280. }
  281. if len(builds.Data) == 0 {
  282. log.Fatal(platform, " ", tag, " no build found")
  283. }
  284. buildID := common.Ptr(builds.Data[0].ID)
  285. if version.ID == "" {
  286. log.Info(string(platform), " ", tag, " create version")
  287. newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
  288. Platform: platform,
  289. VersionString: tag,
  290. }, appID, buildID)
  291. if err != nil {
  292. return err
  293. }
  294. version = newVersion.Data
  295. } else {
  296. log.Info(string(platform), " ", tag, " check build")
  297. currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
  298. if err != nil {
  299. return err
  300. }
  301. if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
  302. switch *version.Attributes.AppStoreState {
  303. case asc.AppStoreVersionStatePrepareForSubmission,
  304. asc.AppStoreVersionStateRejected,
  305. asc.AppStoreVersionStateDeveloperRejected:
  306. case asc.AppStoreVersionStateWaitingForReview,
  307. asc.AppStoreVersionStateInReview,
  308. asc.AppStoreVersionStatePendingDeveloperRelease:
  309. submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
  310. if err != nil {
  311. return err
  312. }
  313. if submission != nil {
  314. log.Info(string(platform), " ", tag, " delete submission")
  315. _, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
  316. if err != nil {
  317. return err
  318. }
  319. time.Sleep(5 * time.Second)
  320. }
  321. default:
  322. log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
  323. }
  324. log.Info(string(platform), " ", tag, " update build")
  325. response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
  326. if err != nil {
  327. return err
  328. }
  329. if response.StatusCode != http.StatusNoContent {
  330. response.Write(os.Stderr)
  331. log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
  332. }
  333. } else {
  334. switch *version.Attributes.AppStoreState {
  335. case asc.AppStoreVersionStatePrepareForSubmission,
  336. asc.AppStoreVersionStateRejected,
  337. asc.AppStoreVersionStateDeveloperRejected:
  338. case asc.AppStoreVersionStateWaitingForReview,
  339. asc.AppStoreVersionStateInReview,
  340. asc.AppStoreVersionStatePendingDeveloperRelease:
  341. continue
  342. default:
  343. log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
  344. }
  345. }
  346. }
  347. log.Info(string(platform), " ", tag, " list localization")
  348. localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
  349. if err != nil {
  350. return err
  351. }
  352. localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
  353. return *it.Attributes.Locale == "en-US"
  354. })
  355. if localization.ID == "" {
  356. log.Info(string(platform), " ", tag, " no en-US localization found")
  357. }
  358. if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
  359. log.Info(string(platform), " ", tag, " update localization")
  360. _, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
  361. PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
  362. WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
  363. })
  364. if err != nil {
  365. return err
  366. }
  367. }
  368. log.Info(string(platform), " ", tag, " create submission")
  369. fixSubmit:
  370. for {
  371. _, response, err := client.Submission.CreateSubmission(ctx, version.ID)
  372. if err != nil {
  373. switch response.StatusCode {
  374. case http.StatusInternalServerError:
  375. continue
  376. default:
  377. return err
  378. }
  379. }
  380. switch response.StatusCode {
  381. case http.StatusCreated:
  382. break fixSubmit
  383. default:
  384. return err
  385. }
  386. }
  387. }
  388. return nil
  389. }
  390. func publishAppStore(ctx context.Context) error {
  391. tag, err := build_shared.ReadTag()
  392. if err != nil {
  393. return err
  394. }
  395. client := createClient(time.Minute)
  396. for _, platform := range []asc.Platform{
  397. asc.PlatformIOS,
  398. asc.PlatformMACOS,
  399. asc.PlatformTVOS,
  400. } {
  401. log.Info(string(platform), " list versions")
  402. versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
  403. FilterPlatform: []string{string(platform)},
  404. })
  405. if err != nil {
  406. return err
  407. }
  408. version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
  409. return *it.Attributes.VersionString == tag
  410. })
  411. switch *version.Attributes.AppStoreState {
  412. case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
  413. log.Fatal(string(platform), " ", tag, " not submitted")
  414. case asc.AppStoreVersionStateWaitingForReview,
  415. asc.AppStoreVersionStateInReview:
  416. log.Warn(string(platform), " ", tag, " waiting for review")
  417. continue
  418. case asc.AppStoreVersionStatePendingDeveloperRelease:
  419. default:
  420. log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
  421. }
  422. _, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
  423. if err != nil {
  424. return err
  425. }
  426. }
  427. return nil
  428. }
  429. func isRetryable(response *asc.Response) bool {
  430. if response == nil {
  431. return false
  432. }
  433. switch response.StatusCode {
  434. case http.StatusInternalServerError, http.StatusUnprocessableEntity:
  435. return true
  436. default:
  437. return false
  438. }
  439. }