Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[eas-cli] create fingerprint on each update #2687

Merged
merged 2 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This is the log of notable changes to EAS CLI and related packages.

### 🎉 New features

- Calculate fingerprint on each update. ([#2687](https://github.com/expo/eas-cli/pull/2687) by [@quinlanj](https://github.com/quinlanj))

### 🐛 Bug fixes

### 🧹 Chores
Expand Down
11 changes: 4 additions & 7 deletions packages/eas-cli/src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends
}> {
const runtimeAndFingerprintMetadata =
await computeAndMaybeUploadFingerprintFromExpoUpdatesAsync(ctx);
if (!runtimeAndFingerprintMetadata?.fingerprint) {
if (!runtimeAndFingerprintMetadata?.fingerprintHash) {
const fingerprint = await computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync(ctx);
return {
...runtimeAndFingerprintMetadata,
Expand All @@ -690,10 +690,7 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
): Promise<{
runtimeVersion?: string;
fingerprintSource?: FingerprintSource;
fingerprint?: {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
};
fingerprintHash?: string;
}> {
const resolvedRuntimeVersion = await resolveRuntimeVersionAsync({
exp: ctx.exp,
Expand Down Expand Up @@ -727,7 +724,7 @@ async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Pl
return {
runtimeVersion: uploadedFingerprint.hash,
fingerprintSource: uploadedFingerprint.fingerprintSource,
fingerprint: resolvedRuntimeVersion.fingerprint,
fingerprintHash: resolvedRuntimeVersion.fingerprintHash ?? undefined,
};
}

Expand All @@ -740,7 +737,7 @@ async function computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync<T extends
}> {
const fingerprint = await createFingerprintAsync(ctx.projectDir, {
workflow: ctx.workflow,
platform: ctx.platform,
platforms: [ctx.platform],
env: ctx.env,
});
if (!fingerprint) {
Expand Down
60 changes: 43 additions & 17 deletions packages/eas-cli/src/commands/update/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import {
EnvironmentVariableEnvironment,
FingerprintInfoGroup,
PublishUpdateGroupInput,
StatuspageServiceName,
UpdateInfoGroup,
Expand All @@ -39,6 +40,7 @@ import {
getRuntimeVersionInfoObjectsAsync,
getUpdateMessageForCommandAsync,
isUploadedAssetCountAboveWarningThreshold,
maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync,
platformDisplayNames,
resolveInputDirectoryAsync,
uploadAssetsAsync,
Expand Down Expand Up @@ -403,22 +405,33 @@ export default class UpdatePublish extends EasCommand {
branchName,
});

const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping = await Promise.all(
runtimeToPlatformsAndFingerprintInfoMapping.map(async info => {
return {
...info,
fingerprintSource: info.fingerprint
? (
await maybeUploadFingerprintAsync({
hash: info.runtimeVersion,
fingerprint: info.fingerprint,
graphqlClient,
})
).fingerprintSource ?? null
: null,
};
})
);
const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMappingFromExpoUpdates =
await Promise.all(
runtimeToPlatformsAndFingerprintInfoMapping.map(async info => {
return {
...info,
fingerprintSource: info.fingerprint
? (
await maybeUploadFingerprintAsync({
hash: info.runtimeVersion,
fingerprint: info.fingerprint,
graphqlClient,
})
).fingerprintSource ?? null
: null,
};
})
);

const runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping =
await maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync({
projectDir,
graphqlClient,
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping:
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMappingFromExpoUpdates,
workflowsByPlatform: workflows,
env: undefined,
});

const runtimeVersionToRolloutInfoGroup =
rolloutPercentage !== undefined
Expand All @@ -436,7 +449,7 @@ export default class UpdatePublish extends EasCommand {
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] =
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.map(
({ runtimeVersion, platforms, fingerprintSource }) => {
({ runtimeVersion, platforms, fingerprintSource, fingerprintInfoGroup }) => {
const localUpdateInfoGroup = Object.fromEntries(
platforms.map(platform => [
platform,
Expand All @@ -455,6 +468,18 @@ export default class UpdatePublish extends EasCommand {
])
)
: null;
const transformedFingerprintInfoGroup = Object.entries(fingerprintInfoGroup).reduce(
(prev, [platform, fingerprintInfo]) => {
return {
...prev,
[platform]: {
...fingerprintInfo,
fingerprintSource: transformFingerprintSource(fingerprintInfo.fingerprintSource),
},
};
},
{} as FingerprintInfoGroup
);

return {
branchId,
Expand All @@ -463,6 +488,7 @@ export default class UpdatePublish extends EasCommand {
runtimeFingerprintSource: fingerprintSource
? transformFingerprintSource(fingerprintSource)
: null,
fingerprintInfoGroup: transformedFingerprintInfoGroup,
runtimeVersion,
message: updateMessage,
gitCommitHash,
Expand Down
142 changes: 126 additions & 16 deletions packages/eas-cli/src/project/publish.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ExpoConfig, Platform as ExpoConfigPlatform } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Env, Workflow } from '@expo/eas-build-job';
import { Env, FingerprintSource, Platform, Workflow } from '@expo/eas-build-job';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import chalk from 'chalk';
Expand All @@ -12,6 +12,7 @@ import nullthrows from 'nullthrows';
import path from 'path';
import promiseLimit from 'promise-limit';

import { maybeUploadFingerprintAsync } from './maybeUploadFingerprintAsync';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from './projectUtils';
import { resolveRuntimeVersionUsingCLIAsync } from './resolveRuntimeVersionAsync';
import { selectBranchOnAppAsync } from '../branch/queries';
Expand Down Expand Up @@ -47,7 +48,9 @@ import { ExpoUpdatesCLIModuleNotFoundError } from '../utils/expoUpdatesCli';
import chunk from '../utils/expodash/chunk';
import { truthy } from '../utils/expodash/filter';
import groupBy from '../utils/expodash/groupBy';
import mapMapAsync from '../utils/expodash/mapMapAsync';
import uniqBy from '../utils/expodash/uniqBy';
import { FingerprintOptions, createFingerprintsByKeyAsync } from '../utils/fingerprintCli';
import { Client } from '../vcs/vcs';

// update publish does not currently support web
Expand Down Expand Up @@ -728,6 +731,16 @@ export type RuntimeVersionInfo = {
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
fingerprintHash: string | null;
};

type FingerprintInfoGroup = {
[key in UpdatePublishPlatform]?: FingerprintInfo;
};

type FingerprintInfo = {
fingerprintHash: string;
fingerprintSource: FingerprintSource;
};

export async function getRuntimeVersionInfoObjectsAsync({
Expand Down Expand Up @@ -782,6 +795,7 @@ async function getRuntimeVersionInfoForPlatformAsync({
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
fingerprintHash: string | null;
}> {
if (await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir)) {
try {
Expand Down Expand Up @@ -833,6 +847,7 @@ async function getRuntimeVersionInfoForPlatformAsync({
return {
runtimeVersion: resolvedRuntimeVersion,
fingerprint: null,
fingerprintHash: null,
};
}

Expand All @@ -858,9 +873,119 @@ export function getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersion
runtimeVersionInfoObjects.map(
runtimeVersionInfoObject => runtimeVersionInfoObject.runtimeVersionInfo.fingerprint
)[0] ?? null,
fingerprintHash:
runtimeVersionInfoObjects.map(
runtimeVersionInfoObject => runtimeVersionInfoObject.runtimeVersionInfo.fingerprintHash
)[0] ?? null,
};
}
);
}

export async function maybeCalculateFingerprintForRuntimeVersionInfoObjectsWithoutExpoUpdatesAsync({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the logic of this function is kind of complicated. would be good to split as smaller functions and add some unit test.
but fine if that requires too much effort.

projectDir,
graphqlClient,
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping,
workflowsByPlatform,
env,
}: {
projectDir: string;
graphqlClient: ExpoGraphqlClient;
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping: (RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
fingerprintSource: FingerprintSource | null;
})[];
workflowsByPlatform: Record<Platform, Workflow>;
env: Env | undefined;
}): Promise<
(RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
fingerprintSource: FingerprintSource | null;
fingerprintInfoGroup: FingerprintInfoGroup;
})[]
> {
const runtimesToComputeFingerprintsFor =
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping.filter(
infoGroup => !infoGroup.fingerprintHash
);
const fingerprintOptionsByRuntimeAndPlatform = new Map<string, FingerprintOptions>();
for (const infoGroup of runtimesToComputeFingerprintsFor) {
for (const platform of infoGroup.platforms) {
const runtimeAndPlatform = `${infoGroup.runtimeVersion}-${platform}`;
const options = {
platforms: [platform],
workflow: workflowsByPlatform[platform],
projectDir,
env,
};
fingerprintOptionsByRuntimeAndPlatform.set(runtimeAndPlatform, options);
}
}
const fingerprintsByRuntimeAndPlatform = await createFingerprintsByKeyAsync(
projectDir,
fingerprintOptionsByRuntimeAndPlatform
);
const uploadedFingerprintsByRuntimeAndPlatform = await mapMapAsync(
fingerprintsByRuntimeAndPlatform,
async fingerprint => {
return {
...fingerprint,
uploadedSource: (
await maybeUploadFingerprintAsync({
hash: fingerprint.hash,
fingerprint: {
fingerprintSources: fingerprint.sources,
isDebugFingerprintSource: fingerprint.isDebugSource,
},
graphqlClient,
})
).fingerprintSource,
};
}
);
const runtimesWithComputedFingerprint = runtimesToComputeFingerprintsFor.map(runtimeInfo => {
const fingerprintInfoGroup: FingerprintInfoGroup = {};
for (const platform of runtimeInfo.platforms) {
const runtimeAndPlatform = `${runtimeInfo.runtimeVersion}-${platform}`;
const fingerprint = uploadedFingerprintsByRuntimeAndPlatform.get(runtimeAndPlatform);
if (fingerprint && fingerprint.uploadedSource) {
fingerprintInfoGroup[platform] = {
fingerprintHash: fingerprint.hash,
fingerprintSource: fingerprint.uploadedSource,
};
}
}
return {
...runtimeInfo,
fingerprintInfoGroup,
};
});

// These are runtimes whose fingerprint has already been computed and uploaded with EAS Update fingerprint runtime policy
const runtimesWithPreviouslyComputedFingerprints =
runtimeToPlatformsAndFingerprintInfoAndFingerprintSourceMapping
.filter(
(
infoGroup
): infoGroup is RuntimeVersionInfo & {
platforms: UpdatePublishPlatform[];
fingerprintSource: FingerprintSource;
fingerprintHash: string;
} => !!infoGroup.fingerprintHash && !!infoGroup.fingerprintSource
)
.map(infoGroup => {
const platform = infoGroup.platforms[0];
return {
...infoGroup,
fingerprintInfoGroup: {
[platform]: {
fingerprintHash: infoGroup.fingerprintHash,
fingerprintSource: infoGroup.fingerprintSource,
},
},
};
});
return [...runtimesWithComputedFingerprint, ...runtimesWithPreviouslyComputedFingerprints];
}

export const platformDisplayNames: Record<UpdatePublishPlatform, string> = {
Expand All @@ -873,21 +998,6 @@ export const updatePublishPlatformToAppPlatform: Record<UpdatePublishPlatform, A
ios: AppPlatform.Ios,
};

const mapMapAsync = async function <K, V, M>(
map: ReadonlyMap<K, V>,
mapper: (value: V, key: K) => Promise<M>
): Promise<Map<K, M>> {
const resultingMap: Map<K, M> = new Map();
await Promise.all(
Array.from(map.keys()).map(async k => {
const initialValue = map.get(k) as V;
const result = await mapper(initialValue, k);
resultingMap.set(k, result);
})
);
return resultingMap;
};

export async function getRuntimeToUpdateRolloutInfoGroupMappingAsync(
graphqlClient: ExpoGraphqlClient,
{
Expand Down
6 changes: 6 additions & 0 deletions packages/eas-cli/src/project/resolveRuntimeVersionAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export async function resolveRuntimeVersionUsingCLIAsync({
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
fingerprintHash: string | null;
}> {
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');

Expand All @@ -52,6 +53,9 @@ export async function resolveRuntimeVersionUsingCLIAsync({
isDebugFingerprintSource: useDebugFingerprintSource,
}
: null,
fingerprintHash: runtimeVersionResult.fingerprintSources
? runtimeVersionResult.runtimeVersion
: null,
};
}

Expand All @@ -75,13 +79,15 @@ export async function resolveRuntimeVersionAsync({
fingerprintSources: object[];
isDebugFingerprintSource: boolean;
} | null;
fingerprintHash: string | null;
} | null> {
if (!(await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir))) {
// fall back to the previous behavior (using the @expo/config-plugins eas-cli dependency rather
// than the versioned @expo/config-plugins dependency in the project)
return {
runtimeVersion: await Updates.getRuntimeVersionNullableAsync(projectDir, exp, platform),
fingerprint: null,
fingerprintHash: null,
};
}

Expand Down
14 changes: 14 additions & 0 deletions packages/eas-cli/src/utils/expodash/mapMapAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default async function mapMapAsync<K, V, M>(
map: ReadonlyMap<K, V>,
mapper: (value: V, key: K) => Promise<M>
): Promise<Map<K, M>> {
const resultingMap: Map<K, M> = new Map();
await Promise.all(
Array.from(map.keys()).map(async k => {
const initialValue = map.get(k) as V;
const result = await mapper(initialValue, k);
resultingMap.set(k, result);
})
);
return resultingMap;
}
Loading
Loading