Skip to content

Commit 36ffd4d

Browse files
committed
Merge remote-tracking branch 'origin/main' into namespace-runner-trial
# Conflicts: # .github/workflows/ci.yml
2 parents c7a597e + dd0a6ab commit 36ffd4d

95 files changed

Lines changed: 8369 additions & 817 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: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
name: CI Status Gate
2+
description: Evaluate required CI job results and fail on unexpected skips or failed jobs.
3+
4+
inputs:
5+
needs-json:
6+
description: JSON representation of the calling job's needs context.
7+
required: true
8+
requirement-context-json:
9+
description: JSON representation of get-requirements outputs.
10+
required: true
11+
e2e-job-regex:
12+
description: Regex matching E2E build/test jobs whose skipped result is allowed. Failed or cancelled E2E jobs still fail.
13+
required: false
14+
default: '^e2e-'
15+
event-name:
16+
description: GitHub event name for the current workflow run.
17+
required: true
18+
is-fork:
19+
description: Whether the current pull request originates from a fork. When true, skipped jobs are treated as allowed skips.
20+
required: false
21+
default: 'false'
22+
23+
runs:
24+
using: composite
25+
steps:
26+
- name: Evaluate CI status
27+
shell: bash
28+
env:
29+
NEEDS_JSON: ${{ inputs.needs-json }}
30+
REQUIREMENT_CONTEXT_JSON: ${{ inputs.requirement-context-json }}
31+
E2E_JOB_REGEX: ${{ inputs.e2e-job-regex }}
32+
EVENT_NAME: ${{ inputs.event-name }}
33+
IS_FORK: ${{ inputs.is-fork }}
34+
run: |
35+
set -euo pipefail
36+
37+
get_requirement() {
38+
local key="$1"
39+
jq -nr --arg key "$key" 'env.REQUIREMENT_CONTEXT_JSON | fromjson | .[$key] // "false"'
40+
}
41+
42+
sanitize_markdown_cell() {
43+
local value="$1"
44+
value="${value//$'\n'/ }"
45+
value="${value//|/\\|}"
46+
printf '%s' "$value"
47+
}
48+
49+
add_summary_row() {
50+
local job_name result decision reason
51+
job_name="$(sanitize_markdown_cell "$1")"
52+
result="$(sanitize_markdown_cell "$2")"
53+
decision="$(sanitize_markdown_cell "$3")"
54+
reason="$(sanitize_markdown_cell "$4")"
55+
56+
printf '| `%s` | `%s` | %s | %s |\n' \
57+
"$job_name" "$result" "$decision" "$reason" >> "$summary_file"
58+
}
59+
60+
mark_failure() {
61+
local message="$1"
62+
failed="true"
63+
echo "::error::$message"
64+
}
65+
66+
validate_json_type() {
67+
local variable_name="$1"
68+
local expected_type="$2"
69+
70+
if ! jq -en --arg variable_name "$variable_name" --arg expected_type "$expected_type" \
71+
'(env[$variable_name] | fromjson | type) == $expected_type' >/dev/null 2>&1; then
72+
echo "::error::$variable_name is not a valid JSON $expected_type"
73+
exit 1
74+
fi
75+
}
76+
77+
require_requirement_key() {
78+
local key="$1"
79+
80+
if ! jq -en --arg key "$key" \
81+
'env.REQUIREMENT_CONTEXT_JSON | fromjson | .[$key] != null' >/dev/null 2>&1; then
82+
echo "::error::REQUIREMENT_CONTEXT_JSON is missing or null for required key: $key"
83+
exit 1
84+
fi
85+
}
86+
87+
validate_json_type NEEDS_JSON object
88+
validate_json_type REQUIREMENT_CONTEXT_JSON object
89+
90+
for required_key in skip_everything block_merge_for_e2e_readiness; do
91+
require_requirement_key "$required_key"
92+
done
93+
94+
skip_everything="$(get_requirement skip_everything)"
95+
block_merge_for_e2e_readiness="$(get_requirement block_merge_for_e2e_readiness)"
96+
97+
if [[ "$block_merge_for_e2e_readiness" == "true" ]]; then
98+
echo "::error::The 'pr-not-ready-for-e2e' label is still applied. Remove it to trigger E2E tests before merging."
99+
exit 1
100+
fi
101+
102+
if [[ "$skip_everything" == "true" ]]; then
103+
echo "skip_everything=true; treating all jobs as passed"
104+
exit 0
105+
fi
106+
107+
failed="false"
108+
summary_file="$(mktemp)"
109+
trap 'if [[ -n "${GITHUB_STEP_SUMMARY:-}" && -f "$summary_file" ]]; then cat "$summary_file" >> "$GITHUB_STEP_SUMMARY"; fi; rm -f "$summary_file"' EXIT
110+
job_count=0
111+
112+
{
113+
echo "### CI Status Gate"
114+
echo
115+
echo "| Job | Result | Decision | Reason |"
116+
echo "| --- | --- | --- | --- |"
117+
} >> "$summary_file"
118+
119+
while IFS=$'\t' read -r job_name result; do
120+
job_count=$((job_count + 1))
121+
122+
case "$result" in
123+
success)
124+
add_summary_row "$job_name" "$result" "pass" "job succeeded"
125+
;;
126+
failure|cancelled)
127+
mark_failure "$job_name finished with result: $result"
128+
add_summary_row "$job_name" "$result" "fail" "job did not complete successfully"
129+
;;
130+
skipped)
131+
if [[ "$job_name" =~ $E2E_JOB_REGEX ]]; then
132+
add_summary_row "$job_name" "$result" "pass" "skipped E2E jobs are allowed"
133+
elif [[ "$EVENT_NAME" == "merge_group" ]]; then
134+
add_summary_row "$job_name" "$result" "pass" "merge queue skip is allowed"
135+
elif [[ "$IS_FORK" == "true" ]]; then
136+
add_summary_row "$job_name" "$result" "pass" "fork-only skip is allowed"
137+
else
138+
mark_failure "$job_name was skipped unexpectedly"
139+
add_summary_row "$job_name" "$result" "fail" "skip was not expected"
140+
fi
141+
;;
142+
*)
143+
mark_failure "$job_name has unknown result: $result"
144+
add_summary_row "$job_name" "$result" "fail" "job result is unknown"
145+
;;
146+
esac
147+
done < <(jq -nr 'env.NEEDS_JSON | fromjson | to_entries[] | [.key, (.value.result // "")] | @tsv')
148+
149+
if [[ "$job_count" -eq 0 ]]; then
150+
echo "::error::NEEDS_JSON does not contain any jobs"
151+
exit 1
152+
fi
153+
154+
if [[ "$failed" == "true" ]]; then
155+
exit 1
156+
fi
157+
158+
echo "All required jobs passed"

.github/actions/setup-e2e-env/action.yml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,9 @@ runs:
283283
node_modules
284284
.yarn/install-state.gz
285285
key: ${{ inputs.cache-prefix }}-yarn-${{ inputs.platform }}-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
286+
restore-keys: |
287+
${{ inputs.cache-prefix }}-yarn-${{ inputs.platform }}-${{ runner.os }}-
288+
continue-on-error: true
286289

287290
- name: Install JavaScript dependencies with retry
288291
id: yarn-install
@@ -387,19 +390,19 @@ runs:
387390
${{ runner.os }}-cocoapods-specs-
388391
continue-on-error: true
389392

390-
- name: Clear CocoaPods trunk to prevent stale specs
391-
if: ${{ inputs.platform == 'ios' }}
392-
run: pod repo remove trunk || true
393-
shell: bash
394-
395393
- name: Install CocoaPods via bundler
396394
if: ${{ inputs.platform == 'ios'}}
397395
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 #v3.0.2
398396
with:
399397
timeout_minutes: 15
400-
max_attempts: 2
401-
retry_wait_seconds: 30
398+
max_attempts: 3
399+
retry_wait_seconds: 60
400+
on_retry_command: |
401+
echo "::warning::CocoaPods install failed, retrying after trunk cleanup..."
402+
pod repo remove trunk || true
402403
command: cd ios && bundle exec pod install --repo-update
404+
env:
405+
COCOAPODS_DISABLE_STATS: 'true'
403406

404407
- name: Install applesimutils
405408
if: ${{ inputs.platform == 'ios' }}

.github/workflows/ci.yml

Lines changed: 24 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -953,113 +953,41 @@ jobs:
953953
fi
954954
fi
955955
956-
all-jobs-pass:
957-
name: All jobs pass
958-
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
959-
if: ${{ !cancelled() }}
960-
needs:
961-
[
962-
check-diff,
963-
dedupe,
964-
scripts,
965-
unit-tests,
966-
component-view-tests,
967-
check-workflows,
968-
js-bundle-size-check,
969-
sonar-cloud-quality-gate-status,
970-
]
971-
outputs:
972-
ALL_JOBS_PASSED: ${{ steps.jobs-passed-status.outputs.ALL_JOBS_PASSED }}
973-
steps:
974-
- name: Set jobs passed status
975-
id: jobs-passed-status
976-
env:
977-
NEEDS_CONTEXT: ${{ toJSON(needs) }}
978-
EVENT_NAME: ${{ github.event_name }}
979-
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
980-
run: |
981-
# Check results of all required jobs dynamically
982-
# On merge_group events, "skipped" is acceptable (some jobs intentionally skip)
983-
# On fork PRs, "skipped" is acceptable (secret-dependent jobs are intentionally skipped)
984-
# On other events (push to main), all jobs must succeed
985-
986-
FAILED="false"
987-
988-
while read -r job_name result; do
989-
if [[ "$result" == "failure" ]] || [[ "$result" == "cancelled" ]]; then
990-
echo "::error::Job '$job_name' failed with result: $result"
991-
FAILED="true"
992-
elif [[ "$result" == "skipped" ]]; then
993-
if [[ "$EVENT_NAME" == "merge_group" ]] || [[ "$IS_FORK" == "true" ]]; then
994-
echo "Job '$job_name' was skipped (OK for merge_group events and fork PRs)"
995-
else
996-
echo "::error::Job '$job_name' was unexpectedly skipped on $EVENT_NAME event"
997-
FAILED="true"
998-
fi
999-
else
1000-
echo "Job '$job_name' passed"
1001-
fi
1002-
done < <(echo "$NEEDS_CONTEXT" | jq -r 'to_entries[] | "\(.key) \(.value.result)"')
1003-
1004-
if [[ "$FAILED" == "true" ]]; then
1005-
echo "Some required jobs failed"
1006-
exit 1
1007-
fi
1008-
1009-
echo "ALL_JOBS_PASSED=true" >> "$GITHUB_OUTPUT"
1010-
1011956
check-all-jobs-pass:
1012957
name: Check all jobs pass
1013-
if: ${{ !cancelled() }}
958+
# Run the aggregate gate even when optional dependencies are skipped.
959+
# The composite action decides which skipped jobs are acceptable.
960+
if: ${{ always() && !cancelled() }}
1014961
runs-on: ${{ inputs.runner_provider == 'namespace' && 'namespace-profile-metamask-ci-linux' || 'ubuntu-latest' }}
1015962
needs:
1016963
- get_requirements
1017-
- all-jobs-pass
964+
- check-diff
965+
- dedupe
966+
- scripts
967+
- unit-tests
968+
- component-view-tests
969+
- check-workflows
970+
- js-bundle-size-check
971+
- sonar-cloud-quality-gate-status
1018972
- build-android-apks
1019973
- build-ios-apps
1020974
- e2e-smoke-tests-android
1021975
- e2e-smoke-tests-ios
1022-
env:
1023-
SKIPPED: ${{ needs.get_requirements.outputs.skip_everything == 'true' }}
1024976
steps:
1025-
- name: Block merge while pr-not-ready-for-e2e label is applied
1026-
if: ${{ needs.get_requirements.outputs.block_merge_for_e2e_readiness == 'true' }}
1027-
run: |
1028-
echo "::error::The 'pr-not-ready-for-e2e' label is still applied. Remove it to trigger E2E tests before merging."
1029-
exit 1
1030-
- run: |
1031-
# If the merge queue was skipped, consider all jobs as passed
1032-
if [[ "$SKIPPED" == "true" ]]; then
1033-
echo "Merge queue skipped, considering all jobs as passed"
1034-
exit 0
1035-
fi
1036-
1037-
# Check if all non-E2E jobs passed
1038-
if [[ "${{ needs.all-jobs-pass.outputs.ALL_JOBS_PASSED }}" != "true" ]]; then
1039-
echo "Non-E2E jobs failed"
1040-
exit 1
1041-
fi
1042-
1043-
# Check E2E build + smoke results only if E2E should have run.
1044-
# 'skipped' is acceptable — covers merge_group, fork PRs, ignorable-only changes,
1045-
# platform-only PRs, and AI selection returning zero tags.
1046-
# 'failure'/'cancelled' on any of build or smoke must block merge.
1047-
if [[ "${{ needs.get_requirements.outputs.skip_e2e }}" != "true" ]]; then
1048-
for entry in \
1049-
"build-android-apks:${{ needs.build-android-apks.result }}" \
1050-
"e2e-smoke-tests-android:${{ needs.e2e-smoke-tests-android.result }}" \
1051-
"build-ios-apps:${{ needs.build-ios-apps.result }}" \
1052-
"e2e-smoke-tests-ios:${{ needs.e2e-smoke-tests-ios.result }}"; do
1053-
name="${entry%%:*}"
1054-
result="${entry#*:}"
1055-
if [[ "$result" == "failure" ]] || [[ "$result" == "cancelled" ]]; then
1056-
echo "::error::Required E2E job '$name' did not succeed (result: $result)"
1057-
exit 1
1058-
fi
1059-
done
1060-
fi
977+
- uses: actions/checkout@v6
978+
with:
979+
fetch-depth: 1
980+
sparse-checkout: |
981+
.github/actions/ci-status-gate
1061982
1062-
echo "All required jobs passed"
983+
- name: Evaluate CI status
984+
uses: ./.github/actions/ci-status-gate
985+
with:
986+
needs-json: ${{ toJSON(needs) }}
987+
requirement-context-json: ${{ toJSON(needs.get_requirements.outputs) }}
988+
e2e-job-regex: '^(build-android-apks|build-ios-apps|e2e-smoke-tests-android|e2e-smoke-tests-ios)$'
989+
event-name: ${{ github.event_name }}
990+
is-fork: ${{ github.event.pull_request.head.repo.fork == true }}
1063991

1064992
log-merge-group-failure:
1065993
name: Log merge group failure

app/components/UI/Rewards/RewardsNavigator.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import OndoCampaignRwaSelectorView from './Views/OndoCampaignRwaSelectorView';
1616
import OndoCampaignPortfolioView from './Views/OndoCampaignPortfolioView';
1717
import OndoCampaignStatsView from './Views/OndoCampaignStatsView';
1818
import CampaignTourStepView from './Views/CampaignTourStepView';
19+
import PerpsTradingCampaignDetailsView from './Views/PerpsTradingCampaignDetailsView';
20+
import PerpsTradingCampaignLeaderboardView from './Views/PerpsTradingCampaignLeaderboardView';
21+
import PerpsTradingCampaignStatsView from './Views/PerpsTradingCampaignStatsView';
1922
import { useDispatch, useSelector } from 'react-redux';
2023
import { selectRewardsSubscriptionId } from '../../../selectors/rewards';
2124
import {
@@ -92,6 +95,8 @@ const RewardsNavigator: React.FC = () => {
9295
navigation.navigate(Routes.REWARDS_ONDO_CAMPAIGN_DETAILS_VIEW);
9396
} else if (pendingDeeplink?.campaign === 'season1') {
9497
navigation.navigate(Routes.REWARDS_SEASON_ONE_CAMPAIGN_DETAILS_VIEW);
98+
} else if (pendingDeeplink?.campaign === 'perps-comp') {
99+
navigation.navigate(Routes.REWARDS_PERPS_TRADING_CAMPAIGN_DETAILS_VIEW);
95100
} else if (pendingDeeplink?.page === 'musd') {
96101
navigation.navigate(Routes.REWARDS_MUSD_CALCULATOR_VIEW);
97102
} else if (pendingDeeplink?.page === 'benefits') {
@@ -194,6 +199,21 @@ const RewardsNavigator: React.FC = () => {
194199
component={OndoCampaignStatsView}
195200
options={{ headerShown: false }}
196201
/>
202+
<Stack.Screen
203+
name={Routes.REWARDS_PERPS_TRADING_CAMPAIGN_DETAILS_VIEW}
204+
component={PerpsTradingCampaignDetailsView}
205+
options={{ headerShown: false }}
206+
/>
207+
<Stack.Screen
208+
name={Routes.REWARDS_PERPS_TRADING_CAMPAIGN_LEADERBOARD}
209+
component={PerpsTradingCampaignLeaderboardView}
210+
options={{ headerShown: false }}
211+
/>
212+
<Stack.Screen
213+
name={Routes.REWARDS_PERPS_TRADING_CAMPAIGN_STATS}
214+
component={PerpsTradingCampaignStatsView}
215+
options={{ headerShown: false }}
216+
/>
197217
</>
198218
) : null}
199219
</Stack.Navigator>

0 commit comments

Comments
 (0)