Skip to content

Commit 7e5bde2

Browse files
committed
chore: merge main, address conflicts
2 parents a378893 + a23ce45 commit 7e5bde2

534 files changed

Lines changed: 32521 additions & 5539 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.

.depcheckrc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ ignores:
140140
- 'react-native-worklets'
141141
# Activated via /** @jest-environment jsdom */ docblock pragma in test files, not imported directly
142142
- 'jest-environment-jsdom'
143+
# Used as the Jest `transform` in jest.config.js, but referenced via the
144+
# array form (['babel-jest', { configFile: ... }]) which depcheck's Jest
145+
# special-parser doesn't recognize (it only detects the plain string form).
146+
- 'babel-jest'
143147

144148
# Used in Yarn plugin for preview builds
145149
- '@yarnpkg/core'

.github/CODEOWNERS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ app/util/confirmation/ @MetaMask/confir
109109
app/util/transaction-controller/ @MetaMask/confirmations
110110
app/util/transactions/ @MetaMask/confirmations
111111

112+
# Product Safety Team
113+
**/product-safety/** @MetaMask/product-safety
114+
112115
# Wallet integrations Team
113116
app/components/Approvals/WalletConnectApproval/ @MetaMask/wallet-integrations
114117
app/components/Views/SDK/ @MetaMask/wallet-integrations

.github/actions/find-reusable-build/action.yml

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ inputs:
5555
until a run-specific status match is found.
5656
required: false
5757
default: '30'
58+
main-branch-only:
59+
description: >-
60+
When true, only search the base branch (typically `main`) for reusable
61+
builds. Used for PRs with test-only changes that should not compile fresh
62+
native artifacts.
63+
required: false
64+
default: 'false'
5865

5966
outputs:
6067
found:
@@ -86,6 +93,7 @@ runs:
8693
STATUS_CONTEXT: ${{ inputs.status-context }}
8794
MAX_CANDIDATES: ${{ inputs.max-candidates-per-branch }}
8895
MAX_CANDIDATES_CROSS_PR: ${{ inputs.max-candidates-cross-pr }}
96+
MAIN_BRANCH_ONLY: ${{ inputs.main-branch-only }}
8997
HEAD_BRANCH: ${{ github.head_ref || github.ref_name }}
9098
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
9199
CURRENT_RUN_ID: ${{ github.run_id }}
@@ -101,6 +109,7 @@ runs:
101109
STATUS_CONTEXT,
102110
MAX_CANDIDATES,
103111
MAX_CANDIDATES_CROSS_PR,
112+
MAIN_BRANCH_ONLY,
104113
HEAD_BRANCH,
105114
HEAD_SHA,
106115
CURRENT_RUN_ID,
@@ -155,25 +164,32 @@ runs:
155164
// lookups can never discover another PR's run
156165
// (GitHub filters `branch` against head_branch,
157166
// which is the PR source branch).
158-
const tiers = [
159-
{
167+
const tiers = [];
168+
const mainBranchOnly = MAIN_BRANCH_ONLY === 'true';
169+
170+
if (!mainBranchOnly) {
171+
tiers.push({
160172
label: `same-branch (branch=${HEAD_BRANCH})`,
161173
params: { branch: HEAD_BRANCH, per_page: maxCandidates },
162-
},
163-
];
164-
if (BASE_BRANCH && BASE_BRANCH !== HEAD_BRANCH) {
174+
});
175+
}
176+
177+
if (BASE_BRANCH && (mainBranchOnly || BASE_BRANCH !== HEAD_BRANCH)) {
165178
tiers.push({
166179
label: `base-branch (branch=${BASE_BRANCH})`,
167180
params: { branch: BASE_BRANCH, per_page: maxCandidates },
168181
});
169182
}
170-
tiers.push({
171-
label: `cross-pr (event=pull_request, any branch, last ${maxCandidatesCrossPr} runs)`,
172-
params: { event: 'pull_request', per_page: maxCandidatesCrossPr },
173-
// Skip runs already visited by the same-branch tier to avoid
174-
// wasting API calls on duplicates.
175-
skipHeadBranch: HEAD_BRANCH,
176-
});
183+
184+
if (!mainBranchOnly) {
185+
tiers.push({
186+
label: `cross-pr (event=pull_request, any branch, last ${maxCandidatesCrossPr} runs)`,
187+
params: { event: 'pull_request', per_page: maxCandidatesCrossPr },
188+
// Skip runs already visited by the same-branch tier to avoid
189+
// wasting API calls on duplicates.
190+
skipHeadBranch: HEAD_BRANCH,
191+
});
192+
}
177193
178194
async function hasRunFingerprintStatus(sha, runId) {
179195
try {

.github/guidelines/E2E_DECISION_TREE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ flowchart TD
1212
L2 -->|ignorable-only changes| NoBlock[No merge block]
1313
L2 -->|non-ignorable changes| Skip2[⛔️ Merge blocked]
1414
GR -->|PR ignorable-only changes| Ignorable[No E2E]
15+
GR -->|PR test-only changes| TestOnly[E2E + Smart selection, reuse main builds]
1516
GR -->|PR has Android-only changes| Android[Android Build + Tests needed]
1617
GR -->|PR has iOS-only changes| iOS[iOS Build + Test needed]
1718
GR -->|PR other files changed| Both[Both Build + Tests needed]
@@ -25,6 +26,18 @@ flowchart TD
2526
CONF -->|no| AllTagsFallback[Run all E2E needed]
2627
```
2728

29+
## Test-only PR changes
30+
31+
When a PR only changes E2E/performance test files (and other ignorable files), CI still runs Smart E2E Selection and the selected E2E/performance suites, but **does not compile fresh iOS/Android native builds**. Instead, it reuses the latest matching artifacts from `main`.
32+
33+
The native build fingerprint for test-only PRs is computed from **`main` HEAD** (not the PR merge tree) so the lookup key matches completed `ci.yml` runs on `main`. Reuse tries GitHub Actions artifacts first, then the Cirrus `main` APK cache on Android.
34+
35+
If `main` has new native-changing commits but its CI build has not finished yet, reuse lookup may miss — CI logs a warning and **falls back to a fresh native build** instead of failing the workflow. Performance E2E on test-only PRs resolves BrowserStack apps by `custom_id` prefix `MetaMask-Android-*-main-*` so only uploads from `main` are reused (not other branches or PRs).
36+
37+
This applies when all changed files match `e2e_test_files` or `e2e_ignorable` filters in `.github/rules/filter-rules.yml`, with at least one E2E test file changed, and no E2E-relevant workflow files were modified.
38+
39+
Use the `force-builds` label or `[force-builds]` commit tag to override reuse and compile fresh builds — including on test-only PRs that would otherwise require main-branch artifacts.
40+
2841
## E2E tests skipped by default on new PRs
2942

3043
To save infra resources while waiting for static analysis findings and potential fixes/iterations:

.github/rules/filter-rules.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,34 @@ config_files: &config_files
4545
ci_files: &ci_files
4646
- '.github/**'
4747

48+
# E2E / PERFORMANCE TEST FILES — do not require a fresh native build, but still
49+
# run E2E + Smart E2E selection when these are the only non-ignorable changes.
50+
e2e_test_files: &e2e_test_files
51+
- 'tests/smoke/**'
52+
- 'tests/regression/**'
53+
- 'tests/performance/**'
54+
- 'tests/smoke-appium/**'
55+
- 'tests/flows/**'
56+
- 'tests/page-objects/**'
57+
- 'tests/selectors/**'
58+
- 'tests/locators/**'
59+
- 'tests/framework/**'
60+
- 'tests/api-mocking/**'
61+
- 'tests/docs/**'
62+
- 'tests/scripts/**'
63+
- 'tests/reporters/**'
64+
- 'tests/tools/e2e-ai-analyzer/**'
65+
- 'tests/playwright*.config.ts'
66+
- 'tests/playwright.*.config.ts'
67+
- 'tests/init.detox.js'
68+
- 'tests/environment.detox.js'
69+
- 'tests/jest.e2e.detox.config.js'
70+
- 'tests/helpers.js'
71+
- 'tests/tags.js'
72+
- 'tests/tags.performance.js'
73+
- 'tests/teams-config.js'
74+
- 'wdio/**'
75+
4876
# ALL IGNORABLE FILES - safe to skip E2E
4977
e2e_ignorable:
5078
- *documentation_files
@@ -54,6 +82,16 @@ e2e_ignorable:
5482
- *config_files
5583
- *ci_files
5684

85+
# Union of ignorable + E2E test files for test-only change detection.
86+
e2e_test_or_ignorable:
87+
- *documentation_files
88+
- *asset_files
89+
- *low_level_test_files
90+
- *locale_translation_files
91+
- *config_files
92+
- *ci_files
93+
- *e2e_test_files
94+
5795
e2e_relevant_workflows:
5896
- '.github/workflows/ci.yml'
5997
- '.github/workflows/get-requirements.yml'
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Validates BrowserStack API fields before they are written to GitHub Actions
3+
* outputs or other local files.
4+
*/
5+
6+
/** BrowserStack app URLs use the bs:// scheme with an opaque app hash. */
7+
const BROWSERSTACK_APP_URL_PATTERN = /^bs:\/\/[A-Za-z0-9]+$/;
8+
9+
const WITH_SRP_CUSTOM_ID_PATTERN =
10+
/^MetaMask-Android-With-SRP-[A-Za-z0-9._-]+-\d+$/;
11+
const WITHOUT_SRP_CUSTOM_ID_PATTERN =
12+
/^MetaMask-Android-Without-SRP-[A-Za-z0-9._-]+-\d+$/;
13+
14+
const MAIN_BRANCH_BROWSERSTACK_SLUG = 'main';
15+
const MAIN_WITH_SRP_CUSTOM_ID_PREFIX = `MetaMask-Android-With-SRP-${MAIN_BRANCH_BROWSERSTACK_SLUG}-`;
16+
const MAIN_WITHOUT_SRP_CUSTOM_ID_PREFIX = `MetaMask-Android-Without-SRP-${MAIN_BRANCH_BROWSERSTACK_SLUG}-`;
17+
18+
/**
19+
* @param {unknown} value
20+
* @param {RegExp} pattern
21+
* @param {string} label
22+
* @returns {string}
23+
*/
24+
function assertMatchesPattern(value, pattern, label) {
25+
if (typeof value !== 'string' || !pattern.test(value)) {
26+
throw new Error(`Invalid ${label}`);
27+
}
28+
return value;
29+
}
30+
31+
/**
32+
* @param {unknown} value
33+
* @param {string} label
34+
* @returns {string}
35+
*/
36+
function assertBrowserStackAppUrl(value, label) {
37+
return assertMatchesPattern(value, BROWSERSTACK_APP_URL_PATTERN, label);
38+
}
39+
40+
/**
41+
* @param {unknown} value
42+
* @param {'with-srp' | 'without-srp'} kind
43+
* @returns {string}
44+
*/
45+
function assertBrowserStackCustomId(value, kind) {
46+
const pattern =
47+
kind === 'with-srp'
48+
? WITH_SRP_CUSTOM_ID_PATTERN
49+
: WITHOUT_SRP_CUSTOM_ID_PATTERN;
50+
return assertMatchesPattern(value, pattern, `${kind} custom_id`);
51+
}
52+
53+
/**
54+
* @param {string} path
55+
* @param {Record<string, string>} outputs
56+
*/
57+
function writeGithubOutputs(path, outputs) {
58+
const lines = [];
59+
for (const [key, value] of Object.entries(outputs)) {
60+
if (value.includes('\n') || value.includes('\r')) {
61+
throw new Error(`Refusing to write multiline GitHub output for ${key}`);
62+
}
63+
lines.push(`${key}=${value}`);
64+
}
65+
require('node:fs').appendFileSync(path, `${lines.join('\n')}\n`);
66+
}
67+
68+
module.exports = {
69+
BROWSERSTACK_APP_URL_PATTERN,
70+
WITH_SRP_CUSTOM_ID_PATTERN,
71+
WITHOUT_SRP_CUSTOM_ID_PATTERN,
72+
MAIN_BRANCH_BROWSERSTACK_SLUG,
73+
MAIN_WITH_SRP_CUSTOM_ID_PREFIX,
74+
MAIN_WITHOUT_SRP_CUSTOM_ID_PREFIX,
75+
assertBrowserStackAppUrl,
76+
assertBrowserStackCustomId,
77+
writeGithubOutputs,
78+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
5+
const {
6+
assertBrowserStackAppUrl,
7+
assertBrowserStackCustomId,
8+
writeGithubOutputs,
9+
} = require('./browserstack-app-validation.cjs');
10+
11+
describe('browserstack-app-validation', () => {
12+
it('accepts valid BrowserStack app URLs', () => {
13+
expect(assertBrowserStackAppUrl('bs://abc123DEF', 'test')).toBe(
14+
'bs://abc123DEF',
15+
);
16+
});
17+
18+
it('rejects malformed BrowserStack app URLs', () => {
19+
expect(() =>
20+
assertBrowserStackAppUrl('https://evil.example/app', 'test'),
21+
).toThrow('Invalid test');
22+
expect(() =>
23+
assertBrowserStackAppUrl('bs://bad chars', 'test'),
24+
).toThrow('Invalid test');
25+
});
26+
27+
it('accepts expected custom_id formats with branch slug', () => {
28+
expect(
29+
assertBrowserStackCustomId(
30+
'MetaMask-Android-With-SRP-main-123',
31+
'with-srp',
32+
),
33+
).toBe('MetaMask-Android-With-SRP-main-123');
34+
expect(
35+
assertBrowserStackCustomId(
36+
'MetaMask-Android-Without-SRP-main-456',
37+
'without-srp',
38+
),
39+
).toBe('MetaMask-Android-Without-SRP-main-456');
40+
expect(
41+
assertBrowserStackCustomId(
42+
'MetaMask-Android-With-SRP-feature_x-789',
43+
'with-srp',
44+
),
45+
).toBe('MetaMask-Android-With-SRP-feature_x-789');
46+
});
47+
48+
it('rejects legacy custom_id formats without branch slug', () => {
49+
expect(() =>
50+
assertBrowserStackCustomId('MetaMask-Android-With-SRP-123', 'with-srp'),
51+
).toThrow('Invalid with-srp custom_id');
52+
});
53+
54+
it('writes only validated single-line GitHub outputs', () => {
55+
const outputPath = path.join(os.tmpdir(), `gh-output-${Date.now()}.txt`);
56+
writeGithubOutputs(outputPath, {
57+
found: 'true',
58+
'with-srp-browserstack-url': 'bs://abc123',
59+
});
60+
61+
expect(fs.readFileSync(outputPath, 'utf8')).toBe(
62+
'found=true\nwith-srp-browserstack-url=bs://abc123\n',
63+
);
64+
fs.unlinkSync(outputPath);
65+
});
66+
});

.github/scripts/check-template-and-add-labels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
upsertStickyComment,
2929
} from './shared/pr-template-comment';
3030

31-
const knownBots = ["metamaskbot", "metamaskbotv2", "dependabot", "github-actions", "sentry-io", "devin-ai-integration", "runway-github" , "cursor", "mm-token-exchange-service"];
31+
const knownBots = ["metamaskbot", "metamaskbotv2", "dependabot", "github-actions", "sentry-io", "devin-ai-integration", "runway-github" , "cursor", "metamask-ci"];
3232

3333
// GitHub App / bot logins that cannot be resolved as User in GraphQL (user(login:) returns null).
3434
// Issues/PRs from these actors still get full template and label checks; we only skip the org check.

0 commit comments

Comments
 (0)