Skip to content

asc: App Store Connect client with auth apple, ios upload and ios submit - #20

Merged
Interlap01 merged 13 commits into
mainfrom
feat/asc-client
Sep 17, 2026
Merged

Interlap01 merged 13 commits into
mainfrom
feat/asc-client

Conversation

@Interlap01

@Interlap01 Interlap01 commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A native Go client for the App Store Connect REST API, so the build → TestFlight → App Store path works from Windows, Linux and WSL with no Mac, altool or Transporter. All ASC calls run on the user's machine; the runner is untouched.

  • internal/asc: ES256 JWT from the API key (kid header, aud appstoreconnect-v1, 15-minute expiry, refreshed early), JSON:API client with typed error decoding, pagination, 429/5xx retry with injectable sleep, and typed helpers for apps, builds, buildUploads/buildUploadFiles, beta groups, beta build localizations, beta app review submissions, app store versions, review submissions.
  • internal/ipa: reads bundle ID, versions and ITSAppUsesNonExemptEncryption from an IPA's Info.plist; internal/dev now uses it instead of its own extractor.
  • builder auth apple [--issuer-id --key-id --key]: stores the key in the OS keyring like the other providers; ASC_ISSUER_ID/ASC_KEY_ID/ASC_KEY_PATH or ASC_PRIVATE_KEY override it for CI and agents. auth status and auth logout apple cover it.
  • builder ios upload [--ipa] [--wait] [--timeout] [--no-encryption] [--json]: newest IPA in ./dist/ by default, resolves the app by bundle ID, runs the chunked upload against presigned URLs streaming from disk, optionally waits for processing and clears the export-compliance flag when the plist declares no non-exempt encryption.
  • builder ios submit --testflight [--group]... [--notes] [--build-number] [--wait]: what-to-test notes, adds the build to the named groups, creates the beta review submission for external groups.
  • builder ios submit --app-store [--version] [--build-number] [--release manual|after-approval]: finds or creates the version, attaches the build, sets the release type, creates and submits the review submission; ASC's 409 metadata errors are surfaced with a hint.
  • Every new command has --json (JSON only on stdout, progress on stderr) and non-zero exit codes; no prompts outside auth apple.
  • Wire shapes were checked against Apple's documentation and the asc-cli models; the README credits asc-cli as the reference that proved the Mac-free upload path.

Test plan

  • go build ./... && go vet ./... && go test ./..., gofmt -l . clean, golangci-lint v2.12.2 0 issues
  • httptest coverage: JWT claims and signature, error decoding, pagination, retry policy, full chunked upload with byte reassembly, both submit flows
  • builder auth apple with a real key, then builder ios upload --wait of a Release IPA signed with an App Store profile
  • builder ios submit --testflight --group Internal and confirm the build appears in TestFlight
  • builder ios submit --app-store --release manual on an app with completed metadata

ES256 JWT auth from the .p8 key (15 minute tokens, cached and refreshed
before expiry), generic JSON:API documents with pagination over links.next,
typed decoding of the errors[] array, and retries on 429 for every method
and on 5xx for idempotent ones only.

Typed helpers cover what upload and submit need: apps by bundle ID, builds
(list/filter, processing state, export compliance, beta group linkage), the
buildUploads/buildUploadFiles chunked delivery with state polling, beta
groups, beta build localizations, beta app review submissions, App Store
versions and review submissions. Everything runs from the developer's
machine; the runner is not involved.
builder auth apple saves the issuer ID, key ID and .p8 key as one secret
through the same keyring/file storage the CI tokens use, after checking the
key against the API. Flags left out are prompted for on a terminal; without
one the command asks for the flags or the ASC_ISSUER_ID, ASC_KEY_ID and
ASC_PRIVATE_KEY/ASC_KEY_PATH variables, which always take precedence so CI
jobs and agents need no keychain. auth status and auth logout apple cover
the new login.
builder ios upload reads the bundle ID, version, build number and
ITSAppUsesNonExemptEncryption from the newest IPA in dist/ (or --ipa),
resolves the app and runs the buildUploads flow: create the delivery,
reserve the file, PUT the chunks to the presigned URLs with their request
headers, commit. With --wait it polls the delivery until COMPLETE, then the
build until VALID, surfacing App Store Connect's error details on failure,
and answers the export compliance question when the plist declares no
non-exempt encryption or --no-encryption is given. --json prints the result
for agents.

Info.plist reading moves from internal/dev into internal/ipa so both the dev
session and the upload share it.
builder ios submit --testflight picks the newest VALID build (or
--build-number), sets the What to Test notes in the app's primary locale,
submits the build for beta review when a chosen group is external and adds
it to the named groups; without --group it reports the build and lists the
groups. --wait follows the beta review decision.

builder ios submit --app-store finds or creates the App Store version for
the marketing version, attaches the build, sets the release type, reuses an
open review submission or creates one, adds the version and submits it.
App Store Connect's 409/422 state errors, nearly always incomplete metadata,
are rewritten with a hint to finish it in App Store Connect or with asc-cli.
README gets a TestFlight and App Store section after Code Signing covering
the API key, upload, TestFlight and App Review steps, the build number and
export compliance rules, and credits asc-cli as the reference that proved the
Mac-free buildUploads path. CLAUDE.md documents the asc, distribute and ipa
packages and the client, credential, upload, compliance and submit-order
patterns.
Every retry and poll now sleeps through Client.sleep, so the 429 test
asserts the Retry-After it was handed instead of waiting a real second,
and status polls grow 1.5x per round up to 4x the base interval.

Remove ListApps, GetBuildUploadFile and Error.HasCode, which nothing
called, and parse the private key once instead of twice on NewClient.
The beta review wait moves into asc as WaitForBetaAppReview beside the
other waits. Options structs over 80 bytes are passed by pointer, and
the tests check their JSON type assertions, both of which golangci-lint
v2.12.2 flags in CI.
finish took the result as any, so a typed nil pointer on failure was
encoded as "null" on stdout in --json mode; a generic finish[T] sees
the nil. The missing-credentials error now names the environment
variables next to builder auth apple, and the key-rejected error is
lower-case for staticcheck.
On Linux and WSL the login is written to a 0600 file in the config dir,
not a keychain, so the auth apple confirmation was wrong there. Word it
like the other provider logins instead.
Drop comments that only restated the function below them (getOne, post,
patch, sleep, utiFor, logf, pollInterval, resolveIPA, getASCClient), cut
the package doc for asc to what is not already in CLAUDE.md, and compress
the six CLAUDE.md bullets this branch added to three lines each.
@Interlap01
Interlap01 merged commit 741a0ac into main Sep 17, 2026
8 checks passed
@Interlap01
Interlap01 deleted the feat/asc-client branch September 17, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant