Skip to content

Commit a486614

Browse files
committed
Merge origin/main into stable-main-7.82.0
2 parents 86745bf + bd42563 commit a486614

1,606 files changed

Lines changed: 92506 additions & 29184 deletions

File tree

Some content is hidden

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

.eslintrc.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ module.exports = {
168168
files: ['tests/**/*.{js,ts}'],
169169
extends: ['./tests/framework/.eslintrc.js'],
170170
},
171+
{
172+
// These files intentionally omit dependencies from a useEffect/useCallback
173+
// (documented inline at each call site). The React Compiler refuses to
174+
// optimize any file containing an inline `eslint-disable` for a React
175+
// rule, so the suppression is relocated here (invisible to the compiler)
176+
// instead of being a per-line comment. Runtime behavior is unchanged.
177+
files: [
178+
'app/components/hooks/useAsyncResult.ts',
179+
'app/components/hooks/useOTAUpdates.ts',
180+
'app/components/Nav/Main/index.js',
181+
'app/components/Nav/App/App.tsx',
182+
],
183+
rules: {
184+
'react-hooks/exhaustive-deps': 'off',
185+
},
186+
},
171187
{
172188
files: ['*.{ts,tsx}'],
173189
extends: ['@metamask/eslint-config-typescript'],

.github/CODEOWNERS

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ app/core/Analytics/MetaMetricsPrivacySegmentPlugin.ts @MetaMask/mobile-pla
1919
app/core/Analytics/MetaMetricsPrivacySegmentPlugin.test.ts @MetaMask/mobile-platform
2020
app/core/Analytics/MetaMetricsTestUtils.test.ts @MetaMask/mobile-platform
2121
app/core/Analytics/MetaMetricsTestUtils.ts @MetaMask/mobile-platform
22-
app/core/Analytics/MetricsEventBuilder.test.ts @MetaMask/mobile-platform
23-
app/core/Analytics/MetricsEventBuilder.ts @MetaMask/mobile-platform
2422
app/util/metrics/ @MetaMask/mobile-platform
2523
app/components/hooks/useMetrics/ @MetaMask/mobile-platform
2624
app/selectors/featureFlagController/* @MetaMask/mobile-platform
@@ -42,6 +40,14 @@ app/core/Engine/README.md @MetaMask/mobile-pla
4240
app/core/Engine/types.ts @MetaMask/mobile-platform
4341
app/core/Engine/controllers/remote-feature-flag-controller/ @MetaMask/mobile-platform
4442
app/core/DeeplinkManager @MetaMask/mobile-platform
43+
# SDK / WalletConnect / BackgroundBridge
44+
app/actions/sdk/ @MetaMask/mobile-platform
45+
app/reducers/sdk/ @MetaMask/mobile-platform
46+
app/core/SDKConnect/ @MetaMask/mobile-platform
47+
app/core/SDKConnectV2/ @MetaMask/mobile-platform
48+
app/core/WalletConnect/ @MetaMask/mobile-platform
49+
app/util/walletconnect.js @MetaMask/mobile-platform
50+
app/core/BackgroundBridge/ @MetaMask/mobile-platform
4551
# Deprecated BN.js helpers. Gated to discourage adding new exports; consumers
4652
# should migrate to app/util/number/bigint. See .eslintrc.js for the import
4753
# fence and bigint-migration-reference.test.ts for migration patterns.
@@ -109,18 +115,9 @@ app/util/transaction-controller/ @MetaMask/confirm
109115
app/util/transactions/ @MetaMask/confirmations
110116

111117
# Wallet integrations Team
112-
app/actions/sdk/ @MetaMask/wallet-integrations
113118
app/components/Approvals/WalletConnectApproval/ @MetaMask/wallet-integrations
114119
app/components/Views/SDK/ @MetaMask/wallet-integrations
115120
app/components/Views/WalletConnectSessions/ @MetaMask/wallet-integrations
116-
app/core/BackgroundBridge/ @MetaMask/wallet-integrations
117-
app/core/RPCMethods/ @MetaMask/wallet-integrations
118-
app/core/SDKConnect/ @MetaMask/wallet-integrations
119-
app/core/SDKConnectV2/ @MetaMask/wallet-integrations
120-
app/core/WalletConnect/ @MetaMask/wallet-integrations
121-
app/reducers/sdk/ @MetaMask/wallet-integrations
122-
app/util/permissions/ @MetaMask/wallet-integrations
123-
app/util/walletconnect.js @MetaMask/wallet-integrations
124121

125122
# Accounts Team
126123
app/core/Encryptor/ @MetaMask/accounts-engineers
@@ -174,6 +171,9 @@ patches/react-native+0.*.patch @MetaMask/supply-chain
174171
# Core Platform Team
175172
**/snaps/** @MetaMask/core-platform
176173
**/Snaps/** @MetaMask/core-platform
174+
# RPC methods / permissions
175+
app/core/RPCMethods/ @MetaMask/core-platform
176+
app/util/permissions/ @MetaMask/core-platform
177177

178178
# Co-owned by Confirmations team and Core Platform team
179179
app/components/UI/TemplateRenderer @MetaMask/confirmations @MetaMask/core-platform
@@ -288,6 +288,7 @@ app/components/Views/Homepage/Sections/NFTs/ @MetaMask/metamask-assets
288288

289289
# UX Team
290290
app/components/Views/AccountActions @MetaMask/mobile-core-ux
291+
app/components/Views/AccountPermissions @MetaMask/mobile-core-ux
291292
app/components/Views/AccountSelector @MetaMask/mobile-core-ux
292293
app/components/Views/AddressQRCode @MetaMask/mobile-core-ux
293294
app/components/Views/EditAccountName @MetaMask/mobile-core-ux
@@ -377,7 +378,6 @@ tests/websocket/ @MetaMask/qa
377378
.github/workflows/run-appium-e2e-workflow.yml @MetaMask/qa
378379
.github/workflows/run-appium-smoke-tests-android.yml @MetaMask/qa
379380
.github/workflows/run-appium-smoke-tests-ios.yml @MetaMask/qa
380-
.github/workflows/prebuild-wda-ios.yml @MetaMask/qa
381381
.github/workflows/run-e2e-workflow.yml @MetaMask/qa
382382
.github/workflows/run-e2e-api-specs.yml @MetaMask/qa
383383
.github/workflows/run-e2e-regression-tests-android.yml @MetaMask/qa

.github/actions/setup-ci-js-deps/action.yml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,10 @@ runs:
2525
node_modules
2626
.yarn/cache
2727
28-
- uses: actions/setup-node@v6
29-
with:
30-
node-version-file: '.nvmrc'
31-
cache: ${{ inputs.runner_provider != 'namespace' && 'yarn' || '' }}
32-
33-
# Namespace: always run install so the shared volume stays in sync with yarn.lock.
34-
# Non-Namespace: skip install when node_modules was extracted from a same-run
35-
# artifact; run install when starting from a clean workspace.
28+
# Run before setup-node so yarn cache is only enabled when this job installs deps.
29+
# When node_modules comes from the ci-js-deps artifact, skipping cache avoids a
30+
# post-job "Path Validation Error" from actions/setup-node trying to save a
31+
# cache path that was never populated.
3632
- name: Determine if install is needed
3733
id: check-deps
3834
shell: bash
@@ -45,6 +41,14 @@ runs:
4541
echo "needs-install=true" >> "$GITHUB_OUTPUT"
4642
fi
4743
44+
- uses: actions/setup-node@v6
45+
with:
46+
node-version-file: '.nvmrc'
47+
cache: ${{ inputs.runner_provider != 'namespace' && steps.check-deps.outputs.needs-install == 'true' && 'yarn' || '' }}
48+
49+
# Namespace: always run install so the shared volume stays in sync with yarn.lock.
50+
# Non-Namespace: skip install when node_modules was extracted from a same-run
51+
# artifact; run install when starting from a clean workspace.
4852
- name: Install Yarn dependencies with retry
4953
if: ${{ steps.check-deps.outputs.needs-install == 'true' }}
5054
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Fetches E2E test timing data and writes it to a file so it can be uploaded
5+
* as a run-scoped artifact and reused by every shard (including re-runs).
6+
*
7+
* Timing source priority:
8+
* 1. qa-stats artifact from the merge-base commit of the PR branch and main
9+
* (most accurate: not affected by test renames/additions on main after branching)
10+
* 2. qa-stats artifact from the latest successful main run (current fallback)
11+
* 3. null → callers fall back to alphabetical equal-count split
12+
*
13+
* Outputs:
14+
* - Writes e2e-timings.json to OUTPUT_PATH (default: ./e2e-timings.json)
15+
* - Sets GITHUB_OUTPUT available=true|false
16+
*/
17+
18+
import fs from 'node:fs';
19+
import os from 'node:os';
20+
import path from 'node:path';
21+
import { spawnSync } from 'node:child_process';
22+
23+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
24+
const REPOSITORY = process.env.REPOSITORY || 'MetaMask/metamask-mobile';
25+
const PR_NUMBER = process.env.PR_NUMBER || '';
26+
const OUTPUT_PATH = process.env.OUTPUT_PATH || './e2e-timings.json';
27+
28+
const QA_STATS_WORKFLOW_FILE = 'qa-stats.yml';
29+
const QA_STATS_ARTIFACT_NAME = 'qa-stats';
30+
const QA_STATS_JSON_FILENAME = 'qa-stats.json';
31+
32+
const [OWNER, REPO] = REPOSITORY.split('/');
33+
const API_BASE = `https://api.github.com/repos/${REPOSITORY}`;
34+
35+
/**
36+
* Minimal GitHub REST helper.
37+
* @param {string} url
38+
* @returns {Promise<any>}
39+
*/
40+
async function githubRest(url) {
41+
const res = await fetch(url, {
42+
headers: {
43+
Authorization: `Bearer ${GITHUB_TOKEN}`,
44+
Accept: 'application/vnd.github+json',
45+
'X-GitHub-Api-Version': '2022-11-28',
46+
'User-Agent': 'metamask-mobile-ci',
47+
},
48+
});
49+
if (!res.ok) {
50+
const body = await res.text().catch(() => '');
51+
const suffix = body ? `: ${body.slice(0, 200)}` : '';
52+
throw new Error(`GET ${url}${res.status} ${res.statusText}${suffix}`);
53+
}
54+
return res.json();
55+
}
56+
57+
/**
58+
* Download and extract qa-stats.json from a GitHub Actions run artifact.
59+
* Returns the e2e_test_times map, or null on any failure.
60+
* @param {number} runId
61+
* @returns {Promise<object|null>}
62+
*/
63+
async function extractTimingsFromRun(runId) {
64+
const artifactsData = await githubRest(`${API_BASE}/actions/runs/${runId}/artifacts`);
65+
const artifact = (artifactsData?.artifacts || []).find(
66+
(a) => a?.name === QA_STATS_ARTIFACT_NAME && !a?.expired,
67+
);
68+
if (!artifact?.archive_download_url) {
69+
return null;
70+
}
71+
72+
// GitHub redirects to a pre-signed storage URL. Follow manually so the
73+
// Authorization header is not forwarded (storage rejects it).
74+
const redirectRes = await fetch(artifact.archive_download_url, {
75+
headers: {
76+
Authorization: `Bearer ${GITHUB_TOKEN}`,
77+
Accept: 'application/vnd.github+json',
78+
'X-GitHub-Api-Version': '2022-11-28',
79+
'User-Agent': 'metamask-mobile-ci',
80+
},
81+
redirect: 'manual',
82+
});
83+
const downloadUrl = redirectRes.headers.get('location');
84+
if (!downloadUrl) {
85+
throw new Error(`no redirect URL for artifact on run #${runId} (status ${redirectRes.status})`);
86+
}
87+
88+
const zipRes = await fetch(downloadUrl);
89+
if (!zipRes.ok) {
90+
throw new Error(`download zip → ${zipRes.status} ${zipRes.statusText}`);
91+
}
92+
93+
// Reject unexpectedly large responses before writing to disk.
94+
const MAX_ZIP_BYTES = 50 * 1024 * 1024; // 50 MB
95+
const contentLength = Number(zipRes.headers.get('content-length') ?? 0);
96+
if (contentLength > MAX_ZIP_BYTES) {
97+
throw new Error(`artifact zip too large (${contentLength} bytes) — aborting`);
98+
}
99+
100+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), `qa-stats-${runId}-`));
101+
const zipPath = path.join(tmpRoot, 'qa-stats.zip');
102+
fs.writeFileSync(zipPath, Buffer.from(await zipRes.arrayBuffer()));
103+
104+
const unzipResult = spawnSync('unzip', ['-o', zipPath, '-d', tmpRoot], { stdio: 'pipe' });
105+
if (unzipResult.status !== 0) {
106+
throw new Error(`unzip failed (code ${unzipResult.status}): ${unzipResult.stderr?.toString() || ''}`);
107+
}
108+
109+
const jsonPath = path.join(tmpRoot, QA_STATS_JSON_FILENAME);
110+
if (!fs.existsSync(jsonPath)) return null;
111+
112+
const parsed = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
113+
const times = parsed?.e2e_test_times;
114+
if (!times || typeof times !== 'object' || Object.keys(times).length === 0) return null;
115+
116+
return times;
117+
}
118+
119+
/**
120+
* Resolve the merge-base SHA for the current PR branch against main using
121+
* the GitHub compare API. Returns null when unavailable.
122+
* @returns {Promise<string|null>}
123+
*/
124+
async function getMergeBaseSha() {
125+
if (!PR_NUMBER) return null;
126+
try {
127+
const pr = await githubRest(`${API_BASE}/pulls/${PR_NUMBER}`);
128+
const headSha = pr?.head?.sha;
129+
if (!headSha) return null;
130+
131+
const compare = await githubRest(
132+
`${API_BASE}/compare/main...${headSha}`,
133+
);
134+
return compare?.merge_base_commit?.sha || null;
135+
} catch (e) {
136+
console.log(`ℹ️ Could not resolve merge-base SHA: ${e?.message || e}`);
137+
return null;
138+
}
139+
}
140+
141+
/**
142+
* Fetch timings from the qa-stats artifact produced for a specific commit SHA.
143+
* Returns null if no matching run or artifact is found.
144+
* @param {string} sha
145+
* @returns {Promise<object|null>}
146+
*/
147+
async function fetchTimingsForSha(sha) {
148+
const runsData = await githubRest(
149+
`${API_BASE}/actions/workflows/${QA_STATS_WORKFLOW_FILE}/runs?head_sha=${sha}&status=success&per_page=1`,
150+
);
151+
const run = runsData?.workflow_runs?.[0];
152+
if (!run?.id) return null;
153+
154+
console.log(`📥 Fetching qa-stats artifact from merge-base run #${run.id} (sha: ${sha.slice(0, 8)})`);
155+
return extractTimingsFromRun(run.id);
156+
}
157+
158+
/**
159+
* Fetch timings from the latest successful qa-stats run on main.
160+
* @returns {Promise<object|null>}
161+
*/
162+
async function fetchLatestMainTimings() {
163+
const runsData = await githubRest(
164+
`${API_BASE}/actions/workflows/${QA_STATS_WORKFLOW_FILE}/runs?branch=main&status=success&per_page=1`,
165+
);
166+
const run = runsData?.workflow_runs?.[0];
167+
if (!run?.id) return null;
168+
169+
console.log(`📥 Fetching qa-stats artifact from latest main run #${run.id}`);
170+
return extractTimingsFromRun(run.id);
171+
}
172+
173+
/**
174+
* Write a value to GITHUB_OUTPUT if available.
175+
* @param {string} name
176+
* @param {string} value
177+
*/
178+
function setOutput(name, value) {
179+
const outputFile = process.env.GITHUB_OUTPUT;
180+
if (outputFile) {
181+
fs.appendFileSync(outputFile, `${name}=${value}\n`);
182+
}
183+
}
184+
185+
async function main() {
186+
if (!GITHUB_TOKEN) {
187+
console.log('ℹ️ No GITHUB_TOKEN — skipping timings fetch');
188+
setOutput('available', 'false');
189+
return;
190+
}
191+
192+
let times = null;
193+
194+
// 1. Try merge-base timings (most stable and accurate for PRs)
195+
try {
196+
const mergeBaseSha = await getMergeBaseSha();
197+
if (mergeBaseSha) {
198+
console.log(`🔍 Resolved merge-base SHA: ${mergeBaseSha.slice(0, 8)}`);
199+
times = await fetchTimingsForSha(mergeBaseSha);
200+
if (times) {
201+
console.log(`✅ Using merge-base timings (${Object.keys(times).length} entries)`);
202+
} else {
203+
console.log('ℹ️ No qa-stats artifact found for merge-base — trying latest main');
204+
}
205+
} else {
206+
console.log('ℹ️ No merge-base SHA (non-PR run) — trying latest main');
207+
}
208+
} catch (e) {
209+
console.log(`ℹ️ Merge-base timings fetch failed (${e?.message || e}) — trying latest main`);
210+
}
211+
212+
// 2. Fall back to latest successful main run
213+
if (!times) {
214+
try {
215+
times = await fetchLatestMainTimings();
216+
if (times) {
217+
console.log(`✅ Using latest-main timings (${Object.keys(times).length} entries)`);
218+
} else {
219+
console.log('ℹ️ No qa-stats artifact on latest main run');
220+
}
221+
} catch (e) {
222+
console.log(`ℹ️ Latest-main timings fetch failed: ${e?.message || e}`);
223+
}
224+
}
225+
226+
if (!times) {
227+
console.log('ℹ️ No timings available — shards will use alphabetical equal-count split');
228+
setOutput('available', 'false');
229+
return;
230+
}
231+
232+
const outputDir = path.dirname(path.resolve(OUTPUT_PATH));
233+
fs.mkdirSync(outputDir, { recursive: true });
234+
fs.writeFileSync(OUTPUT_PATH, JSON.stringify({ e2e_test_times: times }, null, 2));
235+
console.log(`💾 Wrote timings to ${OUTPUT_PATH}`);
236+
setOutput('available', 'true');
237+
}
238+
239+
main().catch((err) => {
240+
console.error('❌ Unexpected error:', err);
241+
setOutput('available', 'false');
242+
// Exit 0 — timings are best-effort; failure here must never block E2E jobs.
243+
process.exit(0);
244+
});

0 commit comments

Comments
 (0)