From a20bd0051e49464f9d5b9b55140c5bf2d12bf8a2 Mon Sep 17 00:00:00 2001 From: Quinlan Jung Date: Wed, 29 Jan 2025 20:54:49 -0800 Subject: [PATCH] [eas-cli] release fingerprint:compare (#2821) * [eas-cli] release fingerprint:compare * Temporary Commit at 1/15/2025, 11:33:18 PM * Temporary Commit at 1/28/2025, 4:41:48 PM * Temporary Commit at 1/28/2025, 5:03:51 PM * Temporary Commit at 1/28/2025, 5:33:24 PM * pr feedbak * Temporary Commit at 1/29/2025, 4:07:55 PM * fix lint --- CHANGELOG.md | 1 + packages/eas-cli/graphql.schema.json | 464 +++++++++++++++++- .../src/commands/fingerprint/compare.ts | 356 +++++++++++--- packages/eas-cli/src/graphql/generated.ts | 76 ++- .../src/graphql/queries/FingerprintQuery.ts | 98 ++++ .../eas-cli/src/graphql/types/Fingerprint.ts | 18 + packages/eas-cli/src/utils/fingerprintCli.ts | 17 +- 7 files changed, 943 insertions(+), 87 deletions(-) create mode 100644 packages/eas-cli/src/graphql/queries/FingerprintQuery.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e4cfd7d1e4..7abfd676bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This is the log of notable changes to EAS CLI and related packages. - Prompt to set non-exempt encryption status for the iOS app to support faster store submissions. ([#2843](https://github.com/expo/eas-cli/pull/2843) by [@EvanBacon](https://github.com/EvanBacon)) - Automatically create internal TestFlight group in EAS Submit command. ([#2839](https://github.com/expo/eas-cli/pull/2839) by [@evanbacon](https://github.com/evanbacon)) - Sanitize and generate names for EAS Submit to prevent failures due to invalid characters or taken names. ([#2842](https://github.com/expo/eas-cli/pull/2842) by [@evanbacon](https://github.com/evanbacon)) +- Release `eas fingerprint:compare`. ([#2821](https://github.com/expo/eas-cli/pull/2821) by [@quinlanj](https://github.com/quinlanj)) ### 🐛 Bug fixes diff --git a/packages/eas-cli/graphql.schema.json b/packages/eas-cli/graphql.schema.json index 31ac2446e7..3a1041b7c1 100644 --- a/packages/eas-cli/graphql.schema.json +++ b/packages/eas-cli/graphql.schema.json @@ -6923,6 +6923,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "environment", + "description": null, + "type": { + "kind": "ENUM", + "name": "EnvironmentVariableEnvironment", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "experimental", "description": null, @@ -10373,6 +10385,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filter", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "UpdateFilterInput", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "first", "description": null, @@ -19744,7 +19768,7 @@ "ofType": null }, "isDeprecated": true, - "deprecationReason": "Use 'runtime.fingerprintDebugInfoUrl' instead." + "deprecationReason": "Use 'runtime.fingerprint.debugInfoUrl' instead." }, { "name": "xcodeBuildLogsUrl", @@ -19987,6 +20011,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "developmentClient", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "distribution", "description": null, @@ -28068,6 +28104,55 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "deleteBulkEnvironmentVariables", + "description": "Bulk delete environment variables", + "args": [ + { + "name": "ids", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DeleteEnvironmentVariableResult", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "deleteEnvironmentVariable", "description": "Delete an environment variable", @@ -28272,6 +28357,55 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "updateBulkEnvironmentVariables", + "description": "Bulk update environment variables", + "args": [ + { + "name": "environmentVariablesData", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "UpdateEnvironmentVariableInput", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "EnvironmentVariable", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "updateEnvironmentVariable", "description": "Update an environment variable", @@ -30085,7 +30219,7 @@ "fields": [ { "name": "account", - "description": null, + "description": "The Expo account that owns the installation entity.", "args": [], "type": { "kind": "NON_NULL", @@ -30296,6 +30430,29 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "GitHubAppInstallationAccountType", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "ORGANIZATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "USER", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "GitHubAppInstallationMetadata", @@ -30315,7 +30472,7 @@ }, { "name": "githubAccountName", - "description": null, + "description": "The login of the GitHub account that owns the installation. Not the display name.", "args": [], "type": { "kind": "SCALAR", @@ -30325,6 +30482,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "githubAccountType", + "description": null, + "args": [], + "type": { + "kind": "ENUM", + "name": "GitHubAppInstallationAccountType", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "installationStatus", "description": null, @@ -34867,6 +35036,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "environment", + "description": null, + "type": { + "kind": "ENUM", + "name": "EnvironmentVariableEnvironment", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "experimental", "description": null, @@ -35709,6 +35890,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "artifacts", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "WorkflowArtifact", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "childJobRun", "description": null, @@ -39891,18 +40096,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "runtimeFingerprintSource", - "description": null, - "type": { - "kind": "INPUT_OBJECT", - "name": "FingerprintSourceInput", - "ofType": null - }, - "defaultValue": null, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "runtimeVersion", "description": null, @@ -43086,12 +43279,12 @@ "deprecationReason": null }, { - "name": "fingerprintDebugInfoUrl", + "name": "fingerprint", "description": null, "args": [], "type": { - "kind": "SCALAR", - "name": "String", + "kind": "OBJECT", + "name": "Fingerprint", "ofType": null }, "isDeprecated": false, @@ -48693,6 +48886,53 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "UpdateFilterInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "fingerprintHash", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasFingerprint", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "runtimeVersion", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "UpdateGitHubBuildTriggerInput", @@ -58844,6 +59084,149 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "WorkflowArtifact", + "description": null, + "fields": [ + { + "name": "contentType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "downloadUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileSizeBytes", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "filename", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "jobRun", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "JobRun", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "DateTime", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "WorkflowJob", @@ -60367,6 +60750,18 @@ "name": "retryWorkflowRun", "description": null, "args": [ + { + "name": "fromFailedJobs", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "workflowRunId", "description": null, @@ -60513,6 +60908,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "GITHUB_PULL_REQUEST_OPENED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GITHUB_PULL_REQUEST_REOPENED", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GITHUB_PULL_REQUEST_SYNCHRONIZE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "GITHUB_PUSH", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "MANUAL", "description": null, @@ -61643,6 +62062,15 @@ } ] }, + { + "name": "oneOf", + "description": "Indicates exactly one field must be supplied and this field must not be `null`.", + "isRepeatable": false, + "locations": [ + "INPUT_OBJECT" + ], + "args": [] + }, { "name": "skip", "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", diff --git a/packages/eas-cli/src/commands/fingerprint/compare.ts b/packages/eas-cli/src/commands/fingerprint/compare.ts index 00b556bf41..91573f5b8e 100644 --- a/packages/eas-cli/src/commands/fingerprint/compare.ts +++ b/packages/eas-cli/src/commands/fingerprint/compare.ts @@ -1,4 +1,4 @@ -import { Platform } from '@expo/eas-build-job'; +import { Platform, Workflow } from '@expo/eas-build-job'; import { Flags } from '@oclif/core'; import chalk from 'chalk'; @@ -6,9 +6,15 @@ import EasCommand from '../../commandUtils/EasCommand'; import { fetchBuildsAsync, formatBuild } from '../../commandUtils/builds'; import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags'; -import { AppPlatform, BuildStatus } from '../../graphql/generated'; +import { + AppPlatform, + BuildFragment, + BuildStatus, + FingerprintFragment, +} from '../../graphql/generated'; import { FingerprintMutation } from '../../graphql/mutations/FingerprintMutation'; import { BuildQuery } from '../../graphql/queries/BuildQuery'; +import { FingerprintQuery } from '../../graphql/queries/FingerprintQuery'; import Log from '../../log'; import { ora } from '../../ora'; import { RequestedPlatform } from '../../platform'; @@ -20,11 +26,52 @@ import { Fingerprint, FingerprintDiffItem } from '../../utils/fingerprint'; import { createFingerprintAsync, diffFingerprint } from '../../utils/fingerprintCli'; import { abridgedDiff } from '../../utils/fingerprintDiff'; import formatFields, { FormatFieldsItem } from '../../utils/formatFields'; -import { enableJsonOutput } from '../../utils/json'; +import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; +import { Client } from '../../vcs/vcs'; + +export interface FingerprintCompareFlags { + buildId?: string; + hash1?: string; + hash2?: string; + nonInteractive: boolean; + json: boolean; +} + +enum FingerprintOriginType { + Build = 'build', + Hash = 'hash', + Project = 'project', +} + +type FingerprintOrigin = { + type: FingerprintOriginType; + build?: BuildFragment; +}; export default class FingerprintCompare extends EasCommand { static override description = 'compare fingerprints of the current project, builds and updates'; - static override hidden = true; + static override strict = false; + + static override examples = [ + '$ eas fingerprint:compare \t # Compare fingerprints in interactive mode', + '$ eas fingerprint:compare c71a7d475aa6f75291bc93cd74aef395c3c94eee \t # Compare fingerprint against local directory', + '$ eas fingerprint:compare c71a7d475aa6f75291bc93cd74aef395c3c94eee f0d6a916e73f401d428e6e006e07b12453317ba2 \t # Compare provided fingerprints', + '$ eas fingerprint:compare --build-id 82bc6456-611a-48cb-8db4-5f9eb2ca1003 \t # Compare fingerprint from build against local directory', + ]; + + static override args = [ + { + name: 'hash1', + description: + "If provided alone, HASH1 is compared against the current project's fingerprint.", + required: false, + }, + { + name: 'hash2', + description: 'If two hashes are provided, HASH1 is compared against HASH2.', + required: false, + }, + ]; static override flags = { 'build-id': Flags.string({ @@ -42,8 +89,10 @@ export default class FingerprintCompare extends EasCommand { }; async runAsync(): Promise { - const { flags } = await this.parse(FingerprintCompare); - const { json: jsonFlag, 'non-interactive': nonInteractive, buildId: buildIdFromArg } = flags; + const { args, flags } = await this.parse(FingerprintCompare); + const { hash1, hash2 } = args; + const { json, 'non-interactive': nonInteractive, 'build-id': buildId } = flags; + const sanitizedFlagsAndArgs = { json, nonInteractive, buildId, hash1, hash2 }; const { projectId, @@ -54,70 +103,52 @@ export default class FingerprintCompare extends EasCommand { nonInteractive, withServerSideEnvironment: null, }); - if (jsonFlag) { + if (json) { enableJsonOutput(); } - const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId); - let buildId: string | null = buildIdFromArg; - if (!buildId) { - if (nonInteractive) { - throw new Error('Build ID must be provided in non-interactive mode'); - } + const firstFingerprintInfo = await getFirstFingerprintInfoAsync( + graphqlClient, + projectId, + sanitizedFlagsAndArgs + ); + const { fingerprint: firstFingerprint, origin: firstFingerprintOrigin } = firstFingerprintInfo; - buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, { - filters: { hasFingerprint: true }, - }); - if (!buildId) { - return; - } - } + const secondFingerprintInfo = await getSecondFingerprintInfoAsync( + graphqlClient, + projectDir, + projectId, + vcsClient, + firstFingerprintInfo, + sanitizedFlagsAndArgs + ); + const { fingerprint: secondFingerprint, origin: secondFingerprintOrigin } = + secondFingerprintInfo; - Log.log(`Comparing fingerprints of the current project and build ${buildId}…`); - const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId); - const fingerprintDebugUrl = buildWithFingerprint.fingerprint?.debugInfoUrl; - if (!fingerprintDebugUrl) { - Log.error('A fingerprint for the build could not be found.'); - return; - } - const fingerprintResponse = await fetch(fingerprintDebugUrl); - const fingerprint = (await fingerprintResponse.json()) as Fingerprint; - const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient); - const buildPlatform = buildWithFingerprint.platform; - const workflow = workflows[appPlatformToPlatform(buildPlatform)]; - - const projectFingerprint = await createFingerprintAsync(projectDir, { - workflow, - platforms: [appPlatformToString(buildPlatform)], - debug: true, - env: undefined, - }); - if (!projectFingerprint) { - Log.error('Project fingerprints can only be computed for projects with SDK 52 or higher'); + if (json) { + printJsonOnlyOutput({ fingerprint1: firstFingerprint, fingerprint2: secondFingerprint }); return; } - const uploadedFingerprint = await maybeUploadFingerprintAsync({ - hash: fingerprint.hash, - fingerprint: { - fingerprintSources: fingerprint.sources, - isDebugFingerprintSource: Log.isDebug, - }, - graphqlClient, - }); - await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, { - hash: uploadedFingerprint.hash, - source: uploadedFingerprint.fingerprintSource, - }); - - if (fingerprint.hash === projectFingerprint.hash) { - Log.log(`✅ Project fingerprint matches build`); + if (firstFingerprint.hash === secondFingerprint.hash) { + Log.log( + `✅ ${capitalizeFirstLetter( + prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin) + )} matches fingerprint from ${prettyPrintFingerprint( + secondFingerprint, + secondFingerprintOrigin + )}` + ); return; } else { - Log.log(`🔄 Project fingerprint differs from build`); + Log.log( + `🔄 ${capitalizeFirstLetter( + prettyPrintFingerprint(firstFingerprint, firstFingerprintOrigin) + )} differs from ${prettyPrintFingerprint(secondFingerprint, secondFingerprintOrigin)}` + ); } - const fingerprintDiffs = diffFingerprint(projectDir, fingerprint, projectFingerprint); + const fingerprintDiffs = diffFingerprint(projectDir, firstFingerprint, secondFingerprint); if (!fingerprintDiffs) { Log.error('Fingerprint diffs can only be computed for projects with SDK 52 or higher'); return; @@ -170,6 +201,204 @@ export default class FingerprintCompare extends EasCommand { } } +function prettyPrintFingerprint(fingerprint: Fingerprint, origin: FingerprintOrigin): string { + if (origin.type === FingerprintOriginType.Hash) { + return `fingerprint ${fingerprint.hash} from hash`; + } + return `fingerprint ${fingerprint.hash} from ${origin.type}`; +} + +function capitalizeFirstLetter(string: string): string { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +async function getFirstFingerprintInfoAsync( + graphqlClient: ExpoGraphqlClient, + projectId: string, + { buildId: buildIdFromArg, hash1, nonInteractive }: FingerprintCompareFlags +): Promise<{ fingerprint: Fingerprint; platforms?: AppPlatform[]; origin: FingerprintOrigin }> { + if (hash1) { + const fingerprintFragment = await getFingerprintFragmentFromHashAsync( + graphqlClient, + projectId, + hash1 + ); + const fingerprint = await getFingerprintFromFingerprintFragmentAsync(fingerprintFragment); + let platforms; + const fingerprintBuilds = fingerprintFragment.builds?.edges.map(edge => edge.node) ?? []; + const fingerprintUpdates = fingerprintFragment.updates?.edges.map(edge => edge.node) ?? []; + if (fingerprintBuilds.length > 0) { + platforms = [fingerprintBuilds[0].platform]; + } else if (fingerprintUpdates.length > 0) { + platforms = [stringToAppPlatform(fingerprintUpdates[0].platform)]; + } + return { + fingerprint, + platforms, + origin: { + type: FingerprintOriginType.Hash, + }, + }; + } + + let buildId: string | null = buildIdFromArg ?? null; + if (!buildId) { + if (nonInteractive) { + throw new Error('Build ID must be provided in non-interactive mode'); + } + + const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId); + buildId = await selectBuildToCompareAsync(graphqlClient, projectId, displayName, { + filters: { hasFingerprint: true }, + }); + if (!buildId) { + throw new Error('Must select build with fingerprint for comparison.'); + } + } + + Log.log(`Comparing fingerprints of the current project and build ${buildId}…`); + const buildWithFingerprint = await BuildQuery.withFingerprintByIdAsync(graphqlClient, buildId); + if (!buildWithFingerprint.fingerprint) { + throw new Error(`Fingerprint for build ${buildId} was not computed.`); + } else if (!buildWithFingerprint.fingerprint.debugInfoUrl) { + throw new Error(`Fingerprint source for build ${buildId} was not computed.`); + } + return { + fingerprint: await getFingerprintFromFingerprintFragmentAsync(buildWithFingerprint.fingerprint), + platforms: [buildWithFingerprint.platform], + origin: { + type: FingerprintOriginType.Build, + build: buildWithFingerprint, + }, + }; +} + +async function getSecondFingerprintInfoAsync( + graphqlClient: ExpoGraphqlClient, + projectDir: string, + projectId: string, + vcsClient: Client, + firstFingerprintInfo: { + fingerprint: Fingerprint; + platforms?: AppPlatform[]; + origin: FingerprintOrigin; + }, + { hash2 }: FingerprintCompareFlags +): Promise<{ fingerprint: Fingerprint; origin: FingerprintOrigin }> { + if (hash2) { + const fingerprintFragment = await getFingerprintFragmentFromHashAsync( + graphqlClient, + projectId, + hash2 + ); + if (!fingerprintFragment) { + throw new Error(`Fingerprint with hash ${hash2} was not uploaded.`); + } + return { + fingerprint: await getFingerprintFromFingerprintFragmentAsync(fingerprintFragment), + origin: { type: FingerprintOriginType.Hash }, + }; + } + + const firstFingerprintPlatforms = firstFingerprintInfo.platforms; + if (!firstFingerprintPlatforms) { + throw new Error( + `Cannot compare the local directory against the provided fingerprint hash "${firstFingerprintInfo.fingerprint.hash}" because the associated platform could not be determined. Ensure the fingerprint is linked to a build or update to identify the platform.` + ); + } + + const workflows = await resolveWorkflowPerPlatformAsync(projectDir, vcsClient); + const optionsFromWorkflow = getFingerprintOptionsFromWorkflow( + firstFingerprintPlatforms, + workflows + ); + + const projectFingerprint = await createFingerprintAsync(projectDir, { + ...optionsFromWorkflow, + platforms: firstFingerprintPlatforms.map(appPlatformToString), + debug: true, + env: undefined, + }); + if (!projectFingerprint) { + throw new Error('Project fingerprints can only be computed for projects with SDK 52 or higher'); + } + + const uploadedFingerprint = await maybeUploadFingerprintAsync({ + hash: projectFingerprint.hash, + fingerprint: { + fingerprintSources: projectFingerprint.sources, + isDebugFingerprintSource: Log.isDebug, + }, + graphqlClient, + }); + await FingerprintMutation.createFingerprintAsync(graphqlClient, projectId, { + hash: uploadedFingerprint.hash, + source: uploadedFingerprint.fingerprintSource, + }); + + return { fingerprint: projectFingerprint, origin: { type: FingerprintOriginType.Project } }; +} + +async function getFingerprintFragmentFromHashAsync( + graphqlClient: ExpoGraphqlClient, + projectId: string, + hash: string +): Promise { + const fingerprint = await FingerprintQuery.byHashAsync(graphqlClient, { + appId: projectId, + hash, + }); + if (!fingerprint) { + const displayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId); + throw new Error(`Fingerprint with hash ${hash} was not uploaded for ${displayName}.`); + } + return fingerprint; +} + +async function getFingerprintFromFingerprintFragmentAsync( + fingerprintFragment: FingerprintFragment +): Promise { + const fingerprintDebugUrl = fingerprintFragment.debugInfoUrl; + if (!fingerprintDebugUrl) { + throw new Error( + `The source for fingerprint hash ${fingerprintFragment.hash} was not computed.` + ); + } + const fingerprintResponse = await fetch(fingerprintDebugUrl); + return (await fingerprintResponse.json()) as Fingerprint; +} + +function getFingerprintOptionsFromWorkflow( + platforms: AppPlatform[], + workflowsByPlatform: Record +): { workflow?: Workflow; ignorePaths?: string[] } { + if (platforms.length === 0) { + throw new Error('Could not determine platform from fingerprint sources'); + } + + // Single platform case + if (platforms.length === 1) { + const platform = platforms[0]; + return { workflow: workflowsByPlatform[appPlatformToPlatform(platform)] }; + } + + // Multiple platforms case + const workflows = platforms.map(platform => workflowsByPlatform[appPlatformToPlatform(platform)]); + + // If all workflows are the same, return the common workflow + const [firstWorkflow, ...restWorkflows] = workflows; + if (restWorkflows.every(workflow => workflow === firstWorkflow)) { + return { workflow: firstWorkflow }; + } + + // Generate ignorePaths for mixed workflows + const ignorePaths = platforms + .filter(platform => workflowsByPlatform[appPlatformToPlatform(platform)] === Workflow.MANAGED) + .map(platform => `${appPlatformToString(platform)}/**/*`); + + return { ignorePaths }; +} + function printContentDiff(diff: FingerprintDiffItem): void { if (diff.op === 'added') { const sourceType = diff.addedSource.type; @@ -350,6 +579,17 @@ function appPlatformToString(platform: AppPlatform): string { } } +function stringToAppPlatform(platform: string): AppPlatform { + switch (platform) { + case 'android': + return AppPlatform.Android; + case 'ios': + return AppPlatform.Ios; + default: + throw new Error(`Unsupported platform: ${platform}`); + } +} + export async function selectBuildToCompareAsync( graphqlClient: ExpoGraphqlClient, projectId: string, diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index bad0aa7ad4..8a97d3f12a 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -1090,6 +1090,7 @@ export type AndroidJobInput = { cache?: InputMaybe; customBuildConfig?: InputMaybe; developmentClient?: InputMaybe; + environment?: InputMaybe; experimental?: InputMaybe; gradleCommand?: InputMaybe; loggerLevel?: InputMaybe; @@ -1601,6 +1602,7 @@ export type AppUpdatesArgs = { export type AppUpdatesPaginatedArgs = { after?: InputMaybe; before?: InputMaybe; + filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; }; @@ -2902,7 +2904,7 @@ export type BuildArtifacts = { applicationArchiveUrl?: Maybe; buildArtifactsUrl?: Maybe; buildUrl?: Maybe; - /** @deprecated Use 'runtime.fingerprintDebugInfoUrl' instead. */ + /** @deprecated Use 'runtime.fingerprint.debugInfoUrl' instead. */ fingerprintUrl?: Maybe; xcodeBuildLogsUrl?: Maybe; }; @@ -2933,6 +2935,7 @@ export type BuildFilter = { appVersion?: InputMaybe; buildProfile?: InputMaybe; channel?: InputMaybe; + developmentClient?: InputMaybe; distribution?: InputMaybe; fingerprintHash?: InputMaybe; gitCommitHash?: InputMaybe; @@ -4092,6 +4095,8 @@ export type EnvironmentVariableMutation = { createEnvironmentVariableForAccount: EnvironmentVariable; /** Create an environment variable for an App */ createEnvironmentVariableForApp: EnvironmentVariable; + /** Bulk delete environment variables */ + deleteBulkEnvironmentVariables: Array; /** Delete an environment variable */ deleteEnvironmentVariable: DeleteEnvironmentVariableResult; /** Bulk link shared environment variables */ @@ -4100,6 +4105,8 @@ export type EnvironmentVariableMutation = { linkSharedEnvironmentVariable: EnvironmentVariable; /** Unlink shared environment variable */ unlinkSharedEnvironmentVariable: EnvironmentVariable; + /** Bulk update environment variables */ + updateBulkEnvironmentVariables: Array; /** Update an environment variable */ updateEnvironmentVariable: EnvironmentVariable; }; @@ -4129,6 +4136,11 @@ export type EnvironmentVariableMutationCreateEnvironmentVariableForAppArgs = { }; +export type EnvironmentVariableMutationDeleteBulkEnvironmentVariablesArgs = { + ids: Array; +}; + + export type EnvironmentVariableMutationDeleteEnvironmentVariableArgs = { id: Scalars['ID']['input']; }; @@ -4153,6 +4165,11 @@ export type EnvironmentVariableMutationUnlinkSharedEnvironmentVariableArgs = { }; +export type EnvironmentVariableMutationUpdateBulkEnvironmentVariablesArgs = { + environmentVariablesData: Array; +}; + + export type EnvironmentVariableMutationUpdateEnvironmentVariableArgs = { environmentVariableData: UpdateEnvironmentVariableInput; }; @@ -4384,6 +4401,7 @@ export enum GitHubAppEnvironment { export type GitHubAppInstallation = { __typename?: 'GitHubAppInstallation'; + /** The Expo account that owns the installation entity. */ account: Account; actor?: Maybe; id: Scalars['ID']['output']; @@ -4403,10 +4421,17 @@ export type GitHubAppInstallationAccessibleRepository = { url: Scalars['String']['output']; }; +export enum GitHubAppInstallationAccountType { + Organization = 'ORGANIZATION', + User = 'USER' +} + export type GitHubAppInstallationMetadata = { __typename?: 'GitHubAppInstallationMetadata'; githubAccountAvatarUrl?: Maybe; + /** The login of the GitHub account that owns the installation. Not the display name. */ githubAccountName?: Maybe; + githubAccountType?: Maybe; installationStatus: GitHubAppInstallationStatus; }; @@ -5050,6 +5075,7 @@ export type IosJobInput = { developmentClient?: InputMaybe; /** @deprecated */ distribution?: InputMaybe; + environment?: InputMaybe; experimental?: InputMaybe; loggerLevel?: InputMaybe; mode?: InputMaybe; @@ -5142,6 +5168,7 @@ export type IosSubmissionConfigInput = { export type JobRun = { __typename?: 'JobRun'; app: App; + artifacts: Array; /** @deprecated No longer supported */ childJobRun?: Maybe; createdAt: Scalars['DateTime']['output']; @@ -5744,7 +5771,6 @@ export type PublishUpdateGroupInput = { message?: InputMaybe; rollBackToEmbeddedInfoGroup?: InputMaybe; rolloutInfoGroup?: InputMaybe; - runtimeFingerprintSource?: InputMaybe; runtimeVersion: Scalars['String']['input']; turtleJobRunId?: InputMaybe; updateInfoGroup?: InputMaybe; @@ -6182,7 +6208,7 @@ export type Runtime = { builds: AppBuildsConnection; createdAt: Scalars['DateTime']['output']; deployments: DeploymentsConnection; - fingerprintDebugInfoUrl?: Maybe; + fingerprint?: Maybe; firstBuildCreatedAt?: Maybe; id: Scalars['ID']['output']; updatedAt: Scalars['DateTime']['output']; @@ -6984,6 +7010,12 @@ export type UpdateEnvironmentVariableInput = { visibility?: InputMaybe; }; +export type UpdateFilterInput = { + fingerprintHash?: InputMaybe; + hasFingerprint?: InputMaybe; + runtimeVersion?: InputMaybe; +}; + export type UpdateGitHubBuildTriggerInput = { autoSubmit: Scalars['Boolean']['input']; buildProfile: Scalars['String']['input']; @@ -8299,6 +8331,19 @@ export type WorkflowRunsPaginatedArgs = { last?: InputMaybe; }; +export type WorkflowArtifact = { + __typename?: 'WorkflowArtifact'; + contentType?: Maybe; + createdAt: Scalars['DateTime']['output']; + downloadUrl?: Maybe; + fileSizeBytes?: Maybe; + filename: Scalars['String']['output']; + id: Scalars['ID']['output']; + jobRun: JobRun; + name: Scalars['String']['output']; + updatedAt: Scalars['DateTime']['output']; +}; + export type WorkflowJob = { __typename?: 'WorkflowJob'; createdAt: Scalars['DateTime']['output']; @@ -8499,6 +8544,7 @@ export type WorkflowRunMutationCreateWorkflowRunArgs = { export type WorkflowRunMutationRetryWorkflowRunArgs = { + fromFailedJobs?: InputMaybe; workflowRunId: Scalars['ID']['input']; }; @@ -8524,6 +8570,10 @@ export enum WorkflowRunStatus { export enum WorkflowRunTriggerEventType { Github = 'GITHUB', + GithubPullRequestOpened = 'GITHUB_PULL_REQUEST_OPENED', + GithubPullRequestReopened = 'GITHUB_PULL_REQUEST_REOPENED', + GithubPullRequestSynchronize = 'GITHUB_PULL_REQUEST_SYNCHRONIZE', + GithubPush = 'GITHUB_PUSH', Manual = 'MANUAL' } @@ -9195,7 +9245,7 @@ export type CreateFingeprintMutationVariables = Exact<{ }>; -export type CreateFingeprintMutation = { __typename?: 'RootMutation', fingerprint: { __typename?: 'FingerprintMutation', createOrGetExistingFingerprint: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null } } }; +export type CreateFingeprintMutation = { __typename?: 'RootMutation', fingerprint: { __typename?: 'FingerprintMutation', createOrGetExistingFingerprint: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } } }; export type CreateKeystoreGenerationUrlMutationVariables = Exact<{ [key: string]: never; }>; @@ -9411,7 +9461,7 @@ export type BuildsWithFingerprintByIdQueryVariables = Exact<{ }>; -export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } }; +export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } }; export type ViewBuildsOnAppQueryVariables = Exact<{ appId: Scalars['String']['input']; @@ -9499,6 +9549,18 @@ export type EnvironmentVariablesSharedWithSensitiveQueryVariables = Exact<{ export type EnvironmentVariablesSharedWithSensitiveQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentVariablesIncludingSensitive: Array<{ __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType, valueWithFileContent?: string | null }> } } } }; +export type FingerprintsByAppIdQueryVariables = Exact<{ + appId: Scalars['String']['input']; + after?: InputMaybe; + first?: InputMaybe; + before?: InputMaybe; + last?: InputMaybe; + fingerprintFilter?: InputMaybe; +}>; + + +export type FingerprintsByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, fingerprintsPaginated: { __typename?: 'AppFingerprintsConnection', edges: Array<{ __typename?: 'AppFingerprintEdge', node: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } } } }; + export type GoogleServiceAccountKeyByIdQueryVariables = Exact<{ ascApiKeyId: Scalars['ID']['input']; }>; @@ -9614,7 +9676,7 @@ export type BuildFragment = { __typename?: 'Build', id: string, status: BuildSta export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }; -export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }; +export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }; export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }; @@ -9622,7 +9684,7 @@ export type EnvironmentVariableFragment = { __typename?: 'EnvironmentVariable', export type EnvironmentVariableWithSecretFragment = { __typename?: 'EnvironmentVariableWithSecret', id: string, name: string, value?: string | null, environments?: Array | null, createdAt: any, updatedAt: any, scope: EnvironmentVariableScope, visibility: EnvironmentVariableVisibility, type: EnvironmentSecretType }; -export type FingerprintFragment = { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null }; +export type FingerprintFragment = { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } }; export type RuntimeFragment = { __typename?: 'Runtime', id: string, version: string }; diff --git a/packages/eas-cli/src/graphql/queries/FingerprintQuery.ts b/packages/eas-cli/src/graphql/queries/FingerprintQuery.ts new file mode 100644 index 0000000000..3ef8f5804d --- /dev/null +++ b/packages/eas-cli/src/graphql/queries/FingerprintQuery.ts @@ -0,0 +1,98 @@ +import { print } from 'graphql'; +import gql from 'graphql-tag'; + +import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; +import { withErrorHandlingAsync } from '../client'; +import { + FingerprintFilterInput, + FingerprintFragment, + FingerprintsByAppIdQuery, +} from '../generated'; +import { FingerprintFragmentNode } from '../types/Fingerprint'; + +export const FingerprintQuery = { + async byHashAsync( + graphqlClient: ExpoGraphqlClient, + { + appId, + hash, + }: { + appId: string; + hash: string; + } + ): Promise { + const fingerprintConnection = await FingerprintQuery.getFingerprintsAsync(graphqlClient, { + appId, + fingerprintFilter: { hashes: [hash] }, + first: 1, + }); + const fingerprints = fingerprintConnection.edges.map(edge => edge.node); + return fingerprints[0] ?? null; + }, + async getFingerprintsAsync( + graphqlClient: ExpoGraphqlClient, + { + appId, + first, + after, + last, + before, + fingerprintFilter, + }: { + appId: string; + first?: number; + after?: string; + last?: number; + before?: string; + fingerprintFilter?: FingerprintFilterInput; + } + ): Promise { + const data = await withErrorHandlingAsync( + graphqlClient + .query( + gql` + query FingerprintsByAppId( + $appId: String! + $after: String + $first: Int + $before: String + $last: Int + $fingerprintFilter: FingerprintFilterInput + ) { + app { + byId(appId: $appId) { + id + fingerprintsPaginated( + after: $after + first: $first + before: $before + last: $last + filter: $fingerprintFilter + ) { + edges { + node { + id + ...FingerprintFragment + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + } + } + ${print(FingerprintFragmentNode)} + `, + { appId, after, first, before, last, fingerprintFilter }, + { additionalTypenames: ['Fingerprint'] } + ) + .toPromise() + ); + + return data.app?.byId.fingerprintsPaginated; + }, +}; diff --git a/packages/eas-cli/src/graphql/types/Fingerprint.ts b/packages/eas-cli/src/graphql/types/Fingerprint.ts index ff797f7c1b..222d9d09b8 100644 --- a/packages/eas-cli/src/graphql/types/Fingerprint.ts +++ b/packages/eas-cli/src/graphql/types/Fingerprint.ts @@ -5,5 +5,23 @@ export const FingerprintFragmentNode = gql` id hash debugInfoUrl + builds(first: 1) { + edges { + node { + id + ... on Build { + platform + } + } + } + } + updates(first: 1) { + edges { + node { + id + platform + } + } + } } `; diff --git a/packages/eas-cli/src/utils/fingerprintCli.ts b/packages/eas-cli/src/utils/fingerprintCli.ts index 651b26bce2..2546bd8aa3 100644 --- a/packages/eas-cli/src/utils/fingerprintCli.ts +++ b/packages/eas-cli/src/utils/fingerprintCli.ts @@ -7,11 +7,12 @@ import Log from '../log'; import { ora } from '../ora'; export type FingerprintOptions = { - workflow: Workflow; + workflow?: Workflow; platforms: string[]; debug?: boolean; env: Env | undefined; cwd?: string; + ignorePaths?: string[]; }; export function diffFingerprint( @@ -85,12 +86,20 @@ async function createFingerprintWithoutLoggingAsync( > { const Fingerprint = require(fingerprintPath); const fingerprintOptions: Record = {}; + const ignorePaths = []; + if (options.workflow === Workflow.MANAGED) { + ignorePaths.push('android/**/*'); + ignorePaths.push('ios/**/*'); + } + if (options.ignorePaths) { + ignorePaths.push(...options.ignorePaths); + } + if (ignorePaths.length > 0) { + fingerprintOptions.ignorePaths = ignorePaths; + } if (options.platforms) { fingerprintOptions.platforms = [...options.platforms]; } - if (options.workflow === Workflow.MANAGED) { - fingerprintOptions.ignorePaths = ['android/**/*', 'ios/**/*']; - } if (options.debug) { fingerprintOptions.debug = true; }