Skip to content

Commit 85f3df1

Browse files
authored
Merge branch 'main' into gar/refactor/mul-1692
2 parents c988de6 + fe0c0ac commit 85f3df1

270 files changed

Lines changed: 12294 additions & 5682 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.

.github/CONTRIBUTING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ When you're done with your project / bugfix / feature and ready to submit a PR,
2323
- [ ] **Get the PR reviewed by code owners**: At least two code owner approvals are mandatory before merging any PR.
2424
- [ ] **Ensure the PR is correctly labeled.**: More detail about labels definitions can be found [here](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md).
2525

26+
### Shadow CI jobs
27+
28+
CI jobs prefixed with `[shadow]` (e.g., from `ci-namespace-shadow.yml`) are **advisory only** and never gate merge. They run the same test suite on Namespace runners for performance benchmarking. If a shadow job fails, it does not indicate a problem with your PR -- it reflects the state of the Namespace runner migration trial.
29+
2630
And that's it! Thanks for helping out.

.github/actions/smart-e2e-selection/action.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ inputs:
2727
required: false
2828
default: 'false'
2929
base-ref:
30-
description: 'PR base branch ref passed to the AI analysis script for diff comparison. When release/*, AI selection is skipped and the full E2E suite is selected.'
30+
description: 'PR base branch ref passed to the AI analysis script for diff comparison. When release/* or stable, AI selection is skipped and the full E2E suite is selected.'
3131
required: false
3232
default: ''
3333
outputs:
@@ -57,15 +57,15 @@ runs:
5757
echo "⏭️ SKIP=true due to 'skip-smart-e2e-selection' label on PR"
5858
fi
5959
60-
- name: Check release target branch (full E2E, no AI selection)
60+
- name: Check release or stable target branch (full E2E, no AI selection)
6161
id: check-release-target
6262
shell: bash
6363
run: |
6464
echo "SKIP=false" >> "$GITHUB_OUTPUT"
6565
BASE='${{ inputs.base-ref }}'
66-
if [[ -n "$BASE" && "$BASE" == release/* ]]; then
66+
if [[ -n "$BASE" && ( "$BASE" == release/* || "$BASE" == "stable" ) ]]; then
6767
echo "SKIP=true" >> "$GITHUB_OUTPUT"
68-
echo "⏭️ Base branch is release/* — skipping AI E2E selection; full E2E suite will run"
68+
echo "⏭️ Base branch is release/* or stable — skipping AI E2E selection; full E2E suite will run"
6969
fi
7070
7171
- name: Checkout for PR analysis
@@ -142,9 +142,9 @@ runs:
142142
echo "SKIP_REASON=skip-smart-e2e-selection label found" >> "$GITHUB_OUTPUT"
143143
echo "ai_confidence=100" >> "$GITHUB_OUTPUT"
144144
elif [[ "${{ steps.check-release-target.outputs.SKIP }}" == "true" ]]; then
145-
echo "⏭️ Skipping AI analysis - PR targets a release branch (release/*)"
145+
echo "⏭️ Skipping AI analysis - PR targets a release or stable branch"
146146
echo "SKIPPED=true" >> "$GITHUB_OUTPUT"
147-
echo "SKIP_REASON=PR targets a release branch (release/*)" >> "$GITHUB_OUTPUT"
147+
echo "SKIP_REASON=PR targets a release or stable branch (release/* or stable)" >> "$GITHUB_OUTPUT"
148148
echo "ai_confidence=100" >> "$GITHUB_OUTPUT"
149149
else
150150
echo "✅ Running AI analysis for PR #$PR_NUMBER"

.github/guidelines/E2E_DECISION_TREE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Flakiness detection is applied to modified E2E test files in PRs:
5656

5757
## Release branches
5858

59-
PRs to release branches (cherry-picked from main) are exempt from the following:
59+
PRs to release branches (cherry-picks from main to release/\* branches and PRs to stable branch) are exempt from the following:
6060

6161
- Label `pr-not-ready-for-e2e` is not applied
6262
- Smart AI E2E selection is skipped - all E2E suites are run (if changes are not ignorable-only, e.g. only docs)

.github/scripts/collect-qa-stats.mjs

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
*
2323
* MetaMetrics (top-level `metametrics` namespace): static scan of
2424
* `tests/helpers/analytics/expectations/*.ts` plus `LEGACY_INLINE_METAMETRICS_PATHS`
25-
* for specs not yet using declarative expectations.
25+
* for specs not yet using declarative expectations. Event names are picked from
26+
* `name:` fields, `eventNames: [...]`, `onboardingEvents.*`, and event-ish `const` arrays.
2627
*
2728
* Example output:
2829
* {
@@ -348,7 +349,63 @@ function parseConstStringLiterals(source) {
348349
}
349350

350351
/**
351-
* Event names from declarative `*.analytics.ts` modules (onboarding refs, `name:` entries, event arrays).
352+
* Content between "[" and matching "]" at the same nesting depth (naive bracket count).
353+
*
354+
* @param {string} source
355+
* @param {number} openBracketIdx index of '[' opening the array
356+
* @returns {string|null}
357+
*/
358+
function sliceBalancedSquareBracketInner(source, openBracketIdx) {
359+
if (source[openBracketIdx] !== '[') return null;
360+
let depth = 0;
361+
for (let i = openBracketIdx; i < source.length; i += 1) {
362+
const c = source[i];
363+
if (c === '[') depth += 1;
364+
else if (c === ']') {
365+
depth -= 1;
366+
if (depth === 0) return source.slice(openBracketIdx + 1, i);
367+
}
368+
}
369+
return null;
370+
}
371+
372+
/**
373+
* Segment from CSV inside `eventNames:` or event-ish `const` arrays. Spread/rest is skipped —
374+
* duplicated by a sibling `const *Names*` list when present (e.g. `...transactionEventNames`).
375+
*
376+
* @param {string} token
377+
* @param {Record<string, string>} onboardingMap
378+
* @param {Record<string, string>} strConsts
379+
* @returns {string|null}
380+
*/
381+
function resolveDeclarativeExpectationListToken(token, onboardingMap, strConsts) {
382+
const t = token.replace(/^\s+|\s+$/g, '');
383+
if (!t) return null;
384+
const lit = t.match(/^['"]([^'"]+)['"]$/);
385+
if (lit) return lit[1];
386+
const onb = t.match(/^onboardingEvents\.(\w+)$/);
387+
if (onb && onboardingMap[onb[1]]) return onboardingMap[onb[1]];
388+
if (/^\.\.\.\s*\w+$/.test(t)) return null;
389+
if (strConsts[t]) return strConsts[t];
390+
return null;
391+
}
392+
393+
/**
394+
* @param {string} inner
395+
* @param {Record<string, string>} onboardingMap
396+
* @param {Record<string, string>} strConsts
397+
* @param {Set<string>} out
398+
*/
399+
function collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out) {
400+
for (const part of inner.split(',')) {
401+
const v = resolveDeclarativeExpectationListToken(part, onboardingMap, strConsts);
402+
if (v) out.add(v);
403+
}
404+
}
405+
406+
/**
407+
* Event names from declarative `*.analytics.ts`: `eventNames:` arrays, onboarding refs,
408+
* `name:` entries, string/const lookups, and event-ish `const [...]` declarations.
352409
*
353410
* @param {string} source
354411
* @param {Record<string, string>} onboardingMap
@@ -368,11 +425,18 @@ function collectFromDeclarativeExpectationsSource(source, onboardingMap, out) {
368425
const v = onboardingMap[m[1]];
369426
if (v) out.add(v);
370427
}
371-
for (const m of source.matchAll(/\bname:\s*(\w+)\s*,/g)) {
428+
// Allow `name: IDENT,` (more properties follow) or `name: IDENT }` (single-field expectation object).
429+
for (const m of source.matchAll(/\bname:\s*(\w+)\s*[},]/g)) {
372430
const v = strConsts[m[1]];
373431
if (v) out.add(v);
374432
}
375433

434+
for (const em of source.matchAll(/\beventNames:\s*\[/g)) {
435+
const openIdx = em.index + em[0].length - 1;
436+
const inner = sliceBalancedSquareBracketInner(source, openIdx);
437+
if (inner) collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out);
438+
}
439+
376440
const reArrays = /\bconst\s+(\w+)\s*=\s*\[([\s\S]*?)\];/g;
377441
let am;
378442
while ((am = reArrays.exec(source)) !== null) {
@@ -382,21 +446,7 @@ function collectFromDeclarativeExpectationsSource(source, onboardingMap, out) {
382446
/(?:event|Event|Expected|expectation|analytics|Names)/.test(varName) ||
383447
/\bonboardingEvents\b|\bexpectedEvents\b/.test(inner);
384448
if (!looksLikeEventList) continue;
385-
for (const part of inner.split(',')) {
386-
const t = part.replace(/^\s+|\s+$/g, '');
387-
if (!t) continue;
388-
const lit = t.match(/^['"]([^'"]+)['"]$/);
389-
if (lit) {
390-
out.add(lit[1]);
391-
continue;
392-
}
393-
const onb = t.match(/^onboardingEvents\.(\w+)$/);
394-
if (onb && onboardingMap[onb[1]]) {
395-
out.add(onboardingMap[onb[1]]);
396-
continue;
397-
}
398-
if (strConsts[t]) out.add(strConsts[t]);
399-
}
449+
collectExpectationCsvArrayInner(inner, onboardingMap, strConsts, out);
400450
}
401451
}
402452

.github/workflows/auto-label-not-ready-for-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
types: [opened]
88
branches-ignore:
99
- 'release/**'
10+
- 'stable'
1011

1112
jobs:
1213
add-label:

.github/workflows/build-android-e2e.yml

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
CACHE_GENERATION: v1 # Increment this to bust the cache (v1, v2, v3, etc.)
4646
YARN_ENABLE_GLOBAL_CACHE: 'true' # Enable Yarn global cache for faster installs
4747
outputs:
48-
apk-uploaded: ${{ steps.upload-apk.outcome == 'success' }}
48+
apk-uploaded: ${{ steps.upload-apk-namespace.outcome == 'success' || steps.upload-apk.outcome == 'success' }}
4949
apk-target-path: ${{ steps.determine-target-paths.outputs.apk-target-path }}
5050
test-apk-target-path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }}
5151
artifact_name: ${{ steps.determine-target-paths.outputs.artifact_name }}
@@ -452,17 +452,39 @@ jobs:
452452
GOOGLE_SERVICES_B64_ANDROID: ${{ secrets.GOOGLE_SERVICES_B64_ANDROID }}
453453
MM_INFURA_PROJECT_ID: ${{ secrets.MM_INFURA_PROJECT_ID }}
454454

455-
- name: Upload Android APK
455+
- name: Upload Android APK (Namespace)
456+
id: upload-apk-namespace
457+
if: ${{ inputs.runner_provider == 'namespace' }}
458+
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
459+
with:
460+
name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release.apk
461+
path: ${{ steps.determine-target-paths.outputs.apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.apk
462+
retention-days: 7
463+
if-no-files-found: error
464+
465+
- name: Upload Android APK (current)
456466
id: upload-apk
467+
if: ${{ inputs.runner_provider != 'namespace' }}
457468
uses: actions/upload-artifact@v4
458469
with:
459470
name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release.apk
460471
path: ${{ steps.determine-target-paths.outputs.apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}.apk
461472
retention-days: 7
462473
if-no-files-found: error
463474

464-
- name: Upload Android Test APK
475+
- name: Upload Android Test APK (Namespace)
476+
id: upload-test-apk-namespace
477+
if: ${{ inputs.runner_provider == 'namespace' }}
478+
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
479+
with:
480+
name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release-androidTest.apk
481+
path: ${{ steps.determine-target-paths.outputs.test-apk-target-path }}/${{ steps.determine-target-paths.outputs.artifact_name }}-androidTest.apk
482+
retention-days: 7
483+
if-no-files-found: error
484+
485+
- name: Upload Android Test APK (current)
465486
id: upload-test-apk
487+
if: ${{ inputs.runner_provider != 'namespace' }}
466488
uses: actions/upload-artifact@v4
467489
with:
468490
name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-release-androidTest.apk

.github/workflows/build-ios-e2e.yml

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ jobs:
3838
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ios-build' || (startsWith(github.base_ref, 'release/') && fromJSON('["ghcr.io/cirruslabs/macos-runner:tahoe-xl"]') || fromJSON('["ghcr.io/cirruslabs/macos-runner:tahoe-xl", "low-priority"]')) }}
3939
outputs:
4040
artifacts-url: ${{ steps.set-artifacts-url.outputs.artifacts-url }}
41-
app-uploaded: ${{ steps.upload-app.outcome == 'success' }}
42-
sourcemap-uploaded: ${{ steps.upload-sourcemap.outcome == 'success' }}
41+
app-uploaded: ${{ steps.upload-app-namespace.outcome == 'success' || steps.upload-app.outcome == 'success' }}
42+
sourcemap-uploaded: ${{ steps.upload-sourcemap-namespace.outcome == 'success' || steps.upload-sourcemap.outcome == 'success' }}
4343
env:
4444
XCODE_CACHE_VERSION: 1
4545
RCT_NO_LAUNCH_PACKAGER: 1
@@ -112,7 +112,8 @@ jobs:
112112

113113
- name: Find reusable build from prior run
114114
id: find-reusable-build
115-
if: ${{ steps.force-builds.outputs.force != 'true' && inputs.source-fingerprint != '' }}
115+
# Match Android: skip cross-run GitHub artifact reuse on Namespace until backend parity is proven.
116+
if: ${{ inputs.runner_provider != 'namespace' && steps.force-builds.outputs.force != 'true' && inputs.source-fingerprint != '' }}
116117
uses: ./.github/actions/find-reusable-build
117118
with:
118119
fingerprint: ${{ inputs.source-fingerprint }}
@@ -385,8 +386,20 @@ jobs:
385386
shell: bash
386387

387388
# Upload the iOS .app file that works in simulators
388-
- name: Upload iOS APP Artifact (Simulator)
389+
- name: Upload iOS APP Artifact (Simulator) (Namespace)
390+
id: upload-app-namespace
391+
if: ${{ inputs.runner_provider == 'namespace' }}
392+
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
393+
with:
394+
name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-MetaMask.app
395+
path: ios/build/Build/Products/Release-iphonesimulator/MetaMask.app
396+
retention-days: 7
397+
if-no-files-found: error
398+
continue-on-error: true
399+
400+
- name: Upload iOS APP Artifact (Simulator) (current)
389401
id: upload-app
402+
if: ${{ inputs.runner_provider != 'namespace' }}
390403
uses: actions/upload-artifact@v4
391404
with:
392405
name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-MetaMask.app
@@ -399,8 +412,20 @@ jobs:
399412
# Both paths produce it: `yarn build:ios:main:e2e` via
400413
# `scripts/ios/bundle-js-and-sentry-upload.sh` and `yarn build:repack:ios`
401414
# via `scripts/repack.js` both write to `sourcemaps/ios/index.js.map`.
402-
- name: Upload iOS Source Map
415+
- name: Upload iOS Source Map (Namespace)
416+
id: upload-sourcemap-namespace
417+
if: ${{ inputs.runner_provider == 'namespace' }}
418+
uses: namespace-actions/upload-artifact@f6ccaacc655aec41b93af180d1d7eef21af862d2 # v1.0.3
419+
with:
420+
name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-index.js.map
421+
path: sourcemaps/ios/index.js.map
422+
retention-days: 7
423+
if-no-files-found: error
424+
continue-on-error: true
425+
426+
- name: Upload iOS Source Map (current)
403427
id: upload-sourcemap
428+
if: ${{ inputs.runner_provider != 'namespace' }}
404429
uses: actions/upload-artifact@v4
405430
with:
406431
name: ${{ inputs.build_type }}-${{ inputs.metamask_environment }}-index.js.map
@@ -418,8 +443,8 @@ jobs:
418443
echo "📦 Artifacts available at: ${ARTIFACTS_URL}"
419444
echo ""
420445
echo "Upload Status Summary:"
421-
echo "- APP (Simulator): ${{ steps.upload-app.outcome }}"
422-
echo "- Source Map: ${{ steps.upload-sourcemap.outcome }}"
446+
echo "- APP (Simulator): namespace=${{ steps.upload-app-namespace.outcome }} current=${{ steps.upload-app.outcome }}"
447+
echo "- Source Map: namespace=${{ steps.upload-sourcemap-namespace.outcome }} current=${{ steps.upload-sourcemap.outcome }}"
423448
424449
env:
425450
GITHUB_REPOSITORY: '${{ github.repository }}'
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI (Namespace shadow)
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
paths-ignore:
7+
- 'docs/**'
8+
- '**/*.md'
9+
- '.github/CODEOWNERS'
10+
push:
11+
branches: [main]
12+
schedule:
13+
- cron: '0 * * * *'
14+
workflow_dispatch:
15+
16+
concurrency:
17+
group: ns-shadow-${{ github.workflow }}-${{ github.ref }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
shadow-ci:
22+
name: '[shadow] CI'
23+
uses: ./.github/workflows/ci.yml
24+
with:
25+
runner_provider: namespace
26+
secrets: inherit

0 commit comments

Comments
 (0)