testflight: description keys by language id or App Store locale, unknown keys are a config error, primary-locale check before Beta App Review
This commit is contained in:
parent
e185b61c8e
commit
1abd0936c3
6 changed files with 35 additions and 3 deletions
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- TestFlight: `apple.testflight.description` takes a language id or its App Store locale (`en` or `en-US`; an `en-US` key was skipped silently), `check` names an unknown key, and `distribute --submit-review` stops (dry run: warns) when the app's primary locale has no beta localization (Beta App Review answers 422 MISSING_BETA_APP_LOCALIZATION).
|
||||
## 0.8.60 · 2026-10-29
|
||||
|
||||
- `apple listing` creates a new version with the release type of the newest version before it (MANUAL or AFTER_APPROVAL) when `apple.releaseType` is not set; a first version stays MANUAL. The plan names where the type came from, and warns when `apple.releaseType` differs from the previous version's.
|
||||
|
|
|
|||
|
|
@ -319,6 +319,16 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (a.has('submit-review') && review) log(TAG, `beta review: already submitted (${review.attributes?.betaReviewState ?? '?'})`);
|
||||
if (external.length && !a.has('submit-review') && b.detail?.externalBuildState === 'READY_FOR_BETA_SUBMISSION')
|
||||
warn(TAG, 'external testers only get the build after Beta App Review: add --submit-review (asks first) or submit it in App Store Connect');
|
||||
// Beta App Review refuses a build whose app has no beta localization in its primary locale (422
|
||||
// MISSING_BETA_APP_LOCALIZATION): said before the submit, in the dry run too
|
||||
if (wantsReview) {
|
||||
const primary = String((await c.maybe(`/v1/apps/${appId}?fields[apps]=primaryLocale`))?.data?.attributes?.primaryLocale ?? '');
|
||||
const have = (await c.all(`/v1/apps/${appId}/betaAppLocalizations?limit=200`)).map((l: J) => String(l.attributes.locale));
|
||||
if (primary && !have.includes(primary)) {
|
||||
const msg = `Beta App Review needs test information in the app's primary locale ${primary} (has: ${have.join(', ') || 'none'}): add it to apple.testflight.description (key ${primary} or its language id), then storebound apple testflight info`;
|
||||
if (dry) warn(TAG, msg); else throw new Error(msg);
|
||||
}
|
||||
}
|
||||
if (dry) {
|
||||
await execute(c.asc, [...reqs, ...(wantsReview ? [betaReviewReq(b.id)] : [])], { dry, tag: TAG });
|
||||
done(TAG, 'distribute checked (dry run)');
|
||||
|
|
|
|||
|
|
@ -1210,7 +1210,7 @@ export function validateConfig(c: unknown): string[] {
|
|||
const enc = cfg.apple.usesNonExemptEncryption;
|
||||
if (enc !== undefined && typeof enc !== 'boolean') e.push('apple.usesNonExemptEncryption: true | false');
|
||||
if (cfg.apple.screenshotTypes && !cfg.apple.screenshotTypes.every(t => /^APP_[A-Z0-9_]+$/.test(t))) e.push('apple.screenshotTypes: display types like APP_IPHONE_67');
|
||||
e.push(...validateTestflight(cfg.apple.testflight));
|
||||
e.push(...validateTestflight(cfg.apple.testflight, cfg.languages ?? []));
|
||||
e.push(...validateSigning(cfg.apple.signing), ...validateAppleBuild(cfg.apple.build));
|
||||
const catalogIds = new Set([...(cfg.products ?? []).map(p => p.id), ...(cfg.subscriptions ?? []).flatMap(g => (g.subscriptions ?? []).map(x => x.id))]);
|
||||
e.push(...validatePromoted(cfg.apple.promoted, catalogIds));
|
||||
|
|
|
|||
|
|
@ -54,9 +54,14 @@ export const groupFromApi = (g: { id: string; attributes: Record<string, unknown
|
|||
});
|
||||
|
||||
/** Problems with the testflight config. Pure. */
|
||||
export function validateTestflight(t: TfConfig | undefined): string[] {
|
||||
export function validateTestflight(t: TfConfig | undefined, langs: { id: string; apple?: string }[] = []): string[] {
|
||||
if (!t) return [];
|
||||
const e: string[] = [];
|
||||
if (t.description && typeof t.description === 'object' && langs.length) {
|
||||
const known = new Set(langs.flatMap(l => [l.id, ...(l.apple ? [l.apple] : [])]));
|
||||
const bad = Object.keys(t.description).filter(k => !known.has(k));
|
||||
if (bad.length) e.push(`apple.testflight.description: ${bad.map(k => `'${k}'`).join(', ')} is not a language of the config (use a language id or its App Store locale: ${langs.filter(l => l.apple).map(l => `${l.id} / ${l.apple}`).join(', ')})`);
|
||||
}
|
||||
const names = new Set<string>();
|
||||
for (const [i, g] of (t.groups ?? []).entries()) {
|
||||
const at = `apple.testflight.groups[${i}]`;
|
||||
|
|
@ -315,7 +320,8 @@ export function betaDescriptions(t: TfConfig | undefined, langs: { id: string; a
|
|||
const out: Record<string, string> = {};
|
||||
for (const l of langs) {
|
||||
if (!l.apple) continue;
|
||||
const d = typeof t.description === 'string' ? t.description : t.description[l.id];
|
||||
// a language id ('en') or its App Store locale ('en-US'): an 'en-US' key was silently skipped (Acme 2026-10-29)
|
||||
const d = typeof t.description === 'string' ? t.description : (t.description[l.id] ?? t.description[l.apple]);
|
||||
if (d) out[l.apple] = d;
|
||||
}
|
||||
return out;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const UPLOAD_HOST = 'https://upload.example.com/';
|
|||
|
||||
/** list relationship -> resource type of its items (parent type / relationship name); `one` for to-one relationships. */
|
||||
const LISTS: Record<string, { type: string; one?: boolean }> = {
|
||||
'apps/betaAppLocalizations': { type: 'betaAppLocalizations' },
|
||||
'apps/inAppPurchasesV2': { type: 'inAppPurchases' },
|
||||
'inAppPurchases/inAppPurchaseLocalizations': { type: 'inAppPurchaseLocalizations' },
|
||||
'inAppPurchases/iapPriceSchedule': { type: 'inAppPurchasePriceSchedules', one: true },
|
||||
|
|
|
|||
14
tests/tf-description-locale.test.ts
Normal file
14
tests/tf-description-locale.test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { expect, test } from 'bun:test';
|
||||
import { betaDescriptions, validateTestflight } from '../cli/lib/testflight';
|
||||
|
||||
const langs = [{ id: 'en', apple: 'en-US' }, { id: 'tr', apple: 'tr' }];
|
||||
|
||||
test('beta descriptions take a language id or its App Store locale ("en-US" was skipped silently)', () => {
|
||||
expect(betaDescriptions({ description: { 'en-US': 'Hello', tr: 'Merhaba' } }, langs)).toEqual({ 'en-US': 'Hello', tr: 'Merhaba' });
|
||||
expect(betaDescriptions({ description: { en: 'Hello' } }, langs)).toEqual({ 'en-US': 'Hello' });
|
||||
});
|
||||
|
||||
test('an unknown description key is a config error naming the accepted keys', () => {
|
||||
expect(validateTestflight({ description: { 'en-US': 'x', de: 'y' } }, langs).join()).toContain("'de' is not a language of the config");
|
||||
expect(validateTestflight({ description: { 'en-US': 'x', tr: 'y' } }, langs)).toEqual([]);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue