ascdir makes recurring App Store release workflows reviewable. It manages
App Store Connect metadata and product-page assets as files, previews changes
before applying them, distributes processed builds through TestFlight, and
submits versions for App Review.
- Keep short values in self-documenting YAML and long-form content in Markdown
- Pull existing metadata from App Store Connect
- Preview concise field- and asset-level changes before writing with
push --dry-run - Protect non-empty remote fields from accidental clearing
- Validate common App Store character limits and URLs locally
- Manage multiple locales from one
ascdir.yaml - Authenticate with App Store Connect API keys
- Create an App Store version, select a processed build, and submit it for review
- Release an approved manual-release version without opening App Store Connect
- Distribute an uploaded build to existing internal or external TestFlight groups
- Follow paginated responses and retry rate limits and transient failures
- Run without telemetry or credential uploads
With Homebrew on macOS or Linux:
brew install Arata1202/tap/ascdirWith aqua:
Initialize the project first if the current directory does not already contain an aqua.yaml:
aqua initThen add and install ascdir:
aqua g -i Arata1202/ascdir
aqua installWith mise through its aqua backend:
mise use -g aqua:Arata1202/ascdirDownload the archive for your platform from GitHub Releases, extract it, and place ascdir (or ascdir.exe on Windows) on your PATH.
On macOS or Linux, the checksum-verifying installer can do this automatically:
curl -fsSLO https://github.com/Arata1202/ascdir/releases/latest/download/install.sh
sh install.sh
rm install.shSet ASCDIR_VERSION to pin the installer in automation:
ASCDIR_VERSION=v1.2.1 sh install.shFor example, in GitHub Actions:
- name: Install ascdir
env:
ASCDIR_VERSION: v1.2.1
run: |
curl -fsSLO "https://github.com/Arata1202/ascdir/releases/download/${ASCDIR_VERSION}/install.sh"
sh install.sh
rm install.shVerify the archive before extracting it:
# Linux
sha256sum --check checksums.txt --ignore-missing
# macOS
shasum -a 256 ascdir_*.tar.gz
# Windows PowerShell
Get-FileHash .\ascdir_*.zip -Algorithm SHA256Compare the printed digest with the corresponding entry in checksums.txt.
Release archives also have signed build provenance generated by GitHub Actions. With the GitHub CLI, verify that an archive was built by this repository:
gh attestation verify ascdir_*.tar.gz --repo Arata1202/ascdirUse the corresponding .zip filename on Windows. Each release also includes a software bill of materials (SBOM) for its archives.
Alternatively, install from source with Go 1.26.6 or later:
go install github.com/Arata1202/ascdir/cmd/ascdir@latestTo build the current source tree instead:
git clone https://github.com/Arata1202/ascdir.git
cd ascdir
make buildRelease binaries support macOS, Linux, and Windows on AMD64 and ARM64. The installer supports macOS and Linux; use Homebrew, aqua, mise, go install, or a release archive on other systems.
Create an App Store Connect API key, download its .p8 private key once, and run:
ascdir auth login
ascdir auth checkThe login command stores the issuer ID, key ID, and absolute path to the private key in your user configuration directory. The private key itself is not copied. For CI or temporary overrides, set:
export ASC_ISSUER_ID="00000000-0000-0000-0000-000000000000"
export ASC_KEY_ID="ABC123DEFG"
export ASC_PRIVATE_KEY_PATH="$HOME/.private_keys/AuthKey_ABC123DEFG.p8"Never commit the private key. ascdir ignores *.p8 and .env by default, but credentials should still be stored outside the repository or in a CI secret store.
The HTTP timeout defaults to 10 minutes so large App Preview transfers can complete on slower connections. Set ASCDIR_TIMEOUT to a positive Go duration such as 15m when needed.
Remove locally stored credentials without deleting the .p8 private key:
ascdir auth logoutInitialize a project from an existing App Store version:
ascdir init \
--bundle-id com.example.myapp \
--platform IOS \
--version 1.2.0This creates ascdir.yaml with short metadata values and downloads long-form content under metadata/. All configured paths are relative to the directory containing ascdir.yaml. A project that also opts into asset management commonly has this layout:
project/
ascdir.yaml
metadata/
en-US/
description.md
promotional_text.md
whats_new.md
assets/
screenshots/
app-previews/
privacy_policy.md is generated only for TV_OS. A custom license_agreement.md is present only when the project manages a custom EULA.
After creating the project, init groups empty managed values by their YAML or file location and prints the exact check and push --dry-run commands to run next. The minimal example is a safe starting point. Copy only the fields you want ascdir to manage; omitted fields remain unchanged in App Store Connect.
Edit the YAML or long-form text files, validate them, and preview the remote changes:
ascdir check
ascdir push --dry-runApply the changes:
ascdir pushOnce Xcode or your build pipeline has uploaded a processed build, preview and run the recurring release workflow from the same project:
# TestFlight
ascdir testflight distribute --group "Internal Team" --dry-run
ascdir testflight distribute --group "Internal Team" --confirm 1.2.0
# App Store review, then manual publication after approval
ascdir app-store submit --dry-run
ascdir app-store submit --confirm 1.2.0
ascdir app-store release --dry-run
ascdir app-store release --confirm 1.2.0The version passed to --confirm must match app.version. Dry runs and status
commands are read-only. See App Store submission and release
and TestFlight distribution before the first
production run.
To replace managed local values with the current App Store Connect values:
ascdir pull --dry-run
ascdir pullIf the remote state would remove local screenshots or App Previews, review the
dry run and confirm with ascdir pull --allow-local-asset-deletions.
Most projects only need the app identity and the localization fields generated
by ascdir init. The remaining sections below are opt-in:
remove any key you do not want ascdir to manage. Age ratings, accessibility
declarations, a custom EULA, media assets, availability, and pricing are
advanced workflows with dedicated guides.
version: "2"
app:
id: "123456789"
bundle_id: com.example.myapp
platform: IOS
version: 1.2.0
metadata:
copyright: "2026 Example, Inc." # Year and rights holder, for example: 2026 Example, Inc.
categories:
primary_category: PRODUCTIVITY # Required top-level App Store category ID
localizations:
en-US:
values:
name: Example App # App Store display name, up to 30 characters
subtitle: A concise summary # Short summary displayed below the name, up to 30 characters
keywords: example,productivity # Comma-separated search keywords, up to 100 bytes
support_url: https://example.com/support # Public HTTP(S) support page
marketing_url: https://example.com # Optional public HTTP(S) marketing page
privacy_policy_url: https://example.com/privacy # Public HTTP(S) privacy policy
privacy_choices_url: "" # Optional public HTTP(S) privacy choices page
files:
description: metadata/en-US/description.md # Required plain-text product description; Markdown is not rendered
promotional_text: metadata/en-US/promotional_text.md # Optional plain-text promotion, up to 170 characters
whats_new: metadata/en-US/whats_new.md # Plain-text release notes; required for app updates
privacy_policy_text: metadata/en-US/privacy_policy.md # Required plain-text tvOS privacy policy; TV_OS onlyStart from the minimal example and add only the advanced sections documented under Managed metadata when opting into assets, age ratings, accessibility declarations, licensing, availability, or pricing.
Find valid price-point IDs without changing App Store state:
ascdir price-points --territory USAShort, single-line values live under values, where the key and generated inline comment describe the expected input. Long-form values live in files referenced under files. The .md extension makes them convenient to review on GitHub; ascdir sends their contents as plain text, so Markdown and HTML formatting are not rendered by the App Store. Paths are relative to the directory containing ascdir.yaml.
Remove a key to leave that field unmanaged. An explicitly empty value remains managed and represents a request to clear the remote field; push still requires --allow-empty when the remote value is non-empty.
Version 1 configurations remain fully supported. Existing projects can continue using one file per field without modification; newly initialized projects use version 2. To migrate manually, move short values into values, keep long-form paths under files, and change version to "2".
Unknown configuration keys are rejected so misspelled fields cannot be silently ignored. During pull, ascdir updates only managed values and preserves YAML comments and key order. Configuration, metadata, and asset replacements are staged and committed as one recoverable local transaction.
Supported platforms are IOS, MAC_OS, TV_OS, and VISION_OS.
Non-localized fields:
- Version copyright
- App accessibility URL
- Content rights declaration
- Primary and secondary categories
- Games and Stickers subcategories
- Age rating declaration, including Made for Kids
- Accessibility Nutrition Labels for each device family
- Custom end-user license agreement text and territories
- App screenshots, including locale, display type, and order
- App Preview videos, display order, and optional poster-frame timecodes
- Territory availability, release dates, and preorder settings
- Base-territory pricing and scheduled price changes
App-level localization fields:
- Name and subtitle
- Privacy policy URL and text
- Privacy choices URL
Version-level localization fields:
- Description and keywords
- Promotional text and what's new text
- Support URL and marketing URL
See Text metadata for the purpose, requirement, and editing lifecycle of every generated long-form file. Complex managed resources have dedicated guides for screenshots, App Previews, age ratings, accessibility, custom license agreements, availability, and pricing.
When a configured locale does not exist remotely, push creates both the app-level and version-level localization resources in the order required by App Store Connect.
Validates the configured private key, creates a short-lived JWT locally, and performs a read-only API request.
Prompts for the issuer ID, key ID, and .p8 path, validates the private key, and saves the configuration with user-only file permissions. On Unix systems, ascdir warns when the private key is readable by other users.
Removes the credentials saved by auth login. It never deletes the .p8 private key. Environment variables remain untouched.
Finds an existing app and version, generates the configuration, and pulls its localizations. It refuses to overwrite an existing configuration unless --force is supplied.
Downloads configured fields into ascdir.yaml, referenced Markdown files, and managed asset directories. Local edits are overwritten, so commit or review them first. Use pull --dry-run to preview the local differences without writing files. If the plan removes local assets, rerun with --allow-local-asset-deletions after reviewing the deletion.
Validates and compares local metadata and assets with App Store Connect, then updates only changed resources. Use --dry-run to inspect changes without writing.
Clearing a non-empty remote field requires the explicit --allow-empty flag:
ascdir push --dry-run
ascdir push --allow-emptyChanging age_rating.kids_age_band may become irreversible after App Review, so it also requires explicit confirmation:
ascdir push --dry-run
ascdir push --allow-irreversibleSee Age rating configuration for every supported declaration and enum value.
Publishing an Accessibility Nutrition Label also requires --allow-irreversible. See Accessibility declaration configuration.
Apple only permits most version metadata to change while the version is in an editable state. API errors are returned without hiding Apple's error code or detail.
App Store Connect does not provide transactions across resource types. ascdir validates and stages the complete plan before writing, creates required localization resources before their assets, and applies availability and pricing changes last. If Apple rejects a later request, earlier successful requests remain applied. Review the error, rerun ascdir push --dry-run to inspect the remaining difference, and then retry.
Checks the configuration, required files, common character limits, and HTTP(S) URLs without contacting App Store Connect.
Shows the configured App Store version, selected build, review submission state, and release type without changing App Store Connect. Use --json for machine-readable output.
ascdir app-store status
ascdir app-store status --jsonBuilds a read-only plan, then converges the configured version toward submission. If the App Store version does not exist, the first confirmed run creates only that version and stops so metadata can be synchronized safely. A later run selects a valid build, creates or resumes a compatible draft Review Submission, adds the version, and submits it for App Review.
Preview the exact operations first. If --build is omitted, ascdir selects the newest valid, unexpired build for the configured platform and version.
ascdir app-store submit --dry-run
ascdir app-store submit --build 42 --dry-runExecution requires the configured version as an explicit confirmation token:
ascdir app-store submit --build 42 --confirm 1.2.0New versions default to MANUAL; existing versions preserve their current release setting unless --release-type is supplied. AFTER_APPROVAL releases automatically after approval. SCHEDULED also requires --earliest-release-date in RFC3339 format. Rerunning the command after submission is a no-op rather than creating a duplicate submission.
Requests publication of a version in PENDING_DEVELOPER_RELEASE. It is only valid for a manual-release version and is a no-op once release processing has started.
ascdir app-store release --dry-run
ascdir app-store release --confirm 1.2.0Review App Store submission and release before the first production run. Build upload, signing, and archive creation remain the responsibility of Xcode, Transporter, or another build pipeline.
Lists builds for the configured platform and prerelease version, newest first, without changing TestFlight. Use --json for machine-readable output.
ascdir testflight status
ascdir testflight status --jsonDistributes an uploaded, valid build to existing TestFlight groups. Group names are exact and --group is repeatable; ascdir never creates groups or testers.
ascdir testflight distribute --group "Internal Team" --dry-run
ascdir testflight distribute --group "Internal Team" --confirm 1.2.0Use --build 42 to pin a build; otherwise the newest valid, unexpired build for the configured platform and version is selected. Internal groups require only build attachment. If any requested group is external, ascdir inspects the build's Beta App Review submission and creates one only when absent or rejected. Existing WAITING_FOR_REVIEW, IN_REVIEW, or APPROVED submissions are reused.
Execution revalidates the complete plan immediately before mutation and requires the configured version through --confirm. See TestFlight distribution for prerequisites and recovery behavior.
Prints shell completion for Bash, Zsh, Fish, or PowerShell. For example, enable Zsh completion for the current session with:
source <(ascdir completion zsh)See Troubleshooting for configuration discovery, authentication, editable-version restrictions, confirmation flags, and recovery after a partially applied push.
ascdir manages App Store Connect metadata and product-page assets, can distribute existing builds through TestFlight groups, and can submit and manually release an App Store version. It does not upload or sign app builds, create TestFlight groups or testers, or manage certificates, subscriptions, analytics, or customer reviews.
The intended boundary is the recurring release work of an individual developer or small team. Initial Apple agreements, tax and banking setup, certificates, profiles, archive creation, signing, and build upload stay in Apple tooling or the existing build pipeline. See Product scope for the detailed supported and deferred areas.
- Authentication uses short-lived ES256 JSON Web Tokens generated locally.
- The
.p8private key is read from the configured local path and never leaves the machine. Apple receives only the signed JWT. - API errors are reported without printing credentials or JWTs.
push --dry-runnever sends mutation requests.app-store submit --dry-runandapp-store release --dry-runnever send mutation requests; execution is bound to the configured version with--confirm.testflight distribute --dry-runnever sends mutation requests; confirmed execution only attaches the selected build to resolved existing groups and, for external testing, submits that build for Beta App Review when needed.- Pagination links are restricted to the configured App Store Connect API origin, preventing bearer tokens from being forwarded to another host.
- Metadata paths are confined to the configuration directory after resolving symbolic links.
- Retried mutations are limited to idempotent updates and requests rejected by rate limiting.
- ascdir collects no telemetry.
make fmt
make check
make buildTests use generated keys and local HTTP servers. They never require real App Store Connect credentials or contact the production API.
See CONTRIBUTING.md for contribution guidelines, CODE_OF_CONDUCT.md for community expectations, and SECURITY.md for private vulnerability reporting.
MIT
This is an independent, unofficial project and is not affiliated with, endorsed by, or sponsored by Apple Inc. App Store Connect is a trademark of Apple Inc.
