Skip to content

Commit e7405cc

Browse files
committed
Merge remote-tracking branch 'origin/main' into namespace-runner-trial4
# Conflicts: # .github/workflows/build-and-upload-to-testflight.yml # .github/workflows/build-ios-e2e.yml # .github/workflows/ci.yml # .github/workflows/run-e2e-smoke-tests-ios.yml # .github/workflows/run-e2e-workflow.yml # .github/workflows/update-e2e-fixtures.yml # .github/workflows/upload-to-testflight.yml
2 parents 1f2f633 + d611a0b commit e7405cc

1,384 files changed

Lines changed: 43684 additions & 46390 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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,18 @@ ignores:
139139

140140
# Used in Yarn plugin for preview builds
141141
- '@yarnpkg/core'
142+
143+
# Babel plugins referenced in babel.config.js by their short name (without
144+
# the `babel-plugin-` prefix). Babel resolves them correctly at build time
145+
# but depcheck doesn't recognize the indirect reference. See babel.config.js
146+
# line 65 comment for details.
147+
- 'babel-plugin-react-compiler' # used as 'react-compiler' (line 68)
148+
- 'babel-plugin-transform-inline-environment-variables' # used as 'transform-inline-environment-variables' (line 81)
149+
- 'babel-plugin-transform-remove-console' # used as 'transform-remove-console' (line 166, production env)
150+
151+
# Listed as a direct dep in package.json but no longer referenced in
152+
# babel.config.js (we use babel-preset-expo) or anywhere in our source.
153+
# It's still available transitively via babel-preset-expo + Metro, so
154+
# removing the direct declaration should be a no-op — left for a follow-up
155+
# cleanup once the RN 0.81 upgrade has settled to avoid surprises.
156+
- '@react-native/babel-preset'

.eslintrc.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
const utilNumberImportBurndownFiles = [
1010
'app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx',
1111
'app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx',
12-
'app/component-library/components-temp/Price/AggregatedPercentage/AggregatedPercentage.tsx',
13-
'app/component-library/components-temp/Price/AggregatedPercentage/utils.ts',
1412
'app/components/UI/AccountInfoCard/index.js',
1513
'app/components/UI/AssetOverview/Price/Price.advanced.tsx',
1614
'app/components/UI/AssetOverview/Price/Price.legacy.tsx',

.github/CODEOWNERS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,12 @@ app/components/UI/TemplateRenderer @MetaMask/confirmations @MetaMask/core-plat
158158
app/components/UI/Stake @MetaMask/earn
159159
app/core/Engine/controllers/earn-controller @MetaMask/earn
160160
app/core/Engine/messengers/earn-controller-messenger @MetaMask/earn
161+
app/core/Engine/controllers/chomp-api-service-init* @MetaMask/earn
162+
app/core/Engine/controllers/money-account-upgrade-controller-init* @MetaMask/earn
163+
app/core/Engine/messengers/chomp-api-service-messenger* @MetaMask/earn
164+
app/core/Engine/messengers/money-account-upgrade-controller-messenger* @MetaMask/earn
161165
app/selectors/earnController @MetaMask/earn
166+
app/selectors/featureFlagController/chompApi/ @MetaMask/earn
162167
**/Earn/** @MetaMask/earn
163168
**/earn/** @MetaMask/earn
164169
**/Money/** @MetaMask/earn
@@ -358,6 +363,7 @@ tests/websocket/ @MetaMask/qa
358363
.github/workflows/run-performance-e2e.yml @MetaMask/qa
359364
.github/workflows/run-performance-e2e-experimental.yml @MetaMask/qa
360365
.github/workflows/run-performance-e2e-release.yml @MetaMask/qa
366+
.github/workflows/run-system-tests.yml @MetaMask/qa
361367
.github/scripts/e2e-*.mjs @MetaMask/qa
362368
.github/scripts/collect-qa-stats.mjs @MetaMask/qa
363369
.github/scripts/generate-regression-slack-summary.mjs @MetaMask/qa
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: 'Check force-builds override'
2+
description: >-
3+
Detects whether the current workflow run should bypass native build reuse
4+
(both the GHA cache and the cross-run artifact lookup) and always compile
5+
fresh. The override is honored on `pull_request` events via a `force-builds`
6+
label OR a `[force-builds]` token in the head commit message. It is
7+
intentionally ignored on `merge_group` and `push` events so the merge queue
8+
always uses hash-verified reuse.
9+
10+
inputs:
11+
github-token:
12+
description: >-
13+
GitHub token with `pull-requests: read` (for label lookup) and
14+
`contents: read` (to fetch the head commit message via the REST API).
15+
required: true
16+
label-name:
17+
description: 'PR label that, when present, forces fresh builds'
18+
required: false
19+
default: 'force-builds'
20+
commit-tag:
21+
description: 'Case-sensitive substring in the head commit message that forces fresh builds'
22+
required: false
23+
default: '[force-builds]'
24+
25+
outputs:
26+
force:
27+
description: "'true' when fresh builds should be forced, otherwise 'false'"
28+
value: ${{ steps.compute.outputs.force }}
29+
30+
runs:
31+
using: 'composite'
32+
steps:
33+
- name: Compute force-builds flag
34+
id: compute
35+
shell: bash
36+
env:
37+
GH_TOKEN: ${{ inputs.github-token }}
38+
LABEL_NAME: ${{ inputs.label-name }}
39+
COMMIT_TAG: ${{ inputs.commit-tag }}
40+
EVENT_NAME: ${{ github.event_name }}
41+
HEAD_COMMIT_HASH: ${{ github.event.pull_request.head.sha }}
42+
PR_NUMBER: ${{ github.event.pull_request.number }}
43+
REPOSITORY: ${{ github.repository }}
44+
run: |
45+
FORCE="false"
46+
47+
if [[ "$EVENT_NAME" != "pull_request" ]]; then
48+
echo "Event is $EVENT_NAME; force-builds override is ignored outside pull_request events."
49+
echo "force=$FORCE" >> "$GITHUB_OUTPUT"
50+
exit 0
51+
fi
52+
53+
# Commit-message tag.
54+
COMMIT_MESSAGE=""
55+
if COMMIT_MESSAGE=$(gh api \
56+
"repos/$REPOSITORY/commits/$HEAD_COMMIT_HASH" \
57+
--jq '.commit.message' 2>/dev/null); then
58+
if printf '%s' "$COMMIT_MESSAGE" \
59+
| grep --fixed-strings --quiet "$COMMIT_TAG"; then
60+
echo "-> force=true because '$COMMIT_TAG' was found in commit message of $HEAD_COMMIT_HASH"
61+
FORCE="true"
62+
fi
63+
else
64+
echo "::warning::Failed to fetch commit message for $HEAD_COMMIT_HASH via GitHub API; commit-tag force-builds check skipped for this run (the '$LABEL_NAME' label path still works)."
65+
fi
66+
67+
# PR label
68+
if [[ -n "$PR_NUMBER" ]]; then
69+
if gh pr view "$PR_NUMBER" --repo "$REPOSITORY" \
70+
--json labels --jq '.labels[].name' \
71+
| grep --fixed-strings --line-regexp --quiet "$LABEL_NAME"; then
72+
echo "-> force=true because '$LABEL_NAME' label is applied to PR #$PR_NUMBER"
73+
FORCE="true"
74+
fi
75+
fi
76+
77+
if [[ "$FORCE" == "false" ]]; then
78+
echo "No force-builds override active."
79+
fi
80+
81+
echo "force=$FORCE" >> "$GITHUB_OUTPUT"
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
name: 'Find reusable build from prior run'
2+
description: >-
3+
Searches recent workflow runs across three tiers (same branch, base branch,
4+
then any open PR branch) for a run whose `build-source-hash` commit status
5+
matches the current fingerprint AND whose required build artifacts are still
6+
available. If a match is found, outputs the run id so a subsequent
7+
`actions/download-artifact` step can pull the artifacts directly instead of
8+
triggering a fresh native build.
9+
10+
The third (cross-PR) tier is required because GitHub's `listWorkflowRuns`
11+
`branch` parameter filters against `head_branch` — the PR source branch for
12+
`pull_request` events — so branch-scoped lookups can never discover other
13+
PRs' runs. The cross-PR tier drops the branch filter and instead uses
14+
`event: pull_request` to let the fingerprint itself act as the cross-PR
15+
deduplication key.
16+
17+
inputs:
18+
fingerprint:
19+
description: 'The @expo/fingerprint hash the candidate must match'
20+
required: true
21+
artifact-names:
22+
description: 'JSON array of artifact names that must all be present on the candidate run'
23+
required: true
24+
github-token:
25+
description: 'GitHub token with `actions: read` and `statuses: read` permissions'
26+
required: true
27+
workflow-file:
28+
description: 'Workflow filename whose runs will be searched'
29+
required: false
30+
default: 'ci.yml'
31+
base-branch:
32+
description: 'Fallback branch when no same-branch match is found'
33+
required: false
34+
default: 'main'
35+
status-context:
36+
description: 'Commit status context that carries the fingerprint'
37+
required: false
38+
default: 'build-source-hash'
39+
max-candidates-per-branch:
40+
description: 'How many recent runs to inspect per branch-scoped tier (same-branch, base-branch)'
41+
required: false
42+
default: '10'
43+
max-candidates-cross-pr:
44+
description: >-
45+
How many recent `pull_request`-event runs (across all branches) to inspect
46+
in the cross-PR tier. The fingerprint filter is highly discriminating, so
47+
the practical cost is one `getCombinedStatusForRef` call per candidate
48+
until a match is found.
49+
required: false
50+
default: '30'
51+
52+
outputs:
53+
found:
54+
description: "'true' when a reusable run was found"
55+
value: ${{ steps.lookup.outputs.found }}
56+
run-id:
57+
description: 'Workflow run id that produced the reusable artifacts'
58+
value: ${{ steps.lookup.outputs.run-id }}
59+
source-sha:
60+
description: 'Commit SHA of the reusable run'
61+
value: ${{ steps.lookup.outputs.source-sha }}
62+
source-branch:
63+
description: 'Branch of the reusable run (same-branch or base-branch)'
64+
value: ${{ steps.lookup.outputs.source-branch }}
65+
66+
runs:
67+
using: 'composite'
68+
steps:
69+
- name: Search prior runs for matching fingerprint
70+
id: lookup
71+
uses: actions/github-script@v7
72+
continue-on-error: true
73+
env:
74+
TARGET_FINGERPRINT: ${{ inputs.fingerprint }}
75+
ARTIFACT_NAMES_JSON: ${{ inputs.artifact-names }}
76+
WORKFLOW_FILE: ${{ inputs.workflow-file }}
77+
BASE_BRANCH: ${{ inputs.base-branch }}
78+
STATUS_CONTEXT: ${{ inputs.status-context }}
79+
MAX_CANDIDATES: ${{ inputs.max-candidates-per-branch }}
80+
MAX_CANDIDATES_CROSS_PR: ${{ inputs.max-candidates-cross-pr }}
81+
HEAD_BRANCH: ${{ github.head_ref || github.ref_name }}
82+
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
83+
CURRENT_RUN_ID: ${{ github.run_id }}
84+
with:
85+
github-token: ${{ inputs.github-token }}
86+
script: |
87+
const {
88+
TARGET_FINGERPRINT,
89+
ARTIFACT_NAMES_JSON,
90+
WORKFLOW_FILE,
91+
BASE_BRANCH,
92+
STATUS_CONTEXT,
93+
MAX_CANDIDATES,
94+
MAX_CANDIDATES_CROSS_PR,
95+
HEAD_BRANCH,
96+
HEAD_SHA,
97+
CURRENT_RUN_ID,
98+
} = process.env;
99+
100+
const setNotFound = () => {
101+
core.setOutput('found', 'false');
102+
core.setOutput('run-id', '');
103+
core.setOutput('source-sha', '');
104+
core.setOutput('source-branch', '');
105+
};
106+
107+
if (!TARGET_FINGERPRINT) {
108+
core.warning('No fingerprint provided; skipping lookup');
109+
setNotFound();
110+
return;
111+
}
112+
113+
let requiredArtifacts;
114+
try {
115+
requiredArtifacts = JSON.parse(ARTIFACT_NAMES_JSON);
116+
} catch (err) {
117+
core.warning(`Could not parse artifact-names input: ${err.message}`);
118+
setNotFound();
119+
return;
120+
}
121+
if (!Array.isArray(requiredArtifacts) || requiredArtifacts.length === 0) {
122+
core.warning('artifact-names must be a non-empty JSON array');
123+
setNotFound();
124+
return;
125+
}
126+
127+
const maxCandidates = Number(MAX_CANDIDATES) || 10;
128+
const maxCandidatesCrossPr = Number(MAX_CANDIDATES_CROSS_PR) || 30;
129+
const currentRunId = String(CURRENT_RUN_ID);
130+
131+
// Three-tier discovery:
132+
// 1. same-branch — fastest path, catches retries and new commits
133+
// on the current PR.
134+
// 2. base-branch — catches post-merge CI runs on `main`. Only
135+
// matches `push`-event runs (pull_request runs
136+
// have head_branch=<source branch>, not main).
137+
// 3. cross-pr — searches recent `pull_request` runs across
138+
// ALL source branches so two unrelated PRs with
139+
// the same fingerprint can reuse each other's
140+
// artifacts. This tier deliberately drops the
141+
// `branch` filter; without it, branch-scoped
142+
// lookups can never discover another PR's run
143+
// (GitHub filters `branch` against head_branch,
144+
// which is the PR source branch).
145+
const tiers = [
146+
{
147+
label: `same-branch (branch=${HEAD_BRANCH})`,
148+
params: { branch: HEAD_BRANCH, per_page: maxCandidates },
149+
},
150+
];
151+
if (BASE_BRANCH && BASE_BRANCH !== HEAD_BRANCH) {
152+
tiers.push({
153+
label: `base-branch (branch=${BASE_BRANCH})`,
154+
params: { branch: BASE_BRANCH, per_page: maxCandidates },
155+
});
156+
}
157+
tiers.push({
158+
label: `cross-pr (event=pull_request, any branch, last ${maxCandidatesCrossPr} runs)`,
159+
params: { event: 'pull_request', per_page: maxCandidatesCrossPr },
160+
// Skip runs already visited by the same-branch tier to avoid
161+
// wasting API calls on duplicates.
162+
skipHeadBranch: HEAD_BRANCH,
163+
});
164+
165+
async function getFingerprintForSha(sha) {
166+
try {
167+
const { data } = await github.rest.repos.getCombinedStatusForRef({
168+
owner: context.repo.owner,
169+
repo: context.repo.repo,
170+
ref: sha,
171+
per_page: 100,
172+
});
173+
const status = data.statuses.find((s) => s.context === STATUS_CONTEXT);
174+
return status ? status.description : null;
175+
} catch (err) {
176+
core.info(`getCombinedStatusForRef failed for ${sha}: ${err.message}`);
177+
return null;
178+
}
179+
}
180+
181+
async function hasAllArtifacts(runId) {
182+
try {
183+
const artifacts = await github.paginate(
184+
github.rest.actions.listWorkflowRunArtifacts,
185+
{
186+
owner: context.repo.owner,
187+
repo: context.repo.repo,
188+
run_id: runId,
189+
per_page: 100,
190+
},
191+
);
192+
const available = new Set(
193+
artifacts
194+
.filter((a) => !a.expired)
195+
.map((a) => a.name),
196+
);
197+
const missing = requiredArtifacts.filter((n) => !available.has(n));
198+
if (missing.length > 0) {
199+
core.info(`Run ${runId} missing artifacts: ${missing.join(', ')}`);
200+
return false;
201+
}
202+
return true;
203+
} catch (err) {
204+
core.info(`listWorkflowRunArtifacts failed for ${runId}: ${err.message}`);
205+
return false;
206+
}
207+
}
208+
209+
const seenRunIds = new Set();
210+
seenRunIds.add(currentRunId);
211+
212+
for (const tier of tiers) {
213+
core.info(`Searching tier: ${tier.label}`);
214+
let runs;
215+
try {
216+
const { data } = await github.rest.actions.listWorkflowRuns({
217+
owner: context.repo.owner,
218+
repo: context.repo.repo,
219+
workflow_id: WORKFLOW_FILE,
220+
...tier.params,
221+
});
222+
runs = data.workflow_runs || [];
223+
} catch (err) {
224+
core.warning(`listWorkflowRuns failed for tier "${tier.label}": ${err.message}`);
225+
continue;
226+
}
227+
228+
for (const run of runs) {
229+
const runIdStr = String(run.id);
230+
if (seenRunIds.has(runIdStr)) continue;
231+
seenRunIds.add(runIdStr);
232+
233+
if (tier.skipHeadBranch && run.head_branch === tier.skipHeadBranch) continue;
234+
235+
if (run.status !== 'completed' && run.status !== 'in_progress') continue;
236+
237+
const fingerprint = await getFingerprintForSha(run.head_sha);
238+
if (!fingerprint) continue;
239+
if (fingerprint !== TARGET_FINGERPRINT) continue;
240+
241+
if (!(await hasAllArtifacts(run.id))) continue;
242+
243+
core.info(
244+
`Match: tier="${tier.label}" run=${run.id} sha=${run.head_sha} branch=${run.head_branch} url=${run.html_url}`,
245+
);
246+
core.setOutput('found', 'true');
247+
core.setOutput('run-id', runIdStr);
248+
core.setOutput('source-sha', run.head_sha);
249+
core.setOutput('source-branch', run.head_branch || '');
250+
return;
251+
}
252+
}
253+
254+
core.info('No reusable build found across any tier');
255+
setNotFound();

0 commit comments

Comments
 (0)