apple: no app info on App Store Connect is a clear error (it crashed apple listing / appinfo with "undefined is not an object (evaluating 'info.id')"); asset uploads use basename (a Windows path sent the whole C:\... path as fileName), reviews send --only and previews names split both separators; tests/matrix-bugs.test.ts

This commit is contained in:
CRTkafa 2026-10-14 08:29:26 +08:00
parent f58e192001
commit 2c14e5318a
5 changed files with 60 additions and 11 deletions

View file

@ -2,6 +2,7 @@
// API; role Admin or App Manager), paging, retries, the asset upload protocol, and the app / version lookups.
import { createHash, createPrivateKey, sign } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { basename } from 'node:path';
import { ascKey } from '../lib/credentials';
import { errorBody } from '../lib/http';
import { noteAsc } from '../lib/quota';
@ -55,6 +56,10 @@ export function ascJwt(root: string): { jwt: () => string; keyId: string } {
return { jwt, keyId: k.keyId };
}
/** App Store Connect answered no appInfos for the app (an app record always has one): a clear error, not a TypeError. */
export const noAppInfo = (appId: string): string =>
`App Store Connect returned no app info for app ${appId}: the app record is not readable with this key (check apple.appId and the key's app access in Users and Access), or the record was just created and is not listed yet (run it again in a minute)`;
export async function ascClient(root: string, bundleId: string, appIdHint?: string) {
const k = ascJwt(root);
const jwt = k.jwt;
@ -141,15 +146,19 @@ export async function ascClient(root: string, bundleId: string, appIdHint?: stri
}
async function editableAppInfo(): Promise<J> {
const infos = await all(`/v1/apps/${await app()}/appInfos`);
return infos.find(i => !['READY_FOR_DISTRIBUTION', 'REPLACED_WITH_NEW_INFO'].includes(i.attributes.state ?? i.attributes.appStoreState)) ?? infos[0];
const id = await app();
const infos = await all(`/v1/apps/${id}/appInfos`);
const info = infos.find(i => !['READY_FOR_DISTRIBUTION', 'REPLACED_WITH_NEW_INFO'].includes(i.attributes.state ?? i.attributes.appStoreState)) ?? infos[0];
if (!info) throw new Error(noAppInfo(id));
return info;
}
/** Reserve -> PUT the parts -> commit with the MD5 (screenshots, IAP review screenshots, app previews). `attrs` go into
* the reservation (app previews: mimeType, previewFrameTimeCode). */
async function uploadAsset(type: string, rel: Record<string, unknown>, file: string, attrs: Record<string, unknown> = {}): Promise<string> {
const buf = readFileSync(file);
const name = file.split('/').pop()!;
// basename, not split('/'): a Windows path has backslashes, and the store would get the whole path as the file name
const name = basename(file);
const res = (await asc('POST', `/v1/${type}`, { data: { type, attributes: { fileName: name, fileSize: buf.length, ...attrs }, relationships: rel } })).data;
for (const op of res.attributes.uploadOperations as J[]) {
const headers: Record<string, string> = {};