diff --git a/src/__tests__/Search/fetchRevisionFromLando.test.tsx b/src/__tests__/Search/fetchRevisionFromLando.test.tsx
index 1a2b24d7d..e100e97c4 100644
--- a/src/__tests__/Search/fetchRevisionFromLando.test.tsx
+++ b/src/__tests__/Search/fetchRevisionFromLando.test.tsx
@@ -1,6 +1,7 @@
import fetchMock from '@fetch-mock/jest';
import App, { router } from '../../components/App';
+import { Strings } from '../../resources/Strings';
import { render } from '../utils/test-utils';
describe('Lando to commit validating', () => {
@@ -74,7 +75,7 @@ describe('Lando to commit validating', () => {
(console.error as jest.Mock).mockClear();
});
- it('should reject', async () => {
+ it('should explain when Lando is still creating the try push', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
fetchMock.get(
'glob:https://api.lando.services.mozilla.com/*',
@@ -83,6 +84,118 @@ describe('Lando to commit validating', () => {
? {
commit_id: null,
id: 108,
+ status: 'SUBMITTED',
+ }
+ : {
+ commit_id: '6331cb86f104e2587160208d8e47d8bef8b38ffc',
+ id: 96,
+ status: 'LANDED',
+ };
+ },
+ );
+ await router.navigate(
+ '/compare-lando-results?baseLando=123&baseRepo=try&newLando=456&newRepo=try&framework=1',
+ );
+ render();
+ expect(console.error).toHaveBeenCalledWith(
+ new Error(Strings.errors.lando.pending('123', 'SUBMITTED')),
+ );
+ expect(console.error).toHaveBeenCalledTimes(1);
+ (console.error as jest.Mock).mockClear();
+ });
+
+ it('should explain when Lando failed to create the try push', async () => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ fetchMock.get(
+ 'glob:https://api.lando.services.mozilla.com/*',
+ ({ url }) => {
+ return url.includes('123')
+ ? {
+ commit_id: '096aa2c25fb2f031021de8c58baf9c46c052ab2e',
+ id: 108,
+ status: 'LANDED',
+ }
+ : {
+ commit_id: null,
+ error: 'Tree is closed',
+ id: 96,
+ status: 'FAILED',
+ };
+ },
+ );
+ await router.navigate(
+ '/compare-lando-results?baseLando=123&baseRepo=try&newLando=456&newRepo=try&framework=1',
+ );
+ render();
+ expect(console.error).toHaveBeenCalledWith(
+ new Error(Strings.errors.lando.failed('456', 'FAILED', 'Tree is closed')),
+ );
+ expect(console.error).toHaveBeenCalledTimes(1);
+ (console.error as jest.Mock).mockClear();
+ });
+
+ it('should explain a failed Lando try push that returned an empty commit_id', async () => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ fetchMock.get('glob:https://lando.moz.tools/*', ({ url }) => {
+ return url.includes('91073')
+ ? {
+ commit_id: '',
+ error:
+ 'Unexpected error while pushing to try.\nhg error in cmd: hg push -r tip ssh://hg.mozilla.org/try -f: pushing to ssh://hg.mozilla.org/try\n\nremote: Connection closed by 63.245.208.203 port 22\nabort: no suitable response from remote hg',
+ id: 91073,
+ status: 'FAILED',
+ }
+ : {
+ commit_id: '8920f830aab97b1912099621e73bd4cd1ee5fa23',
+ error: '',
+ id: 91086,
+ status: 'LANDED',
+ };
+ });
+ await router.navigate(
+ '/compare-lando-results?landoInstance=lando-prod-2025&baseLando=91073&newLando=91086&baseRepo=try&newRepo=try&framework=13',
+ );
+ render();
+ expect(console.error).toHaveBeenCalledWith(
+ new Error(
+ Strings.errors.lando.failed(
+ '91073',
+ 'FAILED',
+ 'Unexpected error while pushing to try.',
+ ),
+ ),
+ );
+ expect(console.error).toHaveBeenCalledTimes(1);
+ (console.error as jest.Mock).mockClear();
+ });
+
+ it('should explain when Lando landed but still has no revision', async () => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ fetchMock.get('glob:https://api.lando.services.mozilla.com/*', {
+ commit_id: null,
+ id: 108,
+ status: 'LANDED',
+ });
+ await router.navigate(
+ '/compare-lando-results?baseLando=123&baseRepo=try&newLando=456&newRepo=try&framework=1',
+ );
+ render();
+ expect(console.error).toHaveBeenCalledWith(
+ new Error(Strings.errors.lando.landedWithoutRevision('123', 'LANDED')),
+ );
+ expect(console.error).toHaveBeenCalledTimes(1);
+ (console.error as jest.Mock).mockClear();
+ });
+
+ it('should explain when Treeherder has not ingested the try push yet', async () => {
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ fetchMock.get(
+ 'glob:https://api.lando.services.mozilla.com/*',
+ ({ url }) => {
+ return url.includes('123')
+ ? {
+ commit_id: '096aa2c25fb2f031021de8c58baf9c46c052ab2e',
+ id: 108,
status: 'LANDED',
}
: {
@@ -92,12 +205,20 @@ describe('Lando to commit validating', () => {
};
},
);
+ fetchMock.get('glob:https://treeherder.mozilla.org/api/project/*/push/*', {
+ results: [],
+ });
await router.navigate(
'/compare-lando-results?baseLando=123&baseRepo=try&newLando=456&newRepo=try&framework=1',
);
render();
expect(console.error).toHaveBeenCalledWith(
- new Error('The parameter baseRev is missing.'),
+ new Error(
+ Strings.errors.lando.notInTreeherder(
+ '123',
+ '096aa2c25fb2f031021de8c58baf9c46c052ab2e',
+ ),
+ ),
);
expect(console.error).toHaveBeenCalledTimes(1);
(console.error as jest.Mock).mockClear();
diff --git a/src/components/CompareResults/landoToCommitLoader.ts b/src/components/CompareResults/landoToCommitLoader.ts
index 3dd29f463..b4bfbee0d 100644
--- a/src/components/CompareResults/landoToCommitLoader.ts
+++ b/src/components/CompareResults/landoToCommitLoader.ts
@@ -1,6 +1,8 @@
import { checkValues, getComparisonInformation } from './loader';
import { compareView } from '../../common/constants';
import { fetchRevisionFromLandoId, LandoInstance } from '../../logic/lando';
+import { fetchRecentRevisions } from '../../logic/treeherder';
+import { Strings } from '../../resources/Strings';
import {
Changeset,
CombinedResultsItemType,
@@ -8,6 +10,24 @@ import {
} from '../../types/state';
import { Framework, TestVersion } from '../../types/types';
+async function ensureTryPushExists({
+ landoId,
+ commitId,
+ repo,
+}: {
+ landoId: string;
+ commitId: string;
+ repo: Repository['name'];
+}) {
+ const pushes = await fetchRecentRevisions({
+ repository: repo,
+ hash: commitId,
+ });
+ if (!pushes.length) {
+ throw new Error(Strings.errors.lando.notInTreeherder(landoId, commitId));
+ }
+}
+
// This function is responsible for fetching the data from the URL. It's called
// by React Router DOM when the compare-lando-results route is requested.
// This loader is used by ./mach try perf, and due to recent changes in
@@ -63,6 +83,16 @@ export async function loader({ request }: { request: Request }) {
replicates: replicatesFromUrl,
testVersion: testVersionFromUrl,
});
+ await ensureTryPushExists({
+ landoId: baseLandoIDFromUrl,
+ commitId: baseRev,
+ repo: baseRepo,
+ });
+ await ensureTryPushExists({
+ landoId: newLandoIDFromUrl,
+ commitId: newRevs[0],
+ repo: newRepos[0],
+ });
return await getComparisonInformation(
baseRev,
baseRepo,
diff --git a/src/logic/lando.ts b/src/logic/lando.ts
index 8c50617bf..83df3fdb6 100644
--- a/src/logic/lando.ts
+++ b/src/logic/lando.ts
@@ -1,3 +1,4 @@
+import { Strings } from '../resources/Strings';
import { LandoToCommit } from '../types/state';
const landoInstances = {
@@ -9,6 +10,54 @@ const landoInstances = {
export type LandoInstance = keyof typeof landoInstances;
+export type LandoRevision = LandoToCommit & { commit_id: string };
+
+const PENDING_STATUSES = new Set([
+ 'submitted',
+ 'in_progress',
+ 'deferred',
+ 'created',
+ 'unknown',
+]);
+
+const FAILED_STATUSES = new Set(['failed', 'aborted', 'cancelled', 'canceled']);
+
+function hasCommitId(job: LandoToCommit): job is LandoRevision {
+ // New Lando returns "" rather than null when a job has not produced a revision.
+ return typeof job.commit_id === 'string' && job.commit_id.trim().length > 0;
+}
+
+function firstLine(text: string | undefined): string | undefined {
+ if (!text) {
+ return undefined;
+ }
+ for (const line of text.split(/\r?\n/)) {
+ const trimmed = line.trim();
+ if (trimmed) {
+ return trimmed;
+ }
+ }
+ return undefined;
+}
+
+export function messageForMissingLandoRevision(
+ landoId: string,
+ job: LandoToCommit,
+) {
+ const status = job.status?.trim() ? job.status : 'unknown';
+ const normalizedStatus = status.toLowerCase();
+
+ if (FAILED_STATUSES.has(normalizedStatus)) {
+ return Strings.errors.lando.failed(landoId, status, firstLine(job.error));
+ }
+
+ if (PENDING_STATUSES.has(normalizedStatus)) {
+ return Strings.errors.lando.pending(landoId, status);
+ }
+
+ return Strings.errors.lando.landedWithoutRevision(landoId, status);
+}
+
async function fetchFromLando(url: string) {
const response = await fetch(url);
if (!response.ok) {
@@ -20,11 +69,17 @@ async function fetchFromLando(url: string) {
}
export async function fetchRevisionFromLandoId(
- landoid: string,
+ landoId: string,
instance: LandoInstance = 'lando-prod',
-) {
+): Promise {
const host = landoInstances[instance] ?? landoInstances['lando-prod'];
- const url = `https://${host}/landing_jobs/${landoid}`;
+ const url = `https://${host}/landing_jobs/${landoId}`;
const response = await fetchFromLando(url);
- return response.json() as Promise;
+ const job = (await response.json()) as LandoToCommit;
+
+ if (!hasCommitId(job)) {
+ throw new Error(messageForMissingLandoRevision(landoId, job));
+ }
+
+ return job;
}
diff --git a/src/resources/Strings.tsx b/src/resources/Strings.tsx
index 171e628af..8b8eff6e4 100644
--- a/src/resources/Strings.tsx
+++ b/src/resources/Strings.tsx
@@ -184,5 +184,17 @@ export const Strings = {
},
errors: {
warningText: 'The search input must be at least three characters.',
+ lando: {
+ pending: (landoId: string, status: string) =>
+ `Lando has not finished creating the try push for job ${landoId} yet (status: ${status}). Please wait a few moments for the push to complete and then refresh the page.`,
+ failed: (landoId: string, status: string, detail?: string) =>
+ detail
+ ? `Lando could not create the try push for job ${landoId} (status: ${status}): ${detail}`
+ : `Lando could not create the try push for job ${landoId} (status: ${status}).`,
+ landedWithoutRevision: (landoId: string, status: string) =>
+ `Lando reports job ${landoId} as ${status}, but no Treeherder revision is available yet. Please wait a few moments and then refresh the page.`,
+ notInTreeherder: (landoId: string, revision: string) =>
+ `Lando job ${landoId} has revision ${revision}, but Treeherder does not have this try push yet. If the push exists, it will appear in a few minutes once Treeherder has processed it. If this Lando ID is from lando.moz.tools, add landoInstance=lando-prod-2025 to the URL.`,
+ },
},
};
diff --git a/src/types/state.ts b/src/types/state.ts
index fd0b467b0..9486de2d5 100644
--- a/src/types/state.ts
+++ b/src/types/state.ts
@@ -222,9 +222,10 @@ export type HashToCommit = {
};
export type LandoToCommit = {
- commit_id: string;
- id: string;
+ commit_id: string | null;
+ id: string | number;
status: string;
+ error?: string;
};
export type InputType = 'base' | 'new';