development
md 258 lines 12 KB
Rendered Raw
1 # AGENTS.md
2
3 This file provides guidance to AI coding agents (siGit Code, Claude Code, etc.) when working with code in this repository. It is the canonical agent doc, symlinked from the repo root as both `AGENTS.md` and `CLAUDE.md`.
4
5 Guidance for AI coding agents working in this repository.
6
7 ## What this is
8
9 `smbcloud-ascapi` is a Rust workspace wrapping the App Store Connect API. It
10 ships two front ends over one shared core:
11
12 - the `asc` CLI (`crates/cli`), and
13 - an MCP stdio server (`crates/mcp`, reachable via `asc --mcp`).
14
15 Both go through `smbcloud-ascapi-frontend` so they can't drift. See `README.md`
16 for the full crate table.
17
18 ## Workspace layout
19
20 Domain crates (`aso`, `pricing`, `signing`, `reports`) know nothing about each
21 other or the front ends. Each extends `smbcloud_ascapi_core::Client` with an
22 **extension trait** (Rust only allows inherent impls in the crate that owns the
23 type). Import a domain's `prelude::*` to pull its traits in at once.
24
25 ## Conventions
26
27 - JSON:API responses deserialize through `ListDocument<T>` / the core
28 envelopes; `Resource.id` is the ASC id.
29 - CLI `ValueEnum` wrappers (`CliPlatform`, `CliBundleIdPlatform`,
30 `CliProfileType`, …) wrap the domain enums so clap owns the arg parsing;
31 map them to the domain type in the match arm (e.g. `.map(Platform::from)`).
32 - Tests live in-module: `#[cfg(test)] mod tests { use super::*; … }` with plain
33 `#[test]` fns (see `crates/aso/src/bundle_id.rs`, `crates/aso/src/build.rs`).
34 - Pin exact ASC filter keys in assertions — e.g.
35 `filter[preReleaseVersion.version]` vs `filter[version]`; the wrong key
36 silently returns the wrong rows.
37
38 ## Builds API (CLI)
39
40 All are subcommands of `apps` (clap kebab-cases the command names):
41
42 ```sh
43 # GET /v1/builds?filter[app]={id} — newest first. Check processingState
44 # (VALID / INVALID / PROCESSING / FAILED).
45 asc apps builds <APP_ID> \
46 [--version <BUILD_NUMBER>] \ # filter[version], e.g. 42
47 [--pre-release-version <MARKETING>] \ # filter[preReleaseVersion.version], e.g. 1.1.0
48 [--processing-state <STATE>] # filter[processingState]
49
50 # GET /v1/builds/{id} — a single build by its ASC id.
51 asc apps build <BUILD_ID>
52
53 # PATCH /v1/builds/{id} usesNonExemptEncryption — answer export compliance on a
54 # build showing "Missing Compliance". Without an answer, review-submissions
55 # submit fails with 409 STATE_ERROR.ENTITY_STATE_INVALID. false = "None of the
56 # algorithms mentioned above" (HTTPS through Apple's OS only).
57 asc apps set-encryption <BUILD_ID> --uses-non-exempt-encryption false
58
59 # GET /v1/preReleaseVersions?filter[app]={id} — the marketing version strings
60 # TestFlight groups builds under.
61 asc apps pre-release-versions <APP_ID> [--platform <ios|mac-os|tv-os|vision-os>]
62
63 # xcrun altool --upload-package, authenticated with the same key id, issuer
64 # and .p8 (--p8-file-path) as every other command. --dry-run prints the argv.
65 asc apps upload <PATH.ipa|PATH.pkg> [--wait]
66 ```
67
68 The underlying API lives in `crates/aso/src/build.rs`: `BuildsApi` (in the
69 `aso` prelude) plus `BuildFilter`, whose private `query_pairs` maps each set
70 field to its ASC filter key. The upload lives in
71 `crates/frontend/src/upload.rs`. It always passes `--p8-file-path` and never
72 `--auth-string`: letting altool search its key directories can pick up a
73 stale `AuthKey_<id>.p8` and fail with a 401, and `--auth-string` would put
74 the key in the process table. Upload is CLI-only, because a tool call that
75 blocks for minutes and publishes a build doesn't belong in MCP.
76
77 ## Git branching workflow
78
79 Branches flow in one direction: `main` ← `development` ← feature branch.
80
81 - `main` is the release branch. Never commit to it directly; it only advances
82 by merging `development`.
83 - `development` is the integration/base branch. Never commit to it directly
84 either — every change lands through a pull request.
85 - Do the work on a short-lived **feature branch** cut from `development`
86 (`feat/…`, `fix/…`, `docs/…`), then **open a PR into `development`**. Let CI
87 (fmt / clippy `-D warnings` / test / doc / lower-crate solo builds) pass
88 before merge.
89 - When `development` is ready to release, it is merged into `main` via its own
90 PR — not by pushing commits onto `main`.
91
92 So: branch off `development`, push the feature branch, open a PR → `development`.
93 Do not commit straight to `development` or `main`.
94
95 ## Verify before committing
96
97 CI (`.github/workflows/ci.yml`, ubuntu + macOS) runs these, and warnings are
98 errors, so run the strict forms locally:
99
100 ```sh
101 cargo fmt --all -- --check
102 cargo clippy --workspace --all-targets --locked -- -D warnings
103 cargo test --workspace --locked
104 RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace --locked
105 cargo check -p smbcloud-ascapi-core --locked # also -aso, -play, -signing: each lower crate must build alone
106 ```
107
108 Single test: `cargo test -p smbcloud-ascapi-aso build::tests::<name>`.
109 Intra-doc links are load-bearing (a broken one fails CI), and the CSR tests
110 shell out to `openssl`, whose output differs between OpenSSL 3 and macOS
111 LibreSSL.
112
113 ## Verify before relying on a freshly built `asc`
114
115 The released/installed `asc` binary can silently lag `development`. This
116 hides because `asc apps upload` only shells out to `xcrun altool` and
117 doesn't touch newer code paths, so an upload succeeds against a stale
118 binary that lacks e.g. the `testflight` subcommand. Before relying on a
119 newly-added command, rebuild from `development`:
120
121 ```sh
122 cargo build --release
123 # or, to be sure you're on the current branch's binary:
124 cargo install --path crates/cli --locked
125 ```
126
127 `unrecognized subcommand` on a command that's been merged is the symptom;
128 the cure is a rebuild, not a workaround.
129
130 ## Architecture notes
131
132 - Layering is `core` → `aso`/`pricing`/`signing`/`reports` → `frontend` →
133 `mcp` / `cli`. Don't add a dependency that points up or sideways; CI checks
134 the lower crates build alone.
135 - The MCP tool set is declared by the `ascapi_mcp_tools!` macro
136 (`crates/mcp/src/lib.rs`), which takes every tool name as a literal;
137 `crates/mcp/src/server.rs` supplies them. Adding a tool means touching the
138 macro, the invocation, and `requests.rs`.
139 - stdout belongs to the MCP JSON-RPC stream: nothing under a tool call may
140 `println!`. Results sent to a model never contain private keys or
141 certificate/profile bodies; return the path written instead. That is
142 enforced by the result types in `crates/frontend`, so keep new result types
143 free of key material rather than filtering at the edge.
144 - Destructive or irreversible operations stay CLI-only (certificate revoke,
145 review submission, upload). Recreatable deletes may be MCP tools, annotated
146 destructive.
147 - `.agents/plans/` holds design plans from past release sessions; check them
148 before reworking upload or signing-identity behaviour.
149
150 ## Credentials
151
152 The CLI needs an ASC API key: `--key-id`/`ASC_API_KEY`,
153 `--issuer-id`/`ASC_ISSUER_ID`, and the `.p8` at
154 `--private-key-path`/`ASC_PRIVATE_KEY_PATH` (defaults to
155 `~/.appstoreconnect/private_keys/AuthKey_<key-id>.p8`). `--mcp` resolves
156 credentials per tool call, so the server can list tools unconfigured.
157
158 ### App Store Connect key location
159
160 `ascapi` itself defaults to `~/.appstoreconnect/private_keys/AuthKey_<key-id>.p8`.
161 Some sibling repos use a different default and can drift when two copies of
162 the same key id are on disk:
163
164 - `onde-store/scripts/build-ios.sh` and `smbcloud-browser/scripts/build-ios.sh`
165 default to `~/private_keys/AuthKey_<id>.p8`.
166
167 A stale `AuthKey_<id>.p8` with the same id surfaces as an indistinguishable
168 401 on ASC API calls — ASC says "valid key id, signature doesn't verify."
169 `ascapi apps upload` dodges the half of the drift that goes through altool
170 by passing `--p8-file-path` explicitly (so altool can't search for a stale
171 key). For every other command the resolution rule is the one above; if a
172 401 appears where it shouldn't, the first thing to check is which `.p8`
173 both sides are actually reading. If you need the scripts to use a single
174 key file, set `ASC_PRIVATE_KEY_PATH` explicitly in the environment
175 instead of relying on either default.
176
177 ## Finish-the-release recipe (TestFlight)
178
179 The shortest path from a signed `.pkg` to a TestFlight group:
180
181 1. **Upload:** `asc apps upload <PKG> --wait` (always passes
182 `--p8-file-path`; never call `altool` by hand — its key search can pick a
183 stale `AuthKey_<id>.p8` and fail with a 401).
184 2. **Poll to VALID:** `asc apps builds <APP_ID> --version <BUILD_NUMBER>`
185 until `processingState` is `VALID`, then read the build id off the
186 response. (`asc apps await-processing` is the non-blocking alternative
187 to `--wait` if you're driving this from CI.)
188 3. **Beta-review contact details** (one-time per app):
189 `asc testflight app-review-detail update ...`.
190 4. **Submit:** find the Public Link group with
191 `asc testflight groups list <APP_ID>`, then either
192 `groups add-build --build-id <BUILD_ID> <GROUP_ID>` or
193 `app-review-submissions submit <BUILD_ID>`. Both are CLI-only — adding
194 to an external group / submitting to Beta App Review can trigger review,
195 which an MCP tool call shouldn't do unattended.
196
197 ## Review submissions (CLI)
198
199 ```sh
200 # GET /v1/appStoreVersions/{id}/build — the build attached to a version, or
201 # null (with "no build attached" on stderr). Run it before submitting.
202 asc app-store-versions build <APP_STORE_VERSION_ID>
203
204 # PATCH the version's build relationship, then read the build back and fail if
205 # it isn't the one requested. Prints the build's id, version, processingState
206 # and usesNonExemptEncryption. --no-verify skips the read-back; --dry-run
207 # stays offline.
208 asc app-store-versions set-build <APP_STORE_VERSION_ID> --build-id <BUILD_ID> [--no-verify]
209
210 # GET /v1/reviewSubmissions?filter[app]={id}
211 asc review-submissions list <APP_ID> [--platform <...>] [--state READY_FOR_REVIEW]
212
213 # Reuse the platform's open (READY_FOR_REVIEW) submission or create one, add
214 # the version as an item, then PATCH submitted: true. --dry-run reads and
215 # prints the plan. Refuses a version that is already WAITING_FOR_REVIEW,
216 # IN_REVIEW or live, before any write.
217 asc review-submissions submit <APP_ID> --version-id <APP_STORE_VERSION_ID>
218
219 # PATCH canceled: true, for a submission App Review hasn't picked up.
220 asc review-submissions cancel <SUBMISSION_ID>
221 ```
222
223 The API is `ReviewSubmissionsApi` in `crates/aso/src/review_submission.rs`;
224 the plan/submit orchestration is `crates/frontend/src/review.rs`. Submitting is
225 CLI-only on purpose: it sends a build to App Review, which an MCP tool call
226 shouldn't do on a model's say-so.
227
228 ## Google Play (CLI)
229
230 `crates/play` is the one non-Apple store backend. It does not depend on
231 `core`: Play authenticates with a service account (RS256 JWT exchanged at
232 `oauth2.googleapis.com/token`, scope `androidpublisher`) and has its own error
233 envelope. The key comes from `--play-service-account`,
234 `PLAY_SERVICE_ACCOUNT_JSON_PATH`, or the contents in `PLAY_SERVICE_ACCOUNT_JSON`.
235 `asc play` does not need the App Store Connect key.
236
237 Everything in Play happens inside an *edit*. Each command opens one, applies,
238 `validate`s, and commits only without `--dry-run`; a dry run prints the body
239 and deletes the edit. Writes read back after commit and fail on any difference.
240
241 ```sh
242 asc play listings list <PACKAGE>
243 asc play listings update <PACKAGE> --locale en-US \
244 --title-file t.txt --short-description-file s.txt --full-description-file d.txt
245 asc play images list <PACKAGE> --locale en-US --type phone-screenshots
246 asc play images replace <PACKAGE> --locale en-US --type phone-screenshots --file a.png --file b.png
247 asc play tracks get <PACKAGE> --track internal
248 asc play tracks set-notes <PACKAGE> --track internal --version-code 120 --locale en-US --notes-file n.txt
249 ```
250
251 Limits linted before sending: title 30, short description 80, full description
252 4000, release notes 500 characters per language; feature graphic exactly
253 1024x500, icon 512x512, screenshots 320 to 3840 px with at most 2:1 aspect.
254 `images replace` deletes the bucket and uploads the files in one edit, so a
255 failed upload keeps the old set. MCP exposes only the reads
256 (`play_listing_list`, `play_images_list`, `play_track_get`); every write stays
257 CLI-only, like review submission. The operations live in
258 `crates/frontend/src/play.rs`.