main.go 13 KB

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