Skip to content

Commit e7ebdc1

Browse files
authored
release: 7.69.0 (#27086)
# 🚀 v7.69.0 Testing & Release Quality Process Hi Team, As part of our new **MetaMask Release Quality Process**, here’s a quick overview of the key processes, testing strategies, and milestones to ensure a smooth and high-quality deployment. --- ## 📋 Key Processes ### Testing Strategy - **Developer Teams:** Conduct regression and exploratory testing for your functional areas, including automated and manual tests for critical workflows. - **QA Team:** Focus on exploratory testing across the wallet, prioritize high-impact areas, and triage any Sentry errors found during testing. - **Customer Success Team:** Validate new functionalities and provide feedback to support release monitoring. ### GitHub Signoff - Each team must **sign off on the Release Candidate (RC)** via GitHub by the end of the validation timeline (**Tuesday EOD PT**). - Ensure all tests outlined in the Testing Plan are executed, and any identified issues are addressed. ### Issue Resolution - **Resolve all Release Blockers** (Sev0 and Sev1) by **Tuesday EOD PT**. - For unresolved blockers, PRs may be reverted, or feature flags disabled to maintain release quality and timelines. ### Cherry-Picking Criteria - Only **critical fixes** meeting outlined criteria will be cherry-picked. - Developers must ensure these fixes are thoroughly reviewed, tested, and merged by **Tuesday EOD PT**. --- ## 🗓️ Timeline and Milestones 1. **Today (Friday):** Begin Release Candidate validation. 2. **Tuesday EOD PT:** Finalize RC with all fixes and cherry-picks. 3. **Wednesday:** Buffer day for final checks. 4. **Thursday:** Submit release to app stores and begin rollout to 1% of users. 5. **Monday:** Scale deployment to 10%. 6. **Tuesday:** Full rollout to 100%. --- ## ✅ Signoff Checklist Each team is responsible for signing off via GitHub. Use the checkbox below to track signoff completion: # Team sign-off checklist - [x] Accounts Framework - [x] Assets - [x] Bots Team - [x] Card - [x] Confirmations - [x] Core Platform - [x] Design System - [x] Earn - [x] EVM Datalake - [x] Extension Platform - [x] Mobile Platform - [x] Mobile UX - [x] Networks - [x] Onboarding - [x] Perps - [x] Predict - [x] Ramp - [x] Rewards - [x] Social & AI - [x] Swaps and Bridge - [x] Wallet Integrations This process is a major step forward in ensuring release stability and quality. Let’s stay aligned and make this release a success! 🚀 Feel free to reach out if you have questions or need clarification. Many thanks in advance # Reference - Testing plan sheet - https://docs.google.com/spreadsheets/d/1tsoodlAlyvEUpkkcNcbZ4PM9HuC9cEM80RZeoVv5OCQ/edit?gid=404070372#gid=404070372
2 parents c97a678 + 3ecae11 commit e7ebdc1

File tree

1,522 files changed

+80419
-55044
lines changed

Some content is hidden

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

1,522 files changed

+80419
-55044
lines changed

.claude/commands/create-bug.md

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,43 @@ For `render: shell` fields, wrap the value in a code fence:
124124
- Output the issue URL so the user can view it
125125
- Ask the user: "Would you like me to investigate the codebase for the possible root cause? (yes/no)"
126126
- Default is **no** — only proceed if the user explicitly says yes
127-
- If yes, investigate the codebase (research only — no code changes) and add a comment using:
127+
- If yes, perform the full **Root Cause Analysis** (Step 6)
128+
129+
## Step 6: Root Cause Analysis
130+
131+
When the user opts in, perform all three phases below and post a single comment on the issue with the combined findings.
132+
133+
### 6a: Investigate the root cause
134+
135+
- Trace the bug through the code to identify the exact files, lines, and logic causing the issue
136+
- Research only — no code changes
137+
138+
### 6b: Identify regression PRs
139+
140+
- Check `git log` history of affected files, comparing the release branch against the previous release branch
141+
- Identify which PRs introduced or surfaced the bug (new code, removed compensating mechanisms, refactors, etc.)
142+
- Note whether the issue is a new regression or pre-existing
143+
144+
### 6c: Scope analysis
145+
146+
- Search the codebase for the same pattern, function, or anti-pattern causing the bug
147+
- Identify all affected areas beyond the originally reported bug
148+
- If other features are impacted, file separate bug issues for each and link them
149+
150+
### Comment format
151+
152+
Post findings as a comment using:
128153

129154
```bash
130155
gh issue comment <issue-number> --repo MetaMask/metamask-mobile --body "..."
131156
```
132157

133158
The comment should include:
134159

135-
- A summary of the possible root cause
136-
- The error flow with relevant file paths and line numbers
137-
- Key files table (file, line(s), description)
138-
- A suggested fix approach
160+
- **Summary** of the root cause
161+
- **Likely Regression PR(s)** — PR numbers, titles, authors, and explanation of what changed (or note if pre-existing)
162+
- **Error flow** with relevant file paths and line numbers
163+
- **Full scope of impact** — all affected files, hooks, and components beyond the reported bug
164+
- **Key files table** (file, line(s), description)
165+
- **Suggested fix approach**
166+
- **Links to related bugs** filed from the scope analysis

.cursor/worktrees.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"setup-worktree": [
3+
"cp $ROOT_WORKTREE_PATH/.js.env .js.env",
4+
"cp $ROOT_WORKTREE_PATH/.ios.env .ios.env",
5+
"cp $ROOT_WORKTREE_PATH/.android.env .android.env",
6+
"cp $ROOT_WORKTREE_PATH/.e2e.env .e2e.env",
7+
"cp -r $ROOT_WORKTREE_PATH/.cursor/plans .cursor/plans"
8+
]
9+
}

.e2e.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ export TEST_SRP_2=
2727
export TEST_SRP_3=
2828
export BROWSERSTACK_USERNAME=
2929
export BROWSERSTACK_ACCESS_KEY=
30+
31+
# Appwright performance -> Sentry instrumentation
32+
# If E2E_PERFORMANCE_SENTRY_DSN is defined, each scenario uploads timers to Sentry.
33+
export E2E_PERFORMANCE_SENTRY_DSN=
34+
# Optional. Set to false to disable upload even when DSN is present.
35+
export E2E_PERFORMANCE_SENTRY_ENABLED=true
36+
# Optional sample rate between 0 and 1 (default: 1).
37+
export E2E_PERFORMANCE_SENTRY_SAMPLE_RATE=1
38+
# Optional metadata for Sentry events.
39+
export E2E_PERFORMANCE_SENTRY_ENVIRONMENT=e2e-performance
40+
export E2E_PERFORMANCE_SENTRY_RELEASE=
41+
3042
# Set BROWSERSTACK_LOCAL=true when using run-appwright:mm-connect-android-bs-local (BrowserStack Local tunnel)
3143
# BROWSERSTACK_LOCAL=true
3244
export SEEDLESS_ONBOARDING_ENABLED=

.eslintignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
/scripts/inpage-bridge
22
/app/core/InpageBridgeWeb3.js
3+
/app/components/UI/Charts/AdvancedChart/webview/chartLogic.js
4+
/app/components/UI/Charts/AdvancedChart/webview/chartLogicString.ts
5+
/app/components/UI/Charts/AdvancedChart/webview/syncChartLogic.js
36
/app/util/blockies.js
47
__snapshots__
58
android

.eslintrc.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,29 @@ module.exports = {
105105
},
106106
},
107107
{
108+
// Temporary rollout strategy:
109+
// Keep color-no-hex disabled for all tests by default, then re-enable it
110+
// for specific folders in small PR batches. Once migration is complete,
111+
// remove this override and enforce across all tests in:
112+
// - app/components/
113+
// - app/component-library/
108114
files: ['**/*.test.{js,ts,tsx}', '**/*.stories.{js,ts,tsx}'],
109115
rules: {
110116
'@metamask/design-tokens/color-no-hex': 'off',
111117
},
112118
},
119+
{
120+
files: ['app/components/UI/Card/**/*.{js,jsx,ts,tsx}'],
121+
rules: {
122+
'@metamask/design-tokens/color-no-hex': 'error',
123+
},
124+
},
125+
{
126+
files: ['app/components/Snaps/**/*.{js,jsx,ts,tsx}'],
127+
rules: {
128+
'@metamask/design-tokens/color-no-hex': 'error',
129+
},
130+
},
113131
{
114132
files: [
115133
'app/components/UI/Name/**/*.{js,ts,tsx}',

.github/CODEOWNERS

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ app/selectors/rampsController @MetaMask/ramp
7070
**/ramps/** @MetaMask/ramp
7171

7272
# Card Team
73-
app/components/UI/Card/ @MetaMask/card
74-
app/core/redux/slices/card/ @MetaMask/card
73+
app/components/UI/Card/ @MetaMask/card
74+
app/core/redux/slices/card/ @MetaMask/card
75+
app/core/Engine/controllers/card-controller @MetaMask/card
76+
app/core/Engine/messengers/card-controller-messenger @MetaMask/card
77+
app/selectors/cardController.ts @MetaMask/card
7578

7679
# Confirmation Team
7780
app/components/Views/confirmations @MetaMask/confirmations
@@ -107,7 +110,6 @@ app/core/SnapKeyring @MetaMask/accounts-e
107110

108111
# Co-owned by accounts and mobile-core-ux
109112
app/components/Views/AccountSelector @MetaMask/accounts-engineers @MetaMask/mobile-core-ux
110-
app/components/UI/EvmAccountSelectorList @MetaMask/accounts-engineers @MetaMask/mobile-core-ux
111113

112114
# Multichain Accounts
113115
**/multichain-accounts/** @MetaMask/accounts-engineers
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
#!/usr/bin/env node
2+
/**
3+
*
4+
* Downloads pre-aggregated QA stats artifacts from the triggering CI run via the
5+
* GitHub API and writes a qa-stats.json file for consumption by downstream workflows.
6+
*
7+
* Required env vars:
8+
* GITHUB_TOKEN — GitHub Actions token for API access
9+
* WORKFLOW_RUN_ID — ID of the CI run that produced the artifacts
10+
*
11+
* Example of output format of qa-stats.json:
12+
* {
13+
* "component_view_tests_count": 34,
14+
* "unit_test_count": 679,
15+
* }
16+
*
17+
* How to add a new metric:
18+
* 1. Add a collector function below (see existing example)
19+
* 2. Call it in main() and assign the result to stats
20+
*/
21+
22+
import { readFile, writeFile, mkdir } from 'fs/promises';
23+
import { execSync } from 'child_process';
24+
import { join } from 'path';
25+
26+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
27+
const WORKFLOW_RUN_ID = process.env.WORKFLOW_RUN_ID;
28+
29+
if (!WORKFLOW_RUN_ID) throw new Error('Missing required WORKFLOW_RUN_ID env var');
30+
if (!GITHUB_TOKEN) throw new Error('Missing required GITHUB_TOKEN env var');
31+
32+
33+
// ---------------------------------------------------------------------------
34+
// GitHub artifact helpers
35+
// ---------------------------------------------------------------------------
36+
37+
let _artifactList = null;
38+
39+
/**
40+
* Fetches (and caches) the list of artifacts for the triggering CI run.
41+
* First call fetches and stores, every subsequent call returns the cached value.
42+
*
43+
* @returns {Promise<Array>}
44+
*/
45+
async function getArtifactList() {
46+
if (_artifactList) return _artifactList;
47+
48+
const artifacts = [];
49+
let page = 1;
50+
51+
while (true) {
52+
const url = `https://api.github.com/repos/MetaMask/metamask-mobile/actions/runs/${WORKFLOW_RUN_ID}/artifacts?per_page=100&page=${page}`;
53+
const res = await fetch(url, {
54+
headers: {
55+
Authorization: `Bearer ${GITHUB_TOKEN}`,
56+
Accept: 'application/vnd.github+json',
57+
},
58+
});
59+
60+
if (!res.ok) {
61+
throw new Error(`Failed to list artifacts (page ${page}): ${res.status} ${res.statusText}`);
62+
}
63+
64+
const data = await res.json();
65+
artifacts.push(...data.artifacts);
66+
67+
if (data.artifacts.length < 100) break;
68+
page++;
69+
}
70+
71+
_artifactList = artifacts;
72+
return _artifactList;
73+
}
74+
75+
/**
76+
* Downloads a named artifact from the triggering CI run, extracts it into a
77+
* local directory named after the artifact, and returns that directory path.
78+
*
79+
* @param {string} artifactName
80+
* @returns {Promise<string>} Path to the directory containing the extracted files
81+
*/
82+
async function downloadArtifact(artifactName) {
83+
const artifacts = await getArtifactList();
84+
const artifact = artifacts.find((a) => a.name === artifactName);
85+
86+
if (!artifact) {
87+
throw new Error(
88+
`Artifact "${artifactName}" not found in run ${WORKFLOW_RUN_ID}`,
89+
);
90+
}
91+
92+
// GitHub redirects to a pre-signed S3 URL. Follow manually so the
93+
// Authorization header is not forwarded to S3.
94+
const redirectRes = await fetch(artifact.archive_download_url, {
95+
headers: {
96+
Authorization: `Bearer ${GITHUB_TOKEN}`,
97+
Accept: 'application/vnd.github+json',
98+
},
99+
redirect: 'manual',
100+
});
101+
102+
const downloadUrl = redirectRes.headers.get('location');
103+
if (!downloadUrl) {
104+
throw new Error(`No redirect URL returned for artifact "${artifactName}"`);
105+
}
106+
107+
const zipRes = await fetch(downloadUrl);
108+
if (!zipRes.ok) {
109+
throw new Error(
110+
`Failed to download artifact "${artifactName}": ${zipRes.status} ${zipRes.statusText}`,
111+
);
112+
}
113+
114+
const destDir = `./${artifactName}`;
115+
await mkdir(destDir, { recursive: true });
116+
const zipPath = join(destDir, `${artifactName}.zip`);
117+
await writeFile(zipPath, Buffer.from(await zipRes.arrayBuffer()));
118+
execSync(`unzip -q "${zipPath}" -d "${destDir}"`);
119+
120+
return destDir;
121+
}
122+
123+
// ---------------------------------------------------------------------------
124+
// Collectors — one async function per metric source
125+
// ---------------------------------------------------------------------------
126+
127+
async function collectComponentViewTestCount() {
128+
const destDir = await downloadArtifact('cv-test-stats');
129+
const raw = await readFile(join(destDir, 'cv-test-stats.json'), 'utf8');
130+
const data = JSON.parse(raw);
131+
return data.component_view_test_number;
132+
}
133+
134+
async function collectUnitTestCount() {
135+
const destDir = await downloadArtifact('unit-test-stats');
136+
const raw = await readFile(join(destDir, 'unit-test-stats.json'), 'utf8');
137+
const data = JSON.parse(raw);
138+
return data.unit_test_number;
139+
}
140+
141+
// ---------------------------------------------------------------------------
142+
// Main
143+
// ---------------------------------------------------------------------------
144+
145+
async function main() {
146+
const stats = {};
147+
148+
const collectors = [
149+
{
150+
key: 'component_view_tests_count',
151+
collect: collectComponentViewTestCount,
152+
},
153+
{
154+
key: 'unit_tests_count',
155+
collect: collectUnitTestCount,
156+
},
157+
];
158+
159+
for (const { key, collect } of collectors) {
160+
try {
161+
stats[key] = await collect();
162+
} catch (err) {
163+
// stat will not be present in the output file if the collector fails
164+
console.error(`[${key}] collector failed, skipping stat:`, err.message);
165+
}
166+
}
167+
168+
const outputPath = './qa-stats.json';
169+
await writeFile(outputPath, JSON.stringify(stats, null, 2), 'utf8');
170+
console.log(`✅ QA stats written to ${outputPath}:`, stats);
171+
}
172+
173+
main().catch((err) => {
174+
console.error('\n❌ Unexpected error:', err);
175+
process.exit(1);
176+
});

0 commit comments

Comments
 (0)