Skip to content

Commit 8b7cc66

Browse files
committed
Merge remote-tracking branch 'origin/main' into first-time-install-e2e-test
* origin/main: (57 commits) fix: cp-12.17.0 Fix remove nft (#32102) chore: rename migration 154 to 152.1 (#32109) feat: bump multichain-api-client 0.2.0 (#32104) chore: upgrade assets controllers to v58.0.0 (#32019) feat: upload test runs (#32103) chore: improving disabled functionality to tabs component (#32081) ci: add a soft gate for our page load benchmarks (#31912) test: Added bridge user action benchmark (#31952) fix: chromedriver crash on Windows (#31962) fix: add retries to get-job-id (#32015) feat: Adds Remote Mode hardware wallet gating (#32012) chore: clean up tech debt for `MegaETH Testnet` integration (#31867) chore: bump `@metamask/{polling,gas-fee}-controller` to `v13`,`v23` (#32035) chore(snaps): Bump Snaps packages (#32042) feat: auto create solana account after ondboarding or srp import cp-12.17.0 (#32038) feat: add option to switch from standard account <-> smart account on accounts details modal (#31899) fix: cp-12.17.0 - Center buttons in wider popup (#31897) feat: add non EVM testnets (#31702) feat: expose Multichain API via window.postMessage for Firefox (#30142) feat: Add "Create Solana Account" in connection flow when Solana is requested chain by dapp (#31781) ...
2 parents f1249fb + e186ccc commit 8b7cc66

File tree

581 files changed

+9946
-5940
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

581 files changed

+9946
-5940
lines changed
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import * as fs from 'fs/promises';
2+
import humanizeDuration from 'humanize-duration';
3+
import path from 'path';
4+
import * as xml2js from 'xml2js';
5+
6+
const XML = {
7+
parse: new xml2js.Parser().parseStringPromise,
8+
};
9+
10+
const humanizer = humanizeDuration.humanizer({
11+
language: 'shortEn',
12+
languages: {
13+
shortEn: {
14+
y: () => 'y',
15+
mo: () => 'mo',
16+
w: () => 'w',
17+
d: () => 'd',
18+
h: () => 'h',
19+
m: () => 'm',
20+
s: () => 's',
21+
ms: () => 'ms',
22+
},
23+
},
24+
delimiter: ' ',
25+
spacer: '',
26+
round: true,
27+
});
28+
29+
function formatTime(ms: number): string {
30+
if (ms < 1000) {
31+
return `${Math.round(ms)}ms`;
32+
}
33+
return humanizer(ms);
34+
}
35+
36+
/**
37+
* Replaces HTML `<strong>` tags with ANSI escape codes to format
38+
* text as bold in the console output.
39+
*/
40+
function consoleBold(str: string): string {
41+
return str
42+
.replaceAll('<strong>', '\x1b[1m')
43+
.replaceAll('</strong>', '\x1b[0m');
44+
}
45+
46+
interface TestRun {
47+
name: string;
48+
testSuites: TestSuite[];
49+
}
50+
51+
interface TestSuite {
52+
name: string;
53+
job: {
54+
name: string;
55+
id: string;
56+
};
57+
path: string;
58+
date: Date;
59+
tests: number;
60+
passed: number;
61+
failed: number;
62+
skipped: number;
63+
time: number;
64+
testCases: TestCase[];
65+
}
66+
67+
type TestCase =
68+
| {
69+
name: string;
70+
time: number;
71+
status: 'passed';
72+
}
73+
| {
74+
name: string;
75+
time: number;
76+
status: 'failed';
77+
error: string;
78+
};
79+
80+
async function main() {
81+
const env = {
82+
OWNER: process.env.OWNER || 'metamask',
83+
REPOSITORY: process.env.REPOSITORY || 'metamask-extension',
84+
BRANCH: process.env.BRANCH || 'main',
85+
TEST_SUMMARY_PATH:
86+
process.env.TEST_SUMMARY_PATH || 'test/test-results/summary.md',
87+
TEST_RESULTS_PATH: process.env.TEST_RESULTS_PATH || 'test/test-results/e2e',
88+
TEST_RUNS_PATH:
89+
process.env.TEST_RUNS_PATH || 'test/test-results/test-runs.json',
90+
RUN_ID: process.env.RUN_ID ? +process.env.RUN_ID : 0,
91+
PR_NUMBER: process.env.PR_NUMBER ? +process.env.PR_NUMBER : 0,
92+
GITHUB_ACTIONS: process.env.GITHUB_ACTIONS === 'true',
93+
};
94+
95+
let summary = '';
96+
const core = env.GITHUB_ACTIONS
97+
? await import('@actions/core')
98+
: {
99+
summary: {
100+
addRaw: (text: string) => {
101+
summary += text;
102+
},
103+
write: async () => await fs.writeFile(env.TEST_SUMMARY_PATH, summary),
104+
},
105+
setFailed: (msg: string) => console.error(msg),
106+
};
107+
108+
try {
109+
const testRuns: TestRun[] = [];
110+
const filenames = await fs.readdir(env.TEST_RESULTS_PATH);
111+
112+
for (const filename of filenames) {
113+
const file = await fs.readFile(
114+
path.join(env.TEST_RESULTS_PATH, filename),
115+
'utf8',
116+
);
117+
const results = await XML.parse(file);
118+
for (const suite of results.testsuites.testsuite || []) {
119+
if (!suite.testcase || !suite.$.file) continue;
120+
const tests = +suite.$.tests;
121+
const failed = +suite.$.failures;
122+
const skipped = tests - suite.testcase.length;
123+
const passed = tests - failed - skipped;
124+
const testSuite: TestSuite = {
125+
name: suite.$.name,
126+
path: suite.$.file.slice(suite.$.file.indexOf(`test${path.sep}`)),
127+
job: {
128+
name: suite.properties?.[0].property?.[0]?.$.value ?? '',
129+
id: suite.properties?.[0].property?.[1]?.$.value ?? '',
130+
},
131+
date: new Date(suite.$.timestamp),
132+
tests,
133+
passed,
134+
failed,
135+
skipped,
136+
time: +suite.$.time * 1000, // convert to ms,
137+
testCases: [],
138+
};
139+
for (const test of suite.testcase || []) {
140+
const testCase: TestCase = {
141+
name: test.$.name,
142+
time: +test.$.time * 1000, // convert to ms
143+
status: test.failure ? 'failed' : 'passed',
144+
error: test.failure ? test.failure[0]._ : undefined,
145+
};
146+
testSuite.testCases.push(testCase);
147+
}
148+
const testRun: TestRun = {
149+
// regex to remove the shard number from the job name
150+
name: testSuite.job.name.replace(/\s+\(\d+\)$/, ''),
151+
testSuites: [testSuite],
152+
};
153+
const existingRun = testRuns.find((run) => run.name === testRun.name);
154+
if (existingRun) {
155+
existingRun.testSuites.push(testSuite);
156+
} else {
157+
testRuns.push(testRun);
158+
}
159+
}
160+
}
161+
162+
for (const testRun of testRuns) {
163+
const deduped: { [path: string]: TestSuite } = {};
164+
for (const suite of testRun.testSuites) {
165+
const existing = deduped[suite.path];
166+
// If there is a duplicate, we keep the suite with the latest date
167+
if (!existing || existing.date < suite.date) {
168+
deduped[suite.path] = suite;
169+
}
170+
}
171+
172+
const suites = Object.values(deduped);
173+
174+
const title = `<strong>${testRun.name}</strong>`;
175+
console.log(consoleBold(title));
176+
177+
if (suites.length) {
178+
const total = suites.reduce(
179+
(acc, suite) => ({
180+
tests: acc.tests + suite.tests,
181+
passed: acc.passed + suite.passed,
182+
failed: acc.failed + suite.failed,
183+
skipped: acc.skipped + suite.skipped,
184+
}),
185+
{ tests: 0, passed: 0, failed: 0, skipped: 0 },
186+
);
187+
188+
core.summary.addRaw(
189+
total.failed ? `\n<details open>\n` : `\n<details>\n`,
190+
);
191+
core.summary.addRaw(`\n<summary>${title}</summary>\n`);
192+
193+
const times = suites.map((suite) => {
194+
const start = suite.date.getTime();
195+
const duration = suite.time;
196+
return { start, end: start + duration };
197+
});
198+
const earliestStart = Math.min(...times.map((t) => t.start));
199+
const latestEnd = Math.max(...times.map((t) => t.end));
200+
const executionTime = latestEnd - earliestStart;
201+
202+
const conclusion = `<strong>${
203+
total.tests
204+
}</strong> tests were completed in <strong>${formatTime(
205+
executionTime,
206+
)}</strong> with <strong>${total.passed}</strong> passed, <strong>${
207+
total.failed
208+
}</strong> failed and <strong>${total.skipped}</strong> skipped.`;
209+
210+
console.log(consoleBold(conclusion));
211+
core.summary.addRaw(`\n${conclusion}\n`);
212+
213+
if (total.failed) {
214+
console.error(`\n❌ Failed tests\n`);
215+
core.summary.addRaw(`\n#### ❌ Failed tests\n`);
216+
core.summary.addRaw(
217+
`\n<hr style="height: 1px; margin-top: -5px; margin-bottom: 10px;">\n`,
218+
);
219+
for (const suite of suites) {
220+
if (!suite.failed) continue;
221+
console.error(suite.path);
222+
core.summary.addRaw(
223+
`\n#### [${suite.path}](https://github.com/${env.OWNER}/${env.REPOSITORY}/blob/${env.BRANCH}/${suite.path})\n`,
224+
);
225+
if (suite.job.name && suite.job.id && env.RUN_ID) {
226+
core.summary.addRaw(
227+
`\n##### Job: [${suite.job.name}](https://github.com/${
228+
env.OWNER
229+
}/${env.REPOSITORY}/actions/runs/${env.RUN_ID}/job/${
230+
suite.job.id
231+
}${env.PR_NUMBER ? `?pr=${env.PR_NUMBER}` : ''})\n`,
232+
);
233+
}
234+
for (const test of suite.testCases) {
235+
if (test.status !== 'failed') continue;
236+
console.error(` ${test.name}`);
237+
console.error(` ${test.error}\n`);
238+
core.summary.addRaw(`\n##### ${test.name}\n`);
239+
core.summary.addRaw(`\n\`\`\`js\n${test.error}\n\`\`\`\n`);
240+
}
241+
}
242+
}
243+
244+
const rows = suites.map((suite) => ({
245+
'Test suite': suite.path,
246+
Passed: suite.passed ? `${suite.passed} ✅` : '',
247+
Failed: suite.failed ? `${suite.failed} ❌` : '',
248+
Skipped: suite.skipped ? `${suite.skipped} ⏩` : '',
249+
Time: formatTime(suite.time),
250+
}));
251+
252+
const columns = Object.keys(rows[0]);
253+
const header = `| ${columns.join(' | ')} |`;
254+
const alignment = '| :--- | ---: | ---: | ---: | ---: |';
255+
const body = rows
256+
.map((row) => {
257+
const data = {
258+
...row,
259+
'Test suite': `[${row['Test suite']}](https://github.com/${env.OWNER}/${env.REPOSITORY}/blob/${env.BRANCH}/${row['Test suite']})`,
260+
};
261+
return `| ${Object.values(data).join(' | ')} |`;
262+
})
263+
.join('\n');
264+
const table = [header, alignment, body].join('\n');
265+
266+
console.table(rows);
267+
core.summary.addRaw(`\n${table}\n`);
268+
} else {
269+
core.summary.addRaw(`\n<details open>\n`);
270+
core.summary.addRaw(`<summary>${title}</summary>\n`);
271+
console.log('No tests found');
272+
core.summary.addRaw('No tests found');
273+
}
274+
console.log();
275+
core.summary.addRaw(`</details>\n`);
276+
}
277+
278+
await core.summary.write();
279+
await fs.writeFile(env.TEST_RUNS_PATH, JSON.stringify(testRuns, null, 2));
280+
} catch (error) {
281+
core.setFailed(`Error creating the test report: ${error}`);
282+
}
283+
}
284+
285+
main();

.github/scripts/get-job-id.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { retry } from './shared/utils';
2+
import * as core from '@actions/core';
3+
4+
async function main() {
5+
const { Octokit } = await import('octokit');
6+
7+
const env = {
8+
OWNER: process.env.OWNER || 'metamask',
9+
REPOSITORY: process.env.REPOSITORY || 'metamask-extension',
10+
RUN_ID: +process.env.RUN_ID!,
11+
JOB_NAME: process.env.JOB_NAME!,
12+
ATTEMPT_NUMBER: +process.env.ATTEMPT_NUMBER!,
13+
GITHUB_TOKEN: process.env.GITHUB_TOKEN!,
14+
GITHUB_ACTIONS: process.env.GITHUB_ACTIONS === 'true',
15+
};
16+
17+
const github = new Octokit({ auth: env.GITHUB_TOKEN });
18+
19+
const job = await retry(async () => {
20+
const jobs = github.paginate.iterator(
21+
github.rest.actions.listJobsForWorkflowRunAttempt,
22+
{
23+
owner: env.OWNER,
24+
repo: env.REPOSITORY,
25+
run_id: env.RUN_ID,
26+
attempt_number: env.ATTEMPT_NUMBER,
27+
per_page: 100,
28+
},
29+
);
30+
for await (const response of jobs) {
31+
const job = response.data.find((job) => job.name.endsWith(env.JOB_NAME));
32+
if (job) return job;
33+
}
34+
throw new Error(`Job with name '${env.JOB_NAME}' not found`);
35+
});
36+
37+
console.log(`The job id for '${env.JOB_NAME}' is '${job.id}'`);
38+
if (env.GITHUB_ACTIONS) core.setOutput('job-id', job.id);
39+
}
40+
41+
main();

.github/scripts/shared/utils.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { setTimeout } from 'node:timers/promises';
2+
13
// This helper function checks if version has the correct format: "x.y.z" where "x", "y" and "z" are numbers.
24
export function isValidVersionFormat(str: string): boolean {
35
const regex = /^\d+\.\d+\.\d+$/;
@@ -39,12 +41,31 @@ export function getCurrentDateFormatted(): string {
3941

4042
// This mapping is used to know what planning repo is used for each code repo
4143
export const codeRepoToPlanningRepo: { [key: string]: string } = {
42-
"metamask-extension": "MetaMask-planning",
43-
"metamask-mobile": "mobile-planning"
44-
}
44+
'metamask-extension': 'MetaMask-planning',
45+
'metamask-mobile': 'mobile-planning',
46+
};
4547

4648
// This mapping is used to know what platform each code repo is used for
4749
export const codeRepoToPlatform: { [key: string]: string } = {
48-
"metamask-extension": "extension",
49-
"metamask-mobile": "mobile",
50+
'metamask-extension': 'extension',
51+
'metamask-mobile': 'mobile',
52+
};
53+
54+
export async function retry<T extends (...args: any[]) => any>(
55+
fn: T,
56+
{ retries = 3, delay = 5000 } = { retries: 3, delay: 5000 },
57+
): Promise<Awaited<ReturnType<T>>> {
58+
for (let attempt = 1; attempt <= retries; attempt++) {
59+
try {
60+
return await fn();
61+
} catch (err) {
62+
if (attempt === retries) throw err;
63+
console.log(
64+
`Attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`,
65+
);
66+
await setTimeout(delay);
67+
delay *= 2;
68+
}
69+
}
70+
throw new Error('Retries exhausted');
5071
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Automated RCA
2+
3+
on:
4+
issues:
5+
types: [closed]
6+
7+
permissions:
8+
issues: write
9+
contents: read
10+
11+
jobs:
12+
automated-rca:
13+
uses: MetaMask/github-tools/.github/workflows/post-gh-rca.yml@115cc6dce7aa32c85cbd77a19e9c04db85fb7920
14+
with:
15+
google-form-base-url: 'https://docs.google.com/forms/d/e/1FAIpQLSfnfEWH7JCFFtvjCHixgyqJdTU3LW3BmTwdF1dDezTNf7m4ig/viewform?usp=pp_url&entry.340898780='
16+
repo-owner: ${{ github.repository_owner }}
17+
repo-name: ${{ github.event.repository.name }}
18+
issue-number: ${{ github.event.issue.number }}
19+
issue-labels: '["Sev0-urgent", "Sev1-high"]'

0 commit comments

Comments
 (0)