@@ -372,73 +372,13 @@ jobs:
372372
373373 - name : Record latest build before upload
374374 id : pre_upload
375+ working-directory : .
375376 env :
376377 API_KEY_ID : ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
377378 ISSUER_ID : ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
378379 BUNDLE_ID : ${{ vars.APP_BUNDLE_ID }}
379380 run : |
380- LATEST_BUILD_ID=$(node << 'SCRIPT'
381- const crypto = require('crypto');
382- const https = require('https');
383- const fs = require('fs');
384-
385- const keyId = process.env.API_KEY_ID;
386- const issuerId = process.env.ISSUER_ID;
387- const keyPath = `${process.env.HOME}/private_keys/AuthKey_${keyId}.p8`;
388- const bundleId = process.env.BUNDLE_ID;
389-
390- function base64url(buf) {
391- return buf.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
392- }
393-
394- function generateJWT() {
395- const key = fs.readFileSync(keyPath, 'utf8');
396- const header = { alg: 'ES256', kid: keyId, typ: 'JWT' };
397- const now = Math.floor(Date.now() / 1000);
398- const payload = { iss: issuerId, iat: now, exp: now + 1200, aud: 'appstoreconnect-v1' };
399- const h = base64url(Buffer.from(JSON.stringify(header)));
400- const p = base64url(Buffer.from(JSON.stringify(payload)));
401- const input = `${h}.${p}`;
402- const sig = crypto.sign('SHA256', Buffer.from(input), { key, dsaEncoding: 'ieee-p1363' });
403- return `${input}.${base64url(sig)}`;
404- }
405-
406- function apiGet(token, path) {
407- return new Promise((resolve, reject) => {
408- https.get({
409- hostname: 'api.appstoreconnect.apple.com',
410- path,
411- headers: { Authorization: `Bearer ${token}` }
412- }, res => {
413- let data = '';
414- res.on('data', c => data += c);
415- res.on('end', () => {
416- if (res.statusCode !== 200) reject(new Error(`HTTP ${res.statusCode}`));
417- else resolve(JSON.parse(data));
418- });
419- }).on('error', reject);
420- });
421- }
422-
423- async function main() {
424- const token = generateJWT();
425- const apps = await apiGet(token, `/v1/apps?filter[bundleId]=${encodeURIComponent(bundleId)}`);
426- if (!apps.data?.length) throw new Error('App not found');
427-
428- const appId = apps.data[0].id;
429- const builds = await apiGet(token, `/v1/builds?filter[app]=${appId}&sort=-uploadedDate&limit=1`);
430- if (builds.data?.length) {
431- process.stdout.write(builds.data[0].id);
432- }
433- }
434-
435- main().catch(e => {
436- process.stderr.write(`Error: ${e.message}\n`);
437- process.exit(1);
438- });
439- SCRIPT
440- ) || LATEST_BUILD_ID=""
441-
381+ LATEST_BUILD_ID=$(node .github/scripts/appstore-connect-build.js latest) || LATEST_BUILD_ID=""
442382 echo "latest_build_id=${LATEST_BUILD_ID}" >> $GITHUB_OUTPUT
443383 echo "Latest build before upload: ${LATEST_BUILD_ID:-none}"
444384
@@ -472,6 +412,7 @@ jobs:
472412
473413 - name : Get TestFlight build URL
474414 id : build_url
415+ working-directory : .
475416 env :
476417 API_KEY_ID : ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
477418 ISSUER_ID : ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
@@ -481,96 +422,7 @@ jobs:
481422 run : |
482423 FALLBACK_URL="https://appstoreconnect.apple.com/apps/${APP_APPLE_ID}/testflight/ios"
483424
484- BUILD_ID=$(node << 'SCRIPT'
485- const crypto = require('crypto');
486- const https = require('https');
487- const fs = require('fs');
488-
489- const keyId = process.env.API_KEY_ID;
490- const issuerId = process.env.ISSUER_ID;
491- const keyPath = `${process.env.HOME}/private_keys/AuthKey_${keyId}.p8`;
492- const bundleId = process.env.BUNDLE_ID;
493- const preUploadBuildId = process.env.PRE_UPLOAD_BUILD_ID || '';
494-
495- function base64url(buf) {
496- return buf.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
497- }
498-
499- function generateJWT() {
500- const key = fs.readFileSync(keyPath, 'utf8');
501- const header = { alg: 'ES256', kid: keyId, typ: 'JWT' };
502- const now = Math.floor(Date.now() / 1000);
503- const payload = { iss: issuerId, iat: now, exp: now + 1200, aud: 'appstoreconnect-v1' };
504- const h = base64url(Buffer.from(JSON.stringify(header)));
505- const p = base64url(Buffer.from(JSON.stringify(payload)));
506- const input = `${h}.${p}`;
507- const sig = crypto.sign('SHA256', Buffer.from(input), { key, dsaEncoding: 'ieee-p1363' });
508- return `${input}.${base64url(sig)}`;
509- }
510-
511- function apiGet(token, path) {
512- return new Promise((resolve, reject) => {
513- https.get({
514- hostname: 'api.appstoreconnect.apple.com',
515- path,
516- headers: { Authorization: `Bearer ${token}` }
517- }, res => {
518- let data = '';
519- res.on('data', c => data += c);
520- res.on('end', () => {
521- if (res.statusCode !== 200) reject(new Error(`HTTP ${res.statusCode}: ${data.substring(0, 300)}`));
522- else resolve(JSON.parse(data));
523- });
524- }).on('error', reject);
525- });
526- }
527-
528- const sleep = ms => new Promise(r => setTimeout(r, ms));
529-
530- async function main() {
531- const token = generateJWT();
532-
533- process.stderr.write(`Looking up app: ${bundleId}\n`);
534- const apps = await apiGet(token, `/v1/apps?filter[bundleId]=${encodeURIComponent(bundleId)}`);
535- if (!apps.data?.length) throw new Error('App not found');
536-
537- const appId = apps.data[0].id;
538- process.stderr.write(`App API ID: ${appId}\n`);
539- process.stderr.write(`Pre-upload latest build: ${preUploadBuildId || 'none'}\n`);
540-
541- // Builds typically appear in the API ~2 min after altool upload.
542- // Wait 90s before first check, then poll every 15s for up to 8 more attempts.
543- process.stderr.write('Waiting 90s for Apple to register the build...\n');
544- await sleep(90000);
545-
546- for (let i = 1; i <= 8; i++) {
547- if (i > 1) await sleep(15000);
548- process.stderr.write(`Attempt ${i}/8: checking for new build...\n`);
549-
550- const builds = await apiGet(token,
551- `/v1/builds?filter[app]=${appId}&sort=-uploadedDate&limit=1&fields[builds]=version,processingState`
552- );
553-
554- if (builds.data?.length) {
555- const latest = builds.data[0];
556- if (latest.id !== preUploadBuildId) {
557- process.stderr.write(`Found new build: ${latest.id} (version: ${latest.attributes?.version}, state: ${latest.attributes?.processingState})\n`);
558- process.stdout.write(latest.id);
559- return;
560- }
561- process.stderr.write(`Latest build ${latest.id} is still the pre-upload build, waiting...\n`);
562- }
563- }
564-
565- throw new Error('New build not found after polling');
566- }
567-
568- main().catch(e => {
569- process.stderr.write(`Error: ${e.message}\n`);
570- process.exit(1);
571- });
572- SCRIPT
573- ) || BUILD_ID=""
425+ BUILD_ID=$(node .github/scripts/appstore-connect-build.js poll "${PRE_UPLOAD_BUILD_ID}") || BUILD_ID=""
574426
575427 if [ -n "$BUILD_ID" ]; then
576428 BUILD_URL="https://appstoreconnect.apple.com/apps/${APP_APPLE_ID}/testflight/ios/${BUILD_ID}"
0 commit comments