Skip to content

Commit 006412b

Browse files
authored
Merge branch 'main' into kureev/MUSD-788
2 parents 16fe4ff + d99af47 commit 006412b

228 files changed

Lines changed: 7722 additions & 3908 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.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: 'Setup CI JS dependencies'
2+
description: >
3+
Sets up node_modules and project build outputs for CI jobs.
4+
On Namespace runners, mounts the shared cache volume then runs
5+
yarn install --immutable to sync with the current yarn.lock.
6+
On non-Namespace runners, skips install when node_modules is
7+
already present from a same-run artifact; otherwise installs from scratch.
8+
9+
inputs:
10+
runner_provider:
11+
description: 'Runner provider (`namespace` or any GitHub-hosted value).'
12+
required: false
13+
default: 'current'
14+
15+
runs:
16+
using: 'composite'
17+
steps:
18+
- name: Configure Namespace cache
19+
if: ${{ inputs.runner_provider == 'namespace' }}
20+
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1
21+
with:
22+
path: |
23+
~/.cache/yarn
24+
.metamask
25+
node_modules
26+
.yarn/cache
27+
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.
36+
- name: Determine if install is needed
37+
id: check-deps
38+
shell: bash
39+
run: |
40+
if [ "${{ inputs.runner_provider }}" != "namespace" ] && \
41+
[ -d node_modules ] && \
42+
[ -f app/util/termsOfUse/termsOfUseContent.ts ]; then
43+
echo "needs-install=false" >> "$GITHUB_OUTPUT"
44+
else
45+
echo "needs-install=true" >> "$GITHUB_OUTPUT"
46+
fi
47+
48+
- name: Install Yarn dependencies with retry
49+
if: ${{ steps.check-deps.outputs.needs-install == 'true' }}
50+
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
51+
with:
52+
timeout_minutes: 10
53+
max_attempts: 3
54+
retry_wait_seconds: 30
55+
command: yarn install --immutable
56+
57+
- name: Run project setup
58+
if: ${{ steps.check-deps.outputs.needs-install == 'true' }}
59+
shell: bash
60+
run: yarn setup:github-ci --node

.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/auto-rc-ota-build-core.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,5 +142,4 @@ jobs:
142142
build_commit_sha: ${{ needs.trigger-build.outputs.built_commit_sha }}
143143
build_version: ${{ needs.trigger-build.outputs.semantic_version }}
144144
build_number: ${{ needs.trigger-build.outputs.ios_version_code }}
145-
distribute_external: false
146145
secrets: inherit

.github/workflows/build-and-upload-to-testflight.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ on:
2020
type: string
2121
default: 'MetaMask BETA & Release Candidates'
2222
distribute_external:
23-
description: 'Whether to distribute to external testers. Defaults to false; nightly-build.yml relies on the script default (true) so it always distributes externally.'
23+
description: 'Whether to distribute to external testers. (default: false)'
2424
required: false
2525
type: boolean
2626
default: false
@@ -55,7 +55,7 @@ on:
5555
- 'MM Card Team'
5656
- 'Ramp Provider Testing'
5757
distribute_external:
58-
description: 'Whether to distribute to external testers'
58+
description: 'Whether to distribute to external testers (default: false)'
5959
required: false
6060
type: boolean
6161
default: false
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
name: Build Android
2+
3+
# Creates a temp branch, bumps version (build.yml), builds the Android APK/AAB.
4+
# Mirrors build-and-upload-to-testflight.yml but Android-only and without the upload step.
5+
# APK/AAB artifacts stay attached to the build.yml run via its existing Upload Android * steps.
6+
#
7+
on:
8+
workflow_call:
9+
inputs:
10+
source_branch:
11+
description: 'Branch, tag, or SHA to build'
12+
required: true
13+
type: string
14+
environment:
15+
description: 'Build environment / track. Must be one of: exp, beta, rc (enforced by validate-inputs).'
16+
required: true
17+
type: string
18+
upload_to_sentry:
19+
description: 'If true, enable Sentry CLI upload of JS source maps and native debug symbols during the build'
20+
required: false
21+
type: boolean
22+
default: false
23+
runner_provider:
24+
description: Runner provider forwarded from the caller
25+
required: false
26+
type: string
27+
default: current
28+
outputs:
29+
build_branch:
30+
description: 'Ephemeral build branch created from source_branch'
31+
value: ${{ jobs.prepare-build-branch.outputs.build_branch }}
32+
built_commit_sha:
33+
description: 'Resolved commit SHA at the version-bump commit after build succeeded'
34+
value: ${{ jobs.build.outputs.built_commit_sha }}
35+
semantic_version:
36+
description: 'package.json version at the built commit'
37+
value: ${{ jobs.build.outputs.semantic_version }}
38+
android_version_code:
39+
description: 'android/app/build.gradle versionCode at the built commit'
40+
value: ${{ jobs.build.outputs.android_version_code }}
41+
workflow_dispatch:
42+
inputs:
43+
source_branch:
44+
description: 'Branch, tag, or SHA to build'
45+
required: true
46+
type: string
47+
default: 'main'
48+
environment:
49+
description: 'Build environment / track'
50+
required: true
51+
type: choice
52+
options:
53+
- exp
54+
- beta
55+
- rc
56+
default: rc
57+
upload_to_sentry:
58+
description: 'Upload JS source maps and native debug symbols to Sentry during the build (requires Sentry auth in the build environment)'
59+
required: false
60+
type: boolean
61+
default: false
62+
runner_provider:
63+
description: Runner provider for this manual trial run
64+
required: false
65+
type: choice
66+
options:
67+
- current
68+
- namespace
69+
default: current
70+
71+
permissions:
72+
contents: write
73+
id-token: write
74+
75+
jobs:
76+
# workflow_call inputs cannot use `type: choice` in GitHub Actions, so we enforce
77+
# the allowed `environment` values at runtime to prevent other workflows from
78+
# invoking this wrapper with arbitrary build tracks.
79+
validate-inputs:
80+
name: Validate inputs
81+
runs-on: ubuntu-latest
82+
steps:
83+
- name: Validate environment input
84+
env:
85+
ENVIRONMENT: ${{ inputs.environment }}
86+
run: |
87+
case "$ENVIRONMENT" in
88+
exp|beta|rc) echo "✅ environment=$ENVIRONMENT is allowed" ;;
89+
*) echo "::error::Invalid environment '$ENVIRONMENT'. Must be one of: exp, beta, rc"; exit 1 ;;
90+
esac
91+
92+
prepare-build-branch:
93+
needs: [validate-inputs]
94+
uses: ./.github/workflows/create-build-branch.yml
95+
with:
96+
source_branch: ${{ inputs.source_branch }}
97+
secrets: inherit
98+
99+
build:
100+
name: Build Android (${{ inputs.environment }})
101+
needs: [prepare-build-branch]
102+
uses: ./.github/workflows/build.yml
103+
with:
104+
build_name: main-${{ inputs.environment }}
105+
platform: android
106+
skip_version_bump: false
107+
source_branch: ${{ needs.prepare-build-branch.outputs.build_branch }}
108+
upload_to_sentry: ${{ inputs.upload_to_sentry }}
109+
runner_provider: ${{ inputs.runner_provider }}
110+
secrets: inherit
111+
112+
cleanup-build-branch:
113+
name: Cleanup build branch
114+
needs: [prepare-build-branch, build]
115+
if: always()
116+
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
117+
steps:
118+
- uses: actions/checkout@v4
119+
with:
120+
token: ${{ secrets.PR_TOKEN || github.token }}
121+
- name: Delete temporary build branch
122+
env:
123+
BRANCH: ${{ needs.prepare-build-branch.outputs.build_branch }}
124+
run: |
125+
if [ -n "$BRANCH" ]; then
126+
git push origin --delete "$BRANCH" || true
127+
echo "🧹 Deleted build branch: $BRANCH"
128+
fi

0 commit comments

Comments
 (0)