Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

- **init** — 从 App Store Connect 最新版本下载元数据,按平台和语言保存到本地目录
- **release** — 创建新版本或更新已有版本,将本地元数据同步到 App Store Connect
- **upload** — 使用 Build Upload API 上传 `.ipa` / `.pkg` 构建文件到 App Store Connect
- **latest-build** — 查询指定平台最新的 App Store Connect build 号

## 安装

Expand Down Expand Up @@ -85,6 +87,57 @@ asctl release --app-id <APP_ID> --version 1.2.0 --whats-new "Bug fixes and impro

如果不指定 `--whats-new`,则从各语言目录下的 `whats_new.txt` 读取。

### 上传构建

使用 App Store Connect Build Upload API 上传构建文件:

```bash
asctl upload --app-id <APP_ID> --platform ios --file ./build/MyApp.ipa
```

`.ipa` 文件会自动从 `Payload/*.app/Info.plist` 读取版本号和 build 号;如果需要覆盖,可以显式传入:

```bash
asctl upload \
--app-id <APP_ID> \
--platform ios \
--file ./build/MyApp.ipa \
--version 1.2.0 \
--build 42
```

上传 macOS `.pkg` 时需要显式提供版本号和 build 号:

```bash
asctl upload --app-id <APP_ID> --platform macos --file ./build/MyApp.pkg --version 1.2.0 --build 42
```

如需等待 App Store Connect 完成上传处理:

```bash
asctl upload --app-id <APP_ID> --file ./build/MyApp.ipa --wait
```

### 查询最新 build 号

查询指定平台最新上传的 App Store Connect build 号:

```bash
asctl latest-build --app-id <APP_ID> --platform ios
```

默认只输出 build 号,方便在脚本中使用。需要更多信息时:

```bash
asctl latest-build --app-id <APP_ID> --platform ios --details
```

只查询某种处理状态的 build:

```bash
asctl latest-build --app-id <APP_ID> --platform ios --state valid
```

### 参数说明

**全局参数**
Expand All @@ -111,6 +164,28 @@ asctl release --app-id <APP_ID> --version 1.2.0 --whats-new "Bug fixes and impro
| `--platform` | `-p` | `ios` | 平台:`ios` 或 `macos` |
| `--whats-new` | — | — | 更新说明,覆盖所有语言的 `whats_new.txt` |

**upload 命令**

| 参数 | 简写 | 默认值 | 说明 |
|------|------|--------|------|
| `--app-id` | `-a` | `ASC_APP_ID` | App Store Connect App ID(必填) |
| `--platform` | `-p` | `ios` | 平台:`ios` 或 `macos` |
| `--file` | `-f` | — | 构建文件路径,支持 `.ipa` / `.pkg`(必填) |
| `--version` | `-v` | — | 版本号,`.ipa` 可自动读取 |
| `--build` | `-b` | — | build 号,`.ipa` 可自动读取 |
| `--uti` | — | 按扩展名推断 | 构建文件 UTI 覆盖值 |
| `--wait` | — | `false` | 等待 App Store Connect 完成上传处理 |
| `--wait-timeout` | — | `30m` | `--wait` 的最长等待时间 |

**latest-build 命令**

| 参数 | 简写 | 默认值 | 说明 |
|------|------|--------|------|
| `--app-id` | `-a` | `ASC_APP_ID` | App Store Connect App ID(必填) |
| `--platform` | `-p` | `ios` | 平台:`ios` 或 `macos` |
| `--state` | — | `all` | 处理状态:`all`、`valid`、`processing`、`failed`、`invalid` |
| `--details` | — | `false` | 输出 build ID、处理状态、上传时间等详细信息 |

## 典型工作流

```bash
Expand Down
108 changes: 108 additions & 0 deletions cmd/latest_build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cmd

import (
"fmt"
"os"
"strings"

"github.com/rainbend/appstoreconnect-cli/internal/api"
"github.com/spf13/cobra"
)

var latestBuildCmd = &cobra.Command{
Use: "latest-build",
Short: "Print the latest build number for a platform",
Long: `Print the latest App Store Connect build number for an app platform.

By default the command prints only Build.Attributes.version so it can be used
from scripts. Use --details to print the build ID, processing state, and upload
date as well.`,
RunE: runLatestBuild,
}

func init() {
latestBuildCmd.Flags().StringP("app-id", "a", os.Getenv("ASC_APP_ID"), "App Store Connect App ID (env: ASC_APP_ID)")
latestBuildCmd.Flags().StringP("platform", "p", "ios", "Platform: ios or macos")
latestBuildCmd.Flags().String("state", "all", "Processing state: all, valid, processing, failed, or invalid")
latestBuildCmd.Flags().Bool("details", false, "Print build ID, state, and upload date")
rootCmd.AddCommand(latestBuildCmd)
}

func runLatestBuild(cmd *cobra.Command, args []string) error {
if err := validateAuthFlags(); err != nil {
return err
}

appID, _ := cmd.Flags().GetString("app-id")
if appID == "" {
return fmt.Errorf("--app-id or ASC_APP_ID environment variable is required")
}

platformStr, _ := cmd.Flags().GetString("platform")
platform, err := api.ParsePlatform(platformStr)
if err != nil {
return err
}

stateFlag, _ := cmd.Flags().GetString("state")
state, err := normalizeBuildProcessingState(stateFlag)
if err != nil {
return err
}

client := api.NewClient(keyID, issuerID, privateKeyPath)
build, err := client.LatestBuild(appID, platform, state)
if err != nil {
return fmt.Errorf("fetching latest build: %w", err)
}
if build == nil {
stateText := "any processing state"
if state != "" {
stateText = state
}
return fmt.Errorf("no builds found for platform %s with %s", platformStr, stateText)
}

details, _ := cmd.Flags().GetBool("details")
if !details {
fmt.Println(build.Attributes.Version)
return nil
}

fmt.Printf("Build: %s\n", build.Attributes.Version)
fmt.Printf("ID: %s\n", build.ID)
if build.Attributes.ProcessingState != "" {
fmt.Printf("State: %s\n", build.Attributes.ProcessingState)
}
if build.Attributes.UploadedDate != "" {
fmt.Printf("Uploaded: %s\n", build.Attributes.UploadedDate)
}
if build.Attributes.MinOSVersion != "" {
fmt.Printf("Min OS: %s\n", build.Attributes.MinOSVersion)
}
if build.Attributes.ExpirationDate != "" {
fmt.Printf("Expires: %s\n", build.Attributes.ExpirationDate)
}
if build.Attributes.Expired {
fmt.Println("Expired: true")
}

return nil
}

func normalizeBuildProcessingState(state string) (string, error) {
switch strings.ToLower(strings.TrimSpace(state)) {
case "", "all":
return "", nil
case "valid":
return "VALID", nil
case "processing":
return "PROCESSING", nil
case "failed":
return "FAILED", nil
case "invalid":
return "INVALID", nil
default:
return "", fmt.Errorf("invalid --state %q: must be all, valid, processing, failed, or invalid", state)
}
}
Loading
Loading