apple build stops before the archive in an SSH / non-GUI session (errSecInternalComponent) with how to run it in the logged-in session, --allow-ssh to skip; relative credential paths resolve from the project and xcodebuild gets an absolute ASC key path; tests, pitfall 101
This commit is contained in:
parent
467bacc00a
commit
b97f23701f
7 changed files with 72 additions and 6 deletions
|
|
@ -23,7 +23,7 @@ import { parseMobileprovision, toXmlPlist } from '../lib/plist';
|
|||
import { nextBuildNumber } from '../lib/release';
|
||||
import { exportOptions, profileDirs } from '../lib/signing';
|
||||
import { capacitorProject, webStep } from '../lib/web-assets';
|
||||
import { archiveArgs, type AscAuth, distributionErrors, exportArgs, foreignRsync, xcodePath, flutterConfigArgs, ipaInfo, pickXcode, scanXcode, showCmd } from '../lib/xcode';
|
||||
import { archiveArgs, type AscAuth, distributionErrors, exportArgs, foreignRsync, remoteBuildSession, xcodePath, flutterConfigArgs, ipaInfo, pickXcode, scanXcode, showCmd } from '../lib/xcode';
|
||||
import { bundleTrackingIssues, ipaManifests } from '../lib/privacy-files';
|
||||
import { ascClient } from './asc';
|
||||
import { findBundleId } from './signing';
|
||||
|
|
@ -168,6 +168,10 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
return;
|
||||
}
|
||||
if (process.platform !== 'darwin' || !rt.which('xcodebuild')) throw new Error('apple build needs macOS with Xcode (xcode-select --install is not enough: the full Xcode app)');
|
||||
// codesign in an SSH session cannot use the login keychain: the archive fails late with errSecInternalComponent
|
||||
// (Embed Pods Frameworks). Stop before the web build and the archive (pitfall 101).
|
||||
const remote = remoteBuildSession(process.env, rt.spawnSync(['launchctl', 'managername'], { timeout: 5000 }).stdout.toString().trim());
|
||||
if (remote && !a.has('allow-ssh')) throw new Error(`apple build: ${remote}; codesign there fails with errSecInternalComponent (locked keychain). Run it in the Mac's logged-in session: a Jenkins / Forgejo agent started in that session, or from SSH: write the command into a .command file and \`open -a Terminal <file>\` (it runs in the GUI session; unset SSH_CONNECTION SSH_CLIENT SSH_TTY inside). --allow-ssh skips this check after \`security unlock-keychain\`.`);
|
||||
const web = await webStep({ ...webOpts, dry: false });
|
||||
for (const n of target.notes) log(TAG, n);
|
||||
const rsync = foreignRsync(process.env.PATH);
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
],
|
||||
"apple appinfo": [],
|
||||
"apple build": [
|
||||
"allow-ssh",
|
||||
"allow-stale-web",
|
||||
"build-number",
|
||||
"configuration",
|
||||
|
|
@ -941,6 +942,7 @@
|
|||
"all-pages",
|
||||
"allow-existing",
|
||||
"allow-price-changes",
|
||||
"allow-ssh",
|
||||
"allow-stale-web",
|
||||
"alpha",
|
||||
"apk",
|
||||
|
|
|
|||
|
|
@ -189,7 +189,13 @@ function value(root: string, name: string): string | undefined {
|
|||
}
|
||||
|
||||
/** A path or the inline content itself. */
|
||||
export const inline = (v: string, marker: RegExp): string => (marker.test(v) ? v : readFileSync(v.replace(/^~(?=\/)/, homedir()), 'utf8'));
|
||||
export const inline = (v: string, marker: RegExp, root?: string): string => (marker.test(v) ? v : readFileSync(credPath(v, root), 'utf8'));
|
||||
|
||||
/** A credential path: ~ expanded; a relative one is taken from the project folder, not the working directory. */
|
||||
export function credPath(v: string, root?: string): string {
|
||||
const h = v.replace(/^~(?=[/\\])/, homedir());
|
||||
return isAbsolute(h) || !root ? h : resolve(root, h);
|
||||
}
|
||||
|
||||
export function parseServiceAccount(text: string): ServiceAccount {
|
||||
const j = JSON.parse(text) as Partial<ServiceAccount>;
|
||||
|
|
@ -201,7 +207,7 @@ export function parseServiceAccount(text: string): ServiceAccount {
|
|||
export function playServiceAccount(root: string): ServiceAccount {
|
||||
const v = value(root, 'PLAY_SERVICE_ACCOUNT');
|
||||
if (!v) throw new Error('PLAY_SERVICE_ACCOUNT is not set (env, the credential profile in ~/.storebound/credentials, or .storebound/credentials.json): path to the service account JSON key; `storebound accounts` lists the profiles');
|
||||
return parseServiceAccount(inline(v, /^\s*\{/));
|
||||
return parseServiceAccount(inline(v, /^\s*\{/, root));
|
||||
}
|
||||
|
||||
export function ascKey(root: string): AscKey {
|
||||
|
|
@ -209,8 +215,8 @@ export function ascKey(root: string): AscKey {
|
|||
const issuerId = value(root, 'ASC_ISSUER_ID');
|
||||
if (!keyId || !issuerId) throw new Error('ASC_KEY_ID and ASC_ISSUER_ID are not set (env, the credential profile in ~/.storebound/credentials, or .storebound/credentials.json); `storebound accounts` lists the profiles');
|
||||
const p8 = value(root, 'ASC_P8') ?? join(homedir(), '.appstoreconnect/private_keys', `AuthKey_${keyId}.p8`);
|
||||
if (!/BEGIN PRIVATE KEY/.test(p8) && !existsSync(p8.replace(/^~(?=\/)/, homedir()))) throw new Error(`App Store Connect key not found: ${p8} (set ASC_P8)`);
|
||||
return { keyId, issuerId, pem: inline(p8, /BEGIN PRIVATE KEY/) };
|
||||
if (!/BEGIN PRIVATE KEY/.test(p8) && !existsSync(credPath(p8, root))) throw new Error(`App Store Connect key not found: ${p8} (set ASC_P8)`);
|
||||
return { keyId, issuerId, pem: inline(p8, /BEGIN PRIVATE KEY/, root) };
|
||||
}
|
||||
|
||||
/** The keys and nestings a review contact is read from (first hit): the documented one, then shapes seen in real files. */
|
||||
|
|
@ -306,7 +312,9 @@ export const maskSecret = (s: string): string => (s ? '********' : '(empty)');
|
|||
export function ascKeyFile(root: string): { keyPath: string; keyId: string; issuerId: string; cleanup: () => void } {
|
||||
const k = ascKey(root);
|
||||
const raw = value(root, 'ASC_P8') ?? join(homedir(), '.appstoreconnect/private_keys', `AuthKey_${k.keyId}.p8`);
|
||||
if (!/BEGIN PRIVATE KEY/.test(raw)) return { keyPath: raw.replace(/^~(?=\/)/, homedir()), keyId: k.keyId, issuerId: k.issuerId, cleanup: () => {} };
|
||||
// xcodebuild refuses a relative -authenticationKeyPath ("must be an absolute path"): resolve it from the project
|
||||
const asPath = (p: string) => { const h = p.replace(/^~(?=[/\\])/, homedir()); return isAbsolute(h) ? h : resolve(root, h); };
|
||||
if (!/BEGIN PRIVATE KEY/.test(raw)) return { keyPath: asPath(raw), keyId: k.keyId, issuerId: k.issuerId, cleanup: () => {} };
|
||||
const dir = mkdtempSync(join(tmpdir(), 'storebound-key-'));
|
||||
chmodSync(dir, 0o700);
|
||||
const keyPath = join(dir, `AuthKey_${k.keyId}.p8`);
|
||||
|
|
|
|||
|
|
@ -242,3 +242,11 @@ export function distributionErrors(output: string, tmp: string = process.env.TMP
|
|||
}
|
||||
return logs[0] ? { log: logs[0], lines: [] } : null;
|
||||
}
|
||||
|
||||
/** Why this is not the Mac's logged-in GUI session (SSH, or a launchd manager other than Aqua), else null: codesign
|
||||
* cannot reach the login keychain there (errSecInternalComponent). */
|
||||
export function remoteBuildSession(env: Record<string, string | undefined>, managerName: string): string | null {
|
||||
if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY) return 'this is an SSH session';
|
||||
if (managerName && managerName !== 'Aqua') return `this is not the logged-in GUI session (launchctl manager: ${managerName})`;
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue