diff --git a/packages/build-tools/src/builders/android.ts b/packages/build-tools/src/builders/android.ts index 15b77ecb8e..88b3a045a0 100644 --- a/packages/build-tools/src/builders/android.ts +++ b/packages/build-tools/src/builders/android.ts @@ -10,6 +10,10 @@ import { resolveGradleCommand, runGradleCommand, } from '../android/gradle'; +import { + extractAndroidVersionAsync, + reportResolvedVersionAsync, +} from '../steps/functions/reportResolvedVersion'; import { eagerBundleAsync, shouldUseEagerBundle } from '../common/eagerBundle'; import { prebuildAsync } from '../common/prebuild'; import { setupAsync } from '../common/setup'; @@ -174,14 +178,32 @@ async function buildAsync(ctx: BuildContext): Promise { await runHookIfPresent(ctx, Hook.PRE_UPLOAD_ARTIFACTS); }); - await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => { - await uploadApplicationArchive(ctx, { + const archivePath = await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => { + return await uploadApplicationArchive(ctx, { patternOrPath: ctx.job.applicationArchivePath ?? 'android/app/build/outputs/**/*.{apk,aab}', rootDir: ctx.getReactNativeProjectDirectory(), logger: ctx.logger, }); }); + if (archivePath) { + try { + const { appVersion, appBuildVersion } = await extractAndroidVersionAsync(archivePath); + const buildId = ctx.env.EAS_BUILD_ID; + if (buildId && (appVersion || appBuildVersion)) { + await reportResolvedVersionAsync(ctx.graphqlClient, buildId, { + appVersion, + appBuildVersion, + }); + ctx.logger.info( + `Reported resolved version: ${appVersion ?? 'N/A'} (${appBuildVersion ?? 'N/A'})` + ); + } + } catch (err) { + ctx.logger.warn({ err }, 'Failed to report resolved version (non-fatal)'); + } + } + await ctx.runBuildPhase(BuildPhase.SAVE_CACHE, async () => { if (ctx.isLocal) { ctx.logger.info('Local builds do not support saving cache.'); diff --git a/packages/build-tools/src/builders/ios.ts b/packages/build-tools/src/builders/ios.ts index 4ee9528627..32b85086dc 100644 --- a/packages/build-tools/src/builders/ios.ts +++ b/packages/build-tools/src/builders/ios.ts @@ -16,6 +16,10 @@ import { runFastlaneGym, runFastlaneResign } from '../ios/fastlane'; import { installPods } from '../ios/pod'; import { downloadApplicationArchiveAsync } from '../ios/resign'; import { resolveArtifactPath, resolveBuildConfiguration, resolveScheme } from '../ios/resolve'; +import { + extractIosVersionAsync, + reportResolvedVersionAsync, +} from '../steps/functions/reportResolvedVersion'; import { cacheStatsAsync, restoreCcacheAsync } from '../steps/functions/restoreBuildCache'; import { saveCcacheAsync } from '../steps/functions/saveBuildCache'; import { uploadApplicationArchive } from '../utils/artifacts'; @@ -175,14 +179,32 @@ async function buildAsync(ctx: BuildContext): Promise { await runHookIfPresent(ctx, Hook.PRE_UPLOAD_ARTIFACTS); }); - await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => { - await uploadApplicationArchive(ctx, { + const archivePath = await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => { + return await uploadApplicationArchive(ctx, { patternOrPath: resolveArtifactPath(ctx), rootDir: ctx.getReactNativeProjectDirectory(), logger: ctx.logger, }); }); + if (archivePath) { + try { + const { appVersion, appBuildVersion } = await extractIosVersionAsync(archivePath); + const buildId = ctx.env.EAS_BUILD_ID; + if (buildId && (appVersion || appBuildVersion)) { + await reportResolvedVersionAsync(ctx.graphqlClient, buildId, { + appVersion, + appBuildVersion, + }); + ctx.logger.info( + `Reported resolved version: ${appVersion ?? 'N/A'} (${appBuildVersion ?? 'N/A'})` + ); + } + } catch (err) { + ctx.logger.warn({ err }, 'Failed to report resolved version (non-fatal)'); + } + } + await ctx.runBuildPhase(BuildPhase.SAVE_CACHE, async () => { if (ctx.isLocal) { ctx.logger.info('Local builds do not support saving cache.'); diff --git a/packages/build-tools/src/steps/easFunctions.ts b/packages/build-tools/src/steps/easFunctions.ts index 1265340a5a..ab4df5593a 100644 --- a/packages/build-tools/src/steps/easFunctions.ts +++ b/packages/build-tools/src/steps/easFunctions.ts @@ -22,6 +22,7 @@ import { createPrebuildBuildFunction } from './functions/prebuild'; import { createReadIpaInfoBuildFunction } from './functions/readIpaInfo'; import { createRepackBuildFunction } from './functions/repack'; import { createReportMaestroTestResultsFunction } from './functions/reportMaestroTestResults'; +import { createReportResolvedVersionBuildFunction } from './functions/reportResolvedVersion'; import { resolveAppleTeamIdFromCredentialsFunction } from './functions/resolveAppleTeamIdFromCredentials'; import { createResolveBuildConfigBuildFunction } from './functions/resolveBuildConfig'; import { @@ -90,6 +91,7 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] { functions.push( ...[ createFindAndUploadBuildArtifactsBuildFunction(ctx), + createReportResolvedVersionBuildFunction(ctx), createResolveBuildConfigBuildFunction(ctx), createGetCredentialsForBuildTriggeredByGithubIntegration(ctx), ] diff --git a/packages/build-tools/src/steps/functionGroups/build.ts b/packages/build-tools/src/steps/functionGroups/build.ts index 01a18e62c3..a714cd5c30 100644 --- a/packages/build-tools/src/steps/functionGroups/build.ts +++ b/packages/build-tools/src/steps/functionGroups/build.ts @@ -11,6 +11,7 @@ import { configureIosCredentialsFunction } from '../functions/configureIosCreden import { configureIosVersionFunction } from '../functions/configureIosVersion'; import { eagerBundleBuildFunction } from '../functions/eagerBundle'; import { createFindAndUploadBuildArtifactsBuildFunction } from '../functions/findAndUploadBuildArtifacts'; +import { createReportResolvedVersionBuildFunction } from '../functions/reportResolvedVersion'; import { generateGymfileFromTemplateFunction } from '../functions/generateGymfileFromTemplate'; import { injectAndroidCredentialsFunction } from '../functions/injectAndroidCredentials'; import { createInstallNodeModulesBuildFunction } from '../functions/installNodeModules'; @@ -122,6 +123,15 @@ function createStepsForIosSimulatorBuild({ createFindAndUploadBuildArtifactsBuildFunction( buildToolsContext ).createBuildStepFromFunctionCall(globalCtx), + createReportResolvedVersionBuildFunction(buildToolsContext).createBuildStepFromFunctionCall( + globalCtx, + { + callInputs: { + application_archive_path: + '${ steps.find_and_upload_build_artifacts.application_archive_path }', + }, + } + ), ]; } @@ -216,6 +226,15 @@ function createStepsForIosBuildWithCredentials({ createFindAndUploadBuildArtifactsBuildFunction( buildToolsContext ).createBuildStepFromFunctionCall(globalCtx), + createReportResolvedVersionBuildFunction(buildToolsContext).createBuildStepFromFunctionCall( + globalCtx, + { + callInputs: { + application_archive_path: + '${ steps.find_and_upload_build_artifacts.application_archive_path }', + }, + } + ), saveCache, createCacheStatsBuildFunction().createBuildStepFromFunctionCall(globalCtx), ]; @@ -287,6 +306,15 @@ function createStepsForAndroidBuildWithoutCredentials({ createFindAndUploadBuildArtifactsBuildFunction( buildToolsContext ).createBuildStepFromFunctionCall(globalCtx), + createReportResolvedVersionBuildFunction(buildToolsContext).createBuildStepFromFunctionCall( + globalCtx, + { + callInputs: { + application_archive_path: + '${ steps.find_and_upload_build_artifacts.application_archive_path }', + }, + } + ), saveCache, createCacheStatsBuildFunction().createBuildStepFromFunctionCall(globalCtx), ]; @@ -360,6 +388,15 @@ function createStepsForAndroidBuildWithCredentials({ createFindAndUploadBuildArtifactsBuildFunction( buildToolsContext ).createBuildStepFromFunctionCall(globalCtx), + createReportResolvedVersionBuildFunction(buildToolsContext).createBuildStepFromFunctionCall( + globalCtx, + { + callInputs: { + application_archive_path: + '${ steps.find_and_upload_build_artifacts.application_archive_path }', + }, + } + ), saveCache, createCacheStatsBuildFunction().createBuildStepFromFunctionCall(globalCtx), ]; diff --git a/packages/build-tools/src/steps/functions/__tests__/findAndUploadBuildArtifacts.test.ts b/packages/build-tools/src/steps/functions/__tests__/findAndUploadBuildArtifacts.test.ts index feb241019f..95a2457dd8 100644 --- a/packages/build-tools/src/steps/functions/__tests__/findAndUploadBuildArtifacts.test.ts +++ b/packages/build-tools/src/steps/functions/__tests__/findAndUploadBuildArtifacts.test.ts @@ -51,6 +51,24 @@ describe(createFindAndUploadBuildArtifactsBuildFunction, () => { await expect(buildStep.executeAsync()).resolves.not.toThrow(); }); + it('sets application_archive_path output to first archive', async () => { + const globalContext = createGlobalContextMock({}); + vol.fromJSON( + { + 'ios/build/test.ipa': '', + }, + globalContext.defaultWorkingDirectory + ); + const buildStep = findAndUploadBuildArtifacts.createBuildStepFromFunctionCall( + globalContext, + {} + ); + + await buildStep.executeAsync(); + + expect(buildStep.outputById.application_archive_path.value).toMatch(/ios\/build\/test\.ipa$/); + }); + it('throws build artifacts error', async () => { const globalContext = createGlobalContextMock({}); ctx.job.buildArtifactPaths = ['worker.log']; diff --git a/packages/build-tools/src/steps/functions/__tests__/reportResolvedVersion.test.ts b/packages/build-tools/src/steps/functions/__tests__/reportResolvedVersion.test.ts new file mode 100644 index 0000000000..c0a2236066 --- /dev/null +++ b/packages/build-tools/src/steps/functions/__tests__/reportResolvedVersion.test.ts @@ -0,0 +1,224 @@ +import { spawnAsync } from '@expo/steps'; +import fs from 'node:fs'; +import path from 'node:path'; + +jest.unmock('fs'); +jest.unmock('node:fs'); +jest.unmock('fs/promises'); +jest.unmock('node:fs/promises'); + +jest.mock('@expo/steps', () => ({ + ...jest.requireActual('@expo/steps'), + spawnAsync: jest.fn(), +})); + +const mockedSpawnAsync = jest.mocked(spawnAsync); + +import { parseInfoPlistBuffer } from '../readIpaInfo'; +import { + extractAndroidVersionAsync, + extractIosVersionAsync, + parseAaptOutput, + parseManifestXml, +} from '../reportResolvedVersion'; + +describe(parseAaptOutput, () => { + it('extracts versionName and versionCode from aapt2 output', () => { + const output = `package: name='com.example.app' versionCode='42' versionName='2.5.0' platformBuildVersionName='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14' +sdkVersion:'21' +targetSdkVersion:'34' +uses-permission: name='android.permission.INTERNET'`; + + expect(parseAaptOutput(output)).toEqual({ + appVersion: '2.5.0', + appBuildVersion: '42', + }); + }); + + it('handles output with no version info', () => { + expect(parseAaptOutput('some unrelated output')).toEqual({ + appVersion: undefined, + appBuildVersion: undefined, + }); + }); + + it('handles versionName with prerelease suffix', () => { + const output = `package: name='com.example' versionCode='1' versionName='1.0.0-beta.1'`; + + expect(parseAaptOutput(output)).toEqual({ + appVersion: '1.0.0-beta.1', + appBuildVersion: '1', + }); + }); +}); + +describe(parseManifestXml, () => { + it('extracts versionName and versionCode from bundletool manifest XML', () => { + const xml = ` + + +`; + + expect(parseManifestXml(xml)).toEqual({ + appVersion: '3.1.0', + appBuildVersion: '10', + }); + }); + + it('handles XML with no version attributes', () => { + const xml = ` + +`; + + expect(parseManifestXml(xml)).toEqual({ + appVersion: undefined, + appBuildVersion: undefined, + }); + }); +}); + +describe('simulator .app Info.plist parsing', () => { + const FIXTURES_DIR = path.join(__dirname, 'fixtures'); + const SIMULATOR_APP_DIR = path.join(FIXTURES_DIR, 'TestSimulator.app'); + const INFO_PLIST_PATH = path.join(SIMULATOR_APP_DIR, 'Info.plist'); + + beforeAll(async () => { + // Create a minimal XML Info.plist fixture + await fs.promises.mkdir(SIMULATOR_APP_DIR, { recursive: true }); + await fs.promises.writeFile( + INFO_PLIST_PATH, + ` + + + + CFBundleIdentifier + com.example.test + CFBundleShortVersionString + 4.2.0 + CFBundleVersion + 99 + +` + ); + }); + + afterAll(async () => { + await fs.promises.rm(SIMULATOR_APP_DIR, { recursive: true, force: true }); + }); + + it('reads version from a .app Info.plist', async () => { + const buffer = await fs.promises.readFile(INFO_PLIST_PATH); + const infoPlist = parseInfoPlistBuffer(buffer); + + expect(infoPlist.CFBundleShortVersionString).toBe('4.2.0'); + expect(infoPlist.CFBundleVersion).toBe('99'); + }); +}); + +describe(extractIosVersionAsync, () => { + const FIXTURES_DIR = path.join(__dirname, 'fixtures'); + const SIMULATOR_APP_DIR = path.join(FIXTURES_DIR, 'ExtractVersion.app'); + const INFO_PLIST_PATH = path.join(SIMULATOR_APP_DIR, 'Info.plist'); + const IPA_FIXTURE_PATH = path.join(FIXTURES_DIR, 'SmallestAppExample.ipa'); + + beforeAll(async () => { + await fs.promises.mkdir(SIMULATOR_APP_DIR, { recursive: true }); + await fs.promises.writeFile( + INFO_PLIST_PATH, + ` + + + + CFBundleShortVersionString + 2.0.1 + CFBundleVersion + 55 + +` + ); + }); + + afterAll(async () => { + await fs.promises.rm(SIMULATOR_APP_DIR, { recursive: true, force: true }); + }); + + it('extracts version from a .app directory', async () => { + const result = await extractIosVersionAsync(SIMULATOR_APP_DIR); + + expect(result).toEqual({ + appVersion: '2.0.1', + appBuildVersion: '55', + }); + }); + + it('extracts version from an .ipa file', async () => { + const result = await extractIosVersionAsync(IPA_FIXTURE_PATH); + + expect(result).toEqual({ + appVersion: '1.0', + appBuildVersion: '1', + }); + }); +}); + +describe(extractAndroidVersionAsync, () => { + afterEach(() => { + mockedSpawnAsync.mockReset(); + }); + + it('extracts version from an .apk via aapt2', async () => { + mockedSpawnAsync.mockResolvedValue({ + stdout: `package: name='com.example.app' versionCode='42' versionName='2.5.0'`, + stderr: '', + } as any); + + const result = await extractAndroidVersionAsync('/path/to/app.apk'); + + expect(mockedSpawnAsync).toHaveBeenCalledWith( + 'aapt2', + ['dump', 'badging', '/path/to/app.apk'], + { + stdio: 'pipe', + } + ); + expect(result).toEqual({ + appVersion: '2.5.0', + appBuildVersion: '42', + }); + }); + + it('extracts version from an .aab via bundletool', async () => { + mockedSpawnAsync.mockResolvedValue({ + stdout: ` + +`, + stderr: '', + } as any); + + const result = await extractAndroidVersionAsync('/path/to/app.aab'); + + expect(mockedSpawnAsync).toHaveBeenCalledWith( + 'bundletool', + ['dump', 'manifest', '--bundle', '/path/to/app.aab'], + { stdio: 'pipe' } + ); + expect(result).toEqual({ + appVersion: '3.1.0', + appBuildVersion: '10', + }); + }); + + it('returns empty object for unknown extension', async () => { + const result = await extractAndroidVersionAsync('/path/to/archive.zip'); + + expect(mockedSpawnAsync).not.toHaveBeenCalled(); + expect(result).toEqual({}); + }); +}); diff --git a/packages/build-tools/src/steps/functions/findAndUploadBuildArtifacts.ts b/packages/build-tools/src/steps/functions/findAndUploadBuildArtifacts.ts index 4803ff7fdb..94c7619ed2 100644 --- a/packages/build-tools/src/steps/functions/findAndUploadBuildArtifacts.ts +++ b/packages/build-tools/src/steps/functions/findAndUploadBuildArtifacts.ts @@ -1,5 +1,5 @@ import { BuildJob, Ios, ManagedArtifactType, Platform } from '@expo/eas-build-job'; -import { BuildFunction, BuildStepContext } from '@expo/steps'; +import { BuildFunction, BuildStepContext, BuildStepOutput } from '@expo/steps'; import { CustomBuildContext } from '../../customBuildContext'; import { findXcodeBuildLogsPathAsync } from '../../ios/xcodeBuildLogs'; @@ -13,7 +13,13 @@ export function createFindAndUploadBuildArtifactsBuildFunction( id: 'find_and_upload_build_artifacts', name: 'Find and upload build artifacts', __metricsId: 'eas/find_and_upload_build_artifacts', - fn: async stepCtx => { + outputProviders: [ + BuildStepOutput.createProvider({ + id: 'application_archive_path', + required: false, + }), + ], + fn: async (stepCtx, { outputs }) => { // We want each upload to print logs on its own // and we don't want to interleave logs from different uploads // so we execute uploads consecutively. @@ -24,7 +30,10 @@ export function createFindAndUploadBuildArtifactsBuildFunction( let firstError: any = null; try { - await uploadApplicationArchivesAsync({ ctx, stepCtx }); + const archivePath = await uploadApplicationArchivesAsync({ ctx, stepCtx }); + if (archivePath) { + outputs.application_archive_path.set(archivePath); + } } catch (err: unknown) { stepCtx.logger.error(`Failed to upload application archives.`, err); firstError ||= err; @@ -68,7 +77,7 @@ async function uploadApplicationArchivesAsync({ }: { ctx: CustomBuildContext; stepCtx: BuildStepContext; -}): Promise { +}): Promise { const applicationArchivePatternOrPath = ctx.job.platform === Platform.ANDROID ? (ctx.job.applicationArchivePath ?? 'android/app/build/outputs/**/*.{apk,aab}') @@ -99,6 +108,8 @@ async function uploadApplicationArchivesAsync({ logger, }); logger.info('Done.'); + + return applicationArchives[0]; } async function uploadBuildArtifacts({ diff --git a/packages/build-tools/src/steps/functions/readIpaInfo.ts b/packages/build-tools/src/steps/functions/readIpaInfo.ts index e5e21ab17e..a697908de4 100644 --- a/packages/build-tools/src/steps/functions/readIpaInfo.ts +++ b/packages/build-tools/src/steps/functions/readIpaInfo.ts @@ -109,7 +109,7 @@ export async function readIpaInfoAsync(ipaPath: string): Promise { } } -function parseInfoPlistBuffer(data: Buffer): Record { +export function parseInfoPlistBuffer(data: Buffer): Record { const isBinaryPlist = data.subarray(0, 8).toString('ascii') === 'bplist00'; if (isBinaryPlist) { const parsedBinaryPlists = bplistParser.parseBuffer(data); diff --git a/packages/build-tools/src/steps/functions/reportResolvedVersion.ts b/packages/build-tools/src/steps/functions/reportResolvedVersion.ts new file mode 100644 index 0000000000..b919e99271 --- /dev/null +++ b/packages/build-tools/src/steps/functions/reportResolvedVersion.ts @@ -0,0 +1,216 @@ +import { BuildJob, Platform } from '@expo/eas-build-job'; +import { + BuildFunction, + BuildStepInput, + BuildStepInputValueTypeName, + spawnAsync, +} from '@expo/steps'; +import { XMLParser } from 'fast-xml-parser'; +import fs from 'fs-extra'; +import { Client } from '@urql/core'; +import { graphql } from 'gql.tada'; +import path from 'node:path'; + +import { parseInfoPlistBuffer, readIpaInfoAsync } from './readIpaInfo'; +import { CustomBuildContext } from '../../customBuildContext'; + +export function createReportResolvedVersionBuildFunction( + ctx: CustomBuildContext +): BuildFunction { + return new BuildFunction({ + namespace: 'eas', + id: 'report_resolved_version', + name: 'Report resolved version', + __metricsId: 'eas/report_resolved_version', + inputProviders: [ + BuildStepInput.createProvider({ + id: 'application_archive_path', + required: false, + allowedValueTypeName: BuildStepInputValueTypeName.STRING, + }), + ], + fn: async (stepCtx, { inputs }) => { + try { + const archivePath = inputs.application_archive_path.value as string | undefined; + if (!archivePath) { + stepCtx.logger.info('No application archive path provided, skipping.'); + return; + } + + const { appVersion, appBuildVersion } = + ctx.job.platform === Platform.IOS + ? await extractIosVersionAsync(archivePath) + : await extractAndroidVersionAsync(archivePath); + + if (!appVersion && !appBuildVersion) { + stepCtx.logger.info('No resolved version found, skipping.'); + return; + } + + stepCtx.logger.info( + `Resolved version: ${appVersion ?? 'N/A'} (${appBuildVersion ?? 'N/A'})` + ); + + const buildId = ctx.env.EAS_BUILD_ID; + if (!buildId) { + stepCtx.logger.warn('EAS_BUILD_ID not set, cannot report resolved version.'); + return; + } + + await reportResolvedVersionAsync(ctx.graphqlClient, buildId, { + appVersion, + appBuildVersion, + }); + + stepCtx.logger.info('Reported resolved version to EAS.'); + } catch (err) { + stepCtx.logger.warn('Failed to report resolved version (non-fatal):', err); + } + }, + }); +} + +export async function extractIosVersionAsync( + archivePath: string +): Promise<{ appVersion?: string; appBuildVersion?: string }> { + const ext = path.extname(archivePath).toLowerCase(); + + if (ext === '.app') { + return await extractSimulatorAppVersionAsync(archivePath); + } + + const ipaInfo = await readIpaInfoAsync(archivePath); + return { + appVersion: ipaInfo.bundleShortVersion, + appBuildVersion: ipaInfo.bundleVersion, + }; +} + +async function extractSimulatorAppVersionAsync( + appPath: string +): Promise<{ appVersion?: string; appBuildVersion?: string }> { + const infoPlistPath = path.join(appPath, 'Info.plist'); + + if (!(await fs.pathExists(infoPlistPath))) { + return {}; + } + + const infoPlistBuffer = await fs.readFile(infoPlistPath); + const infoPlist = parseInfoPlistBuffer(infoPlistBuffer); + + return { + appVersion: + typeof infoPlist.CFBundleShortVersionString === 'string' + ? infoPlist.CFBundleShortVersionString + : undefined, + appBuildVersion: + typeof infoPlist.CFBundleVersion === 'string' ? infoPlist.CFBundleVersion : undefined, + }; +} + +export async function extractAndroidVersionAsync( + archivePath: string +): Promise<{ appVersion?: string; appBuildVersion?: string }> { + const ext = path.extname(archivePath).toLowerCase(); + + if (ext === '.apk') { + return await extractVersionFromApkAsync(archivePath); + } else if (ext === '.aab') { + return await extractVersionFromAabAsync(archivePath); + } + + return {}; +} + +async function extractVersionFromApkAsync( + apkPath: string +): Promise<{ appVersion?: string; appBuildVersion?: string }> { + const result = await spawnAsync('aapt2', ['dump', 'badging', apkPath], { + stdio: 'pipe', + }); + + return parseAaptOutput(result.stdout); +} + +async function extractVersionFromAabAsync( + aabPath: string +): Promise<{ appVersion?: string; appBuildVersion?: string }> { + const result = await spawnAsync('bundletool', ['dump', 'manifest', '--bundle', aabPath], { + stdio: 'pipe', + }); + + return parseManifestXml(result.stdout); +} + +export function parseAaptOutput(output: string): { appVersion?: string; appBuildVersion?: string } { + const versionNameMatch = output.match(/versionName='([^']+)'/); + const versionCodeMatch = output.match(/versionCode='([^']+)'/); + + return { + appVersion: versionNameMatch?.[1], + appBuildVersion: versionCodeMatch?.[1], + }; +} + +export function parseManifestXml(xml: string): { appVersion?: string; appBuildVersion?: string } { + const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' }); + const parsed = parser.parse(xml); + + const manifest = parsed?.manifest; + if (!manifest) { + return {}; + } + + const versionName = manifest['@_android:versionName']; + const versionCode = manifest['@_android:versionCode']; + + return { + appVersion: versionName != null ? String(versionName) : undefined, + appBuildVersion: versionCode != null ? String(versionCode) : undefined, + }; +} + +export async function reportResolvedVersionAsync( + graphqlClient: Client, + buildId: string, + { + appVersion, + appBuildVersion, + }: { + appVersion?: string; + appBuildVersion?: string; + } +): Promise { + const result = await graphqlClient + .mutation( + graphql(` + mutation ReportResolvedVersionMutation( + $buildId: ID! + $appVersion: String + $appBuildVersion: String + ) { + build { + updateBuildMetadata( + buildId: $buildId + metadata: { + appVersion: $appVersion + appBuildVersion: $appBuildVersion + } + ) { + id + } + } + } + `), + { + buildId, + appVersion: appVersion ?? null, + appBuildVersion: appBuildVersion ?? null, + } + ) + .toPromise(); + + if (result.error) { + throw result.error; + } +} diff --git a/packages/build-tools/src/utils/artifacts.ts b/packages/build-tools/src/utils/artifacts.ts index cdf8a8a773..7fe3e9e3dd 100644 --- a/packages/build-tools/src/utils/artifacts.ts +++ b/packages/build-tools/src/utils/artifacts.ts @@ -120,7 +120,7 @@ export async function uploadApplicationArchive( patternOrPath: string; rootDir: string; } -): Promise { +): Promise { const applicationArchives = await findArtifacts({ rootDir, patternOrPath, logger }); const artifactsSizes = await getArtifactsSizes(applicationArchives); logger.info(`Application archives:`); @@ -136,6 +136,8 @@ export async function uploadApplicationArchive( }, logger, }); + + return applicationArchives[0]; } async function getArtifactsSizes(artifacts: string[]): Promise> {