Skip to content

Commit 3bfab33

Browse files
committed
Initialize App Store Connect CLI with core functionality and metadata management
- Added main application structure with `main.go` and command handling in `cmd/`. - Implemented `init` command to download metadata from App Store Connect. - Implemented `release` command to create or update app versions with local metadata. - Introduced API client for App Store Connect interactions in `internal/api/`. - Added metadata handling for local storage in `internal/metadata/`. - Created initial documentation in `README.md` and `AGENTS.md`. - Added `.gitignore` and `.env-template` for environment configuration. - Included a Makefile for build and installation processes. - Set up GitHub Actions workflow for automated testing.
0 parents  commit 3bfab33

17 files changed

Lines changed: 995 additions & 0 deletions

File tree

.env-template

Whitespace-only changes.

.github/workflows/test.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-go@v5
16+
with:
17+
go-version-file: go.mod
18+
19+
- name: Run tests
20+
run: go test -v -race -count=1 ./...

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bin/
2+
*.p8
3+
.env
4+
.metadata/

AGENTS.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
## AppStoreConnect-Cli
2+
3+
管理 App Store Connect 应用元数据的命令行工具,使用 Go 实现。
4+
5+
### 功能需求
6+
7+
1. `init` 命令:从 App Store Connect 最新版本下载推广文本、描述、关键字和新增内容,按平台(iOS/macOS)和语言区分,保存到本地 `.metadata/` 目录
8+
2. `release` 命令:创建新版本或更新已有版本,推广文本、描述、关键字从本地 `.metadata/` 读取,新增内容(whats_new)每次发布前更新;版本已存在时触发更新而非报错
9+
3. 认证方式:JWT (ES256),参考 https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api
10+
11+
### 项目结构
12+
13+
```
14+
├── main.go # 入口
15+
├── cmd/
16+
│ ├── root.go # 根命令,全局认证参数 (--key-id, --issuer-id, --private-key)
17+
│ ├── init.go # init 子命令:下载元数据
18+
│ └── release.go # release 子命令:创建/更新版本
19+
├── internal/
20+
│ ├── api/
21+
│ │ ├── client.go # HTTP 客户端,JWT token 生成与缓存
22+
│ │ ├── types.go # App Store Connect API JSON:API 类型定义
23+
│ │ ├── versions.go # 版本相关 API (List, Create)
24+
│ │ └── localizations.go # 本地化相关 API (List, Create, Update)
25+
│ └── metadata/
26+
│ └── metadata.go # 本地 metadata 文件读写 (Read, Write, ListLocales)
27+
└── .metadata/ # 运行时生成的元数据目录(隐藏目录)
28+
└── {platform}/ # ios 或 macos
29+
└── {locale}/ # 如 en-US, zh-Hans
30+
├── description.txt
31+
├── keywords.txt
32+
├── promotional_text.txt
33+
└── whats_new.txt
34+
```
35+
36+
### 技术栈
37+
38+
- Go 1.23+
39+
- github.com/spf13/cobra — CLI 框架
40+
- github.com/golang-jwt/jwt/v5 — JWT 签名 (ES256)
41+
- App Store Connect API v1 (JSON:API 格式),Base URL: `https://api.appstoreconnect.apple.com/v1`
42+
43+
### 关键设计
44+
45+
- **认证**:通过 .p8 私钥文件生成 ES256 JWT,token 缓存 15 分钟自动刷新。三个参数支持命令行 flag 和环境变量 (`ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_PRIVATE_KEY`)
46+
- **API 客户端** (`internal/api/client.go`):`Client.do(method, path, body)` 是核心请求方法,自动附加 Bearer token,处理 JSON 序列化和错误响应
47+
- **类型系统** (`internal/api/types.go`):使用 Go 泛型 `ListResponse[T]` / `SingleResponse[T]` 解析 JSON:API 响应;请求体通过 `Create*Request` / `Update*Request` 结构体构建
48+
- **平台映射**:CLI 使用 `ios`/`macos`,API 使用 `IOS`/`MAC_OS`,通过 `ParsePlatform()` 转换
49+
- **元数据存储**:每个字段一个纯文本文件,方便直接编辑和版本控制
50+
- **release 流程**:先查版本是否存在 → 不存在则创建 → 读取本地 metadata → 对比远端已有 localization → 存在则 PATCH 更新,不存在则 POST 创建
51+
- `--whats-new` flag 可一次性覆盖所有语言的 whats_new,否则从各语言的 `whats_new.txt` 读取
52+
53+
### 编码规范
54+
55+
- 使用 `internal/` 包限制内部实现不被外部引用
56+
- API 方法按资源拆分文件(versions.go, localizations.go),新增资源类型时新建文件
57+
- 错误处理使用 `fmt.Errorf("context: %w", err)` 包装
58+
- CLI 输出使用 `fmt.Printf` 打印进度,警告用 `fmt.Fprintf(os.Stderr, ...)`

Makefile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
BINARY := asctl
2+
MODULE := $(shell go list -m)
3+
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
4+
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
5+
DATE := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
6+
7+
LDFLAGS := -s -w \
8+
-X '$(MODULE)/cmd.version=$(VERSION)' \
9+
-X '$(MODULE)/cmd.commit=$(COMMIT)' \
10+
-X '$(MODULE)/cmd.date=$(DATE)'
11+
12+
.PHONY: build clean install lint test
13+
14+
build:
15+
@mkdir -p bin
16+
go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) .
17+
18+
install:
19+
go build -ldflags "$(LDFLAGS)" -o $(shell go env GOPATH)/bin/$(BINARY) .
20+
21+
clean:
22+
rm -rf bin
23+
24+
lint:
25+
go vet ./...
26+
27+
test:
28+
go test ./...

README.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# asctl
2+
3+
管理 App Store Connect 应用元数据的命令行工具。
4+
5+
从 App Store Connect 下载应用的描述、关键字、推广文本等元数据到本地纯文本文件,方便编辑和版本控制;发布时将本地元数据同步到新版本。
6+
7+
## 功能
8+
9+
- **init** — 从 App Store Connect 最新版本下载元数据,按平台和语言保存到本地目录
10+
- **release** — 创建新版本或更新已有版本,将本地元数据同步到 App Store Connect
11+
12+
## 安装
13+
14+
```bash
15+
go install github.com/rainbend/appstoreconnect-cli@latest
16+
```
17+
18+
或从源码构建:
19+
20+
```bash
21+
git clone https://github.com/rainbend/appstoreconnect-cli.git
22+
cd appstoreconnect-cli
23+
make build # 输出到 bin/asctl
24+
make install # 安装到 $GOPATH/bin/asctl
25+
```
26+
27+
## 前置准备
28+
29+
需要一个 App Store Connect API 密钥(.p8 文件)。参考 [创建 API 密钥](https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api) 获取以下信息:
30+
31+
- **Key ID** — API 密钥 ID
32+
- **Issuer ID** — 颁发者 ID
33+
- **Private Key** — 下载的 `.p8` 私钥文件路径
34+
35+
认证参数可通过命令行 flag、环境变量或项目根目录的 `.env` 文件提供:
36+
37+
| Flag | 环境变量 | 说明 |
38+
|------|---------|------|
39+
| `--key-id` | `ASC_KEY_ID` | API Key ID |
40+
| `--issuer-id` | `ASC_ISSUER_ID` | Issuer ID |
41+
| `--private-key` | `ASC_PRIVATE_KEY` | .p8 私钥文件路径(默认 `~/.config/appstoreconnect/AuthKey_{KEY_ID}.p8`|
42+
43+
## 使用
44+
45+
### 初始化元数据
46+
47+
从 App Store Connect 下载最新版本的所有本地化元数据到本地:
48+
49+
```bash
50+
asctl init --app-id <APP_ID> --platform ios
51+
```
52+
53+
执行后会在 `.metadata/` 目录下生成如下结构:
54+
55+
```
56+
.metadata/
57+
└── ios/
58+
├── en-US/
59+
│ ├── description.txt
60+
│ ├── keywords.txt
61+
│ ├── promotional_text.txt
62+
│ └── whats_new.txt
63+
└── zh-Hans/
64+
├── description.txt
65+
├── keywords.txt
66+
├── promotional_text.txt
67+
└── whats_new.txt
68+
```
69+
70+
每个字段对应一个纯文本文件,直接编辑即可。
71+
72+
### 发布版本
73+
74+
将本地元数据同步到 App Store Connect,如果版本不存在会自动创建:
75+
76+
```bash
77+
asctl release --app-id <APP_ID> --version 1.2.0 --platform ios
78+
```
79+
80+
使用 `--whats-new` 一次性为所有语言设置更新说明:
81+
82+
```bash
83+
asctl release --app-id <APP_ID> --version 1.2.0 --whats-new "Bug fixes and improvements"
84+
```
85+
86+
如果不指定 `--whats-new`,则从各语言目录下的 `whats_new.txt` 读取。
87+
88+
### 参数说明
89+
90+
**全局参数**
91+
92+
| 参数 | 环境变量 | 说明 |
93+
|------|---------|------|
94+
| `--key-id` | `ASC_KEY_ID` | API Key ID(必填) |
95+
| `--issuer-id` | `ASC_ISSUER_ID` | Issuer ID(必填) |
96+
| `--private-key` | `ASC_PRIVATE_KEY` | .p8 私钥文件路径(默认 `~/.config/appstoreconnect/AuthKey_{KEY_ID}.p8`|
97+
98+
**init 命令**
99+
100+
| 参数 | 简写 | 默认值 | 说明 |
101+
|------|------|--------|------|
102+
| `--app-id` | `-a` | `ASC_APP_ID` | App Store Connect App ID(必填) |
103+
| `--platform` | `-p` | `ios` | 平台:`ios``macos` |
104+
105+
**release 命令**
106+
107+
| 参数 | 简写 | 默认值 | 说明 |
108+
|------|------|--------|------|
109+
| `--app-id` | `-a` | `ASC_APP_ID` | App Store Connect App ID(必填) |
110+
| `--version` | `-v` || 版本号,如 `1.2.0`(必填) |
111+
| `--platform` | `-p` | `ios` | 平台:`ios``macos` |
112+
| `--whats-new` ||| 更新说明,覆盖所有语言的 `whats_new.txt` |
113+
114+
## 典型工作流
115+
116+
```bash
117+
# 1. 设置环境变量(或创建 .env 文件)
118+
export ASC_KEY_ID="your-key-id"
119+
export ASC_ISSUER_ID="your-issuer-id"
120+
export ASC_PRIVATE_KEY="/path/to/AuthKey.p8"
121+
export ASC_APP_ID="123456789"
122+
123+
# 2. 首次初始化,拉取现有元数据
124+
asctl init
125+
126+
# 3. 编辑本地元数据文件
127+
vim .metadata/ios/en-US/description.txt
128+
129+
# 4. 发布新版本
130+
asctl release --version 2.0.0 --whats-new "全新设计"
131+
```
132+
133+
## License
134+
135+
MIT

cmd/init.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/rainbend/appstoreconnect-cli/internal/api"
8+
"github.com/rainbend/appstoreconnect-cli/internal/metadata"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var initCmd = &cobra.Command{
13+
Use: "init",
14+
Short: "Initialize local metadata from App Store Connect",
15+
Long: `Download promotional text, description, keywords and what's new
16+
from the latest version on App Store Connect, organized by platform and locale.
17+
18+
The metadata is saved to:
19+
.metadata/{platform}/{locale}/description.txt
20+
.metadata/{platform}/{locale}/keywords.txt
21+
.metadata/{platform}/{locale}/promotional_text.txt
22+
.metadata/{platform}/{locale}/whats_new.txt`,
23+
RunE: runInit,
24+
}
25+
26+
func init() {
27+
initCmd.Flags().StringP("app-id", "a", os.Getenv("ASC_APP_ID"), "App Store Connect App ID (env: ASC_APP_ID)")
28+
initCmd.Flags().StringP("platform", "p", "ios", "Platform: ios or macos")
29+
rootCmd.AddCommand(initCmd)
30+
}
31+
32+
func runInit(cmd *cobra.Command, args []string) error {
33+
if err := validateAuthFlags(); err != nil {
34+
return err
35+
}
36+
37+
appID, _ := cmd.Flags().GetString("app-id")
38+
if appID == "" {
39+
return fmt.Errorf("--app-id or ASC_APP_ID environment variable is required")
40+
}
41+
platformStr, _ := cmd.Flags().GetString("platform")
42+
43+
platform, err := api.ParsePlatform(platformStr)
44+
if err != nil {
45+
return err
46+
}
47+
48+
client := api.NewClient(keyID, issuerID, privateKeyPath)
49+
50+
fmt.Printf("Fetching latest %s version...\n", platformStr)
51+
versions, err := client.ListAppStoreVersions(appID, platform)
52+
if err != nil {
53+
return fmt.Errorf("listing versions: %w", err)
54+
}
55+
if len(versions) == 0 {
56+
return fmt.Errorf("no versions found for platform %s", platformStr)
57+
}
58+
59+
version := versions[0]
60+
fmt.Printf("Found version %s (%s)\n", version.Attributes.VersionString, version.Attributes.AppStoreState)
61+
62+
localizations, err := client.ListVersionLocalizations(version.ID)
63+
if err != nil {
64+
return fmt.Errorf("listing localizations: %w", err)
65+
}
66+
if len(localizations) == 0 {
67+
return fmt.Errorf("no localizations found for version %s", version.Attributes.VersionString)
68+
}
69+
70+
fmt.Printf("Saving %d localization(s)...\n", len(localizations))
71+
72+
for _, loc := range localizations {
73+
m := metadata.Localization{
74+
Description: loc.Attributes.Description,
75+
Keywords: loc.Attributes.Keywords,
76+
PromotionalText: loc.Attributes.PromotionalText,
77+
WhatsNew: loc.Attributes.WhatsNew,
78+
}
79+
if err := metadata.Write(platformStr, loc.Attributes.Locale, m); err != nil {
80+
fmt.Fprintf(os.Stderr, "Warning: failed to write %s: %v\n", loc.Attributes.Locale, err)
81+
continue
82+
}
83+
fmt.Printf(" %s\n", loc.Attributes.Locale)
84+
}
85+
86+
fmt.Println("Done! Metadata saved to .metadata/ directory.")
87+
return nil
88+
}

0 commit comments

Comments
 (0)