apple upload: find the IPA of a build made with --out (build notes it in .storebound/last-apple-build.json; upload --out; the error names every folder searched and the last build's IPA)
This commit is contained in:
parent
ef80cd2760
commit
9a878d6562
6 changed files with 58 additions and 11 deletions
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- `apple upload` finds the IPA of a build made with `--out`: `apple build` notes its IPA and out folder in `.storebound/last-apple-build.json`, and upload searches `--out` (new flag), then that last build's out, then `apple.build.out` / `build/ios`. When nothing is found, the error names every folder it searched and the last build's IPA.
|
||||
|
||||
## 0.8.56 · 2026-10-27
|
||||
|
||||
- Release type. `apple.releaseType: afterApproval | manual | scheduled` (+ `apple.earliestReleaseDate`): `apple listing` creates versions with it (MANUAL when unset, existing versions untouched) and `apple submit` / `resubmit` set it before submitting when it differs, the dry run showing old -> new. New `apple release --after-approval | --manual | --scheduled <ISO date> [--platform] [--version] [--first-release] --yes` changes it on a version App Review has not approved yet (clear refusal otherwise, with `--manual-release` as the way out); exit 7 when it already has that type; an update is an agent step, after-approval on the first version of an app is exit 3 OWNER_STEP. Pitfall 125.
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ import { liveDeviceFamilies } from './device-families';
|
|||
import { keychainPassword, keychainSigningArgs, parseKeychainList, showSecurity, sshBuildRefusal, unlockSteps } from '../lib/keychain';
|
||||
import { appGroupSteps, appGroupVerdicts, type GroupEvidence } from '../lib/targets';
|
||||
import { capacitorProject, webStep } from '../lib/web-assets';
|
||||
import { archiveArgs, type AscAuth, distributionErrors, exportArgs, foreignRsync, remoteBuildSession, xcodePath, flutterConfigArgs, ipaInfo, ipaVersionProblem, pickXcode, scanXcode, showCmd } from '../lib/xcode';
|
||||
import { archiveArgs, type AscAuth, distributionErrors, exportArgs, foreignRsync, remoteBuildSession, xcodePath, flutterConfigArgs, ipaInfo, ipaVersionProblem, pickXcode, recordLastBuild, scanXcode, showCmd } from '../lib/xcode';
|
||||
import { bundleTrackingIssues, ipaManifests } from '../lib/privacy-files';
|
||||
import { ascClient } from './asc';
|
||||
import { parsePlatform } from './platform';
|
||||
|
|
@ -305,6 +305,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const result = { ipa: join(exportPath, ipa), bundleId: info.bundleId, version: info.version, build: info.build, web: web.bundle, derivedData: derivedData ?? null, pathFree, ...(scanPaths ? { leaks } : {}) };
|
||||
writeFileSync(join(out, 'latest.json'), `${JSON.stringify(result, null, 2)}
|
||||
`);
|
||||
recordLastBuild(root, { ipa: result.ipa, out, version: info.version, build: info.build });
|
||||
if (json) printJson(result);
|
||||
done(TAG, `${relative(root, result.ipa)}: ${info.bundleId} ${info.version} (${info.build}). Next: storebound apple upload ${relative(root, result.ipa)} --dry`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Upload an IPA to App Store Connect and wait until Apple has processed the build.
|
||||
// storebound apple upload [<app.ipa>] [--build N] [--allow-existing] [--via api|altool] [--no-wait] [--timeout 60m] [--pre-check "<cmd with {file}>"] [--evidence <path.json>] [--dry] [--yes] [--json]
|
||||
// storebound apple upload [<app.ipa>] [--out D] [--build N] [--allow-existing] [--via api|altool] [--no-wait] [--timeout 60m] [--pre-check "<cmd with {file}>"] [--evidence <path.json>] [--dry] [--yes] [--json]
|
||||
// storebound apple upload <App.pkg> --platform macos [...] (Mac App Store .pkg from apple build --platform macos --tauri; platform MAC_OS, uti com.apple.pkg; altool gets --type macos; no SDK / privacy-manifest check)
|
||||
// Default IPA: the newest in <apple.build.out or build/ios>/export (and its <version>-<build>/ folders), or with
|
||||
// Default IPA: the newest in <--out>/export, else the last apple build's <out>/export (.storebound/last-apple-build.json,
|
||||
// so a build with --out is found), then <apple.build.out or build/ios>/export (and its <version>-<build>/ folders), or with
|
||||
// --build N the one with that build number. The bundle id, version and build number come from the IPA's Info.plist.
|
||||
// An IPA whose build number is already on App Store Connect fails with exit 1 (a stale IPA left by a failed
|
||||
// `apple build`, pitfall 53); only --allow-existing (flows that resume after a finished upload) re-checks it instead. Before any network call every PrivacyInfo.xcprivacy in the IPA
|
||||
|
|
@ -27,7 +28,7 @@ import type { Ctx } from '../lib/ctx';
|
|||
import { confirm, need } from '../lib/ctx';
|
||||
import { done, log, printJson, warn } from '../lib/log';
|
||||
import { compareBuild, nextBuildNumber, nextPollDelay, parseDuration, WAIT_EXIT } from '../lib/release';
|
||||
import { altoolArgs, buildUploadReqs, exportedIpas, ipaInfo, ipaVersionProblem, showCmd, UPLOAD_DONE, uploadProblems } from '../lib/xcode';
|
||||
import { altoolArgs, buildUploadReqs, exportedIpas, ipaInfo, ipaVersionProblem, readLastBuild, showCmd, UPLOAD_DONE, uploadProblems, uploadSearchDirs } from '../lib/xcode';
|
||||
import { bundleTrackingIssues, ipaManifests } from '../lib/privacy-files';
|
||||
import { BUILD_WAIT_EXIT } from '../lib/build-ops';
|
||||
import { type Asc, ascClient, type J } from './asc';
|
||||
|
|
@ -238,11 +239,20 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const macos = parsePlatform(a.get('platform')) === 'MAC_OS';
|
||||
let ipa = a._.find(x => x.endsWith('.ipa') || x.endsWith('.pkg'));
|
||||
if (!ipa) {
|
||||
const dir = join(resolve(config.apple?.build?.out ?? 'build/ios'), 'export');
|
||||
let found = exportedIpas(dir);
|
||||
if (want) found = found.filter(f => { try { return ipaInfo(f).build === want; } catch { return false; } });
|
||||
const last = readLastBuild(root);
|
||||
const flagOut = a.get('out');
|
||||
const dirs = uploadSearchDirs({ flagOut: flagOut ? resolve(flagOut) : undefined, lastOut: last?.out, configOut: resolve(config.apple?.build?.out ?? 'build/ios') });
|
||||
let found: string[] = [];
|
||||
for (const dir of dirs) {
|
||||
found = exportedIpas(dir);
|
||||
if (want) found = found.filter(f => { try { return ipaInfo(f).build === want; } catch { return false; } });
|
||||
if (found[0]) break;
|
||||
}
|
||||
if (!found[0] && macos) throw new Error('usage: storebound apple upload <app.pkg> --platform macos (storebound apple build --platform macos --tauri <src-tauri> makes one)');
|
||||
if (!found[0]) throw new Error(`usage: storebound apple upload <app.ipa> (no ${want ? `build ${want} ` : ''}IPA in ${relative(root, dir)}; storebound apple build makes one)`);
|
||||
if (!found[0]) {
|
||||
const lastHint = last && !existsSync(last.ipa) ? `; the last apple build (${last.build}) wrote ${last.ipa}, which is gone` : last ? `; the last apple build (${last.build}) is ${last.ipa}` : '';
|
||||
throw new Error(`usage: storebound apple upload <app.ipa> (no ${want ? `build ${want} ` : ''}IPA in ${dirs.map(d => relative(root, d) || '.').join(', ')}${lastHint}; pass the IPA, --out <the build's --out>, or run storebound apple build)`);
|
||||
}
|
||||
ipa = found[0];
|
||||
} else ipa = resolve(ipa);
|
||||
shipPhase('verify', `checking ${relative(root, ipa)}`);
|
||||
|
|
|
|||
|
|
@ -366,6 +366,7 @@
|
|||
"locale",
|
||||
"no-wait",
|
||||
"older-than",
|
||||
"out",
|
||||
"platform",
|
||||
"pre-check",
|
||||
"progress",
|
||||
|
|
@ -1574,7 +1575,7 @@
|
|||
"storebound apple testflight expire (--keep N | --older-than D) [--version x.y.z] [--dry] [--yes] [--json]"
|
||||
],
|
||||
"apple upload": [
|
||||
"storebound apple upload [<app.ipa>] [--build N] [--allow-existing] [--via api|altool] [--no-wait] [--timeout 60m] [--pre-check \"<cmd with {file}>\"] [--evidence <path.json>] [--dry] [--yes] [--json]",
|
||||
"storebound apple upload [<app.ipa>] [--out D] [--build N] [--allow-existing] [--via api|altool] [--no-wait] [--timeout 60m] [--pre-check \"<cmd with {file}>\"] [--evidence <path.json>] [--dry] [--yes] [--json]",
|
||||
"storebound apple upload <App.pkg> --platform macos [...] (Mac App Store .pkg from apple build --platform macos --tauri; platform MAC_OS, uti com.apple.pkg; altool gets --type macos; no SDK / privacy-manifest check)"
|
||||
],
|
||||
"apple win-back": [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Xcode builds without Xcode's UI: find the workspace / project and scheme (native, Capacitor, React Native, Flutter,
|
||||
// Unity-exported projects), build the xcodebuild archive and export command lines, read an IPA's Info.plist.
|
||||
// Pure except scanXcode (a bounded directory walk) and ipaInfo (reads the IPA).
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, join, relative } from 'node:path';
|
||||
import { parsePlist, type PlistValue } from './plist';
|
||||
import { zipEntries, zipRead } from './zip';
|
||||
|
|
@ -266,3 +266,21 @@ export function remoteBuildSession(env: Record<string, string | undefined>, mana
|
|||
if (managerName && managerName !== 'Aqua') return `this is not the logged-in GUI session (launchctl manager: ${managerName})`;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Where `apple build` notes its last IPA for `apple upload`: .storebound/last-apple-build.json (gitignored). */
|
||||
export const lastBuildFile = (root: string): string => join(root, '.storebound', 'last-apple-build.json');
|
||||
|
||||
export function recordLastBuild(root: string, r: { ipa: string; out: string; version: string; build: string }): void {
|
||||
try { mkdirSync(join(root, '.storebound'), { recursive: true }); writeFileSync(lastBuildFile(root), `${JSON.stringify(r, null, 2)}\n`); } catch { /* best effort */ }
|
||||
}
|
||||
|
||||
export function readLastBuild(root: string): { ipa: string; out: string; version?: string; build?: string } | null {
|
||||
try { const j = JSON.parse(readFileSync(lastBuildFile(root), 'utf8')); return typeof j?.ipa === 'string' && typeof j?.out === 'string' ? j : null; } catch { return null; }
|
||||
}
|
||||
|
||||
/** The export folders `apple upload` searches, in order: --out, else the last build's out (a --out that build used),
|
||||
* then apple.build.out / build/ios. Duplicates dropped. */
|
||||
export function uploadSearchDirs(o: { flagOut?: string; lastOut?: string; configOut: string }): string[] {
|
||||
const dirs = o.flagOut ? [o.flagOut] : [...(o.lastOut ? [o.lastOut] : []), o.configOut];
|
||||
return [...new Set(dirs.map(d => join(d, 'export')))];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { archiveScanDirs, derivedDataFor, leakNeedles, leakProblem, outsideHome, pathFreeSettings, prefixMaps, scanForPaths } from '../cli/lib/path-free';
|
||||
import { archiveArgs, validateAppleBuild } from '../cli/lib/xcode';
|
||||
import { archiveArgs, readLastBuild, recordLastBuild, uploadSearchDirs, validateAppleBuild } from '../cli/lib/xcode';
|
||||
|
||||
const resolve = (p: string): string => (p.startsWith('/') ? p : `/proj/${p}`);
|
||||
const t = { framework: 'native' as const, project: 'App.xcodeproj', scheme: 'App', notes: [] };
|
||||
|
|
@ -102,3 +102,18 @@ describe('leak scan', () => {
|
|||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('apple upload finds the last build', () => {
|
||||
test('--out wins; else the last build out, then the config out', () => {
|
||||
expect(uploadSearchDirs({ flagOut: '/o', lastOut: '/l', configOut: '/c' })).toEqual([join('/o', 'export')]);
|
||||
expect(uploadSearchDirs({ lastOut: '/l', configOut: '/c' })).toEqual([join('/l', 'export'), join('/c', 'export')]);
|
||||
expect(uploadSearchDirs({ lastOut: '/c', configOut: '/c' })).toEqual([join('/c', 'export')]);
|
||||
});
|
||||
test('build records the IPA and out in .storebound, upload reads it back', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'lb-'));
|
||||
expect(readLastBuild(root)).toBeNull();
|
||||
recordLastBuild(root, { ipa: '/shared/out/export/build-31/App.ipa', out: '/shared/out', version: '1.0', build: '31' });
|
||||
expect(readLastBuild(root)).toEqual({ ipa: '/shared/out/export/build-31/App.ipa', out: '/shared/out', version: '1.0', build: '31' });
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue