Skip to content

Commit 4b756de

Browse files
authored
ci: extract mobile CI status gate (#29619)
## **Description** This PR extracts the final Mobile CI status gate into a composite action while preserving the existing `Check all jobs pass` status check name. Why: - The final gate decides whether the workflow should pass after standard CI, E2E build/test jobs, fork-only skips, and merge queue skips complete. - Keeping that logic in a dedicated action makes the workflow easier to maintain and keeps the pass/fail decision consistent. What changed: - Added `.github/actions/ci-status-gate`. - Updated `.github/workflows/ci.yml` so `Check all jobs pass` calls the composite action. - Removed the intermediate `All jobs pass` job. - Preserved current handling for failed jobs, cancelled jobs, skipped E2E jobs, fork PR skips, merge queue skips, and E2E readiness blocking. - Added a step summary table that explains each job result evaluated by the gate. ## **Changelog** CHANGELOG entry: null ## **Related issues** No public issue: CI maintenance refactor. ## **Manual testing steps** Validated the workflow files locally: ```bash actionlint -color -config-file .github/actionlint.yaml .github/workflows/ci.yml ruby -e 'require "yaml"; YAML.load_file(".github/workflows/ci.yml"); YAML.load_file(".github/actions/ci-status-gate/action.yml")' git diff --check ``` Validated extracted gate behavior in the public test fork: https://github.com/consensys-test/metamask-mobile-test - Non-ignorable app-code PR with `pr-not-ready-for-e2e`: `Check all jobs pass` failed as expected because `block_merge_for_e2e_readiness=true`. - PR: consensys-test#3 - Gate: https://github.com/consensys-test/metamask-mobile-test/actions/runs/25321128954/job/74237306086 - Locale-only PR without readiness blocking: E2E build/test jobs skipped, and the final gate accepted those skips. - PR: consensys-test#2 - Gate: https://github.com/consensys-test/metamask-mobile-test/actions/runs/25318425027/job/74223723784 - Locale-only PR with `pr-not-ready-for-e2e`: E2E jobs stayed skipped and `Check all jobs pass` passed. - PR: consensys-test#2 - Gate: https://github.com/consensys-test/metamask-mobile-test/actions/runs/25321510460/job/74234336146 - Standard CI failure: `Check all jobs pass` failed when a required standard CI job failed. - PR: consensys-test#4 - Gate: https://github.com/consensys-test/metamask-mobile-test/actions/runs/25321403908/job/74233562352 - E2E job failure: `Check all jobs pass` failed when an E2E smoke job failed. - PR: consensys-test#6 - E2E job: https://github.com/consensys-test/metamask-mobile-test/actions/runs/25328925239/job/74257209475 - Gate: https://github.com/consensys-test/metamask-mobile-test/actions/runs/25328925239/job/74259419260 - Build job failure: `Check all jobs pass` failed when an E2E build job failed. - PR: consensys-test#5 - Build job: https://github.com/consensys-test/metamask-mobile-test/actions/runs/25331281786/job/74265395383 - Gate: https://github.com/consensys-test/metamask-mobile-test/actions/runs/25331281786/job/74267776538 ## **Screenshots/Recordings** ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** - [x] I've followed MetaMask Contributor Docs and MetaMask Mobile Coding Standards. - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using JSDoc format if applicable - [x] I've applied the right labels on the PR if applicable. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR where applicable. - [ ] I confirm that this PR addresses the described change and includes the necessary testing evidence.
1 parent 9348a74 commit 4b756de

2 files changed

Lines changed: 182 additions & 96 deletions

File tree

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/workflows/ci.yml

Lines changed: 24 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -937,113 +937,41 @@ jobs:
937937
fi
938938
fi
939939
940-
all-jobs-pass:
941-
name: All jobs pass
942-
runs-on: ubuntu-latest
943-
if: ${{ !cancelled() }}
944-
needs:
945-
[
946-
check-diff,
947-
dedupe,
948-
scripts,
949-
unit-tests,
950-
component-view-tests,
951-
check-workflows,
952-
js-bundle-size-check,
953-
sonar-cloud-quality-gate-status,
954-
]
955-
outputs:
956-
ALL_JOBS_PASSED: ${{ steps.jobs-passed-status.outputs.ALL_JOBS_PASSED }}
957-
steps:
958-
- name: Set jobs passed status
959-
id: jobs-passed-status
960-
env:
961-
NEEDS_CONTEXT: ${{ toJSON(needs) }}
962-
EVENT_NAME: ${{ github.event_name }}
963-
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
964-
run: |
965-
# Check results of all required jobs dynamically
966-
# On merge_group events, "skipped" is acceptable (some jobs intentionally skip)
967-
# On fork PRs, "skipped" is acceptable (secret-dependent jobs are intentionally skipped)
968-
# On other events (push to main), all jobs must succeed
969-
970-
FAILED="false"
971-
972-
while read -r job_name result; do
973-
if [[ "$result" == "failure" ]] || [[ "$result" == "cancelled" ]]; then
974-
echo "::error::Job '$job_name' failed with result: $result"
975-
FAILED="true"
976-
elif [[ "$result" == "skipped" ]]; then
977-
if [[ "$EVENT_NAME" == "merge_group" ]] || [[ "$IS_FORK" == "true" ]]; then
978-
echo "Job '$job_name' was skipped (OK for merge_group events and fork PRs)"
979-
else
980-
echo "::error::Job '$job_name' was unexpectedly skipped on $EVENT_NAME event"
981-
FAILED="true"
982-
fi
983-
else
984-
echo "Job '$job_name' passed"
985-
fi
986-
done < <(echo "$NEEDS_CONTEXT" | jq -r 'to_entries[] | "\(.key) \(.value.result)"')
987-
988-
if [[ "$FAILED" == "true" ]]; then
989-
echo "Some required jobs failed"
990-
exit 1
991-
fi
992-
993-
echo "ALL_JOBS_PASSED=true" >> "$GITHUB_OUTPUT"
994-
995940
check-all-jobs-pass:
996941
name: Check all jobs pass
997-
if: ${{ !cancelled() }}
942+
# Run the aggregate gate even when optional dependencies are skipped.
943+
# The composite action decides which skipped jobs are acceptable.
944+
if: ${{ always() && !cancelled() }}
998945
runs-on: ubuntu-latest
999946
needs:
1000947
- get_requirements
1001-
- all-jobs-pass
948+
- check-diff
949+
- dedupe
950+
- scripts
951+
- unit-tests
952+
- component-view-tests
953+
- check-workflows
954+
- js-bundle-size-check
955+
- sonar-cloud-quality-gate-status
1002956
- build-android-apks
1003957
- build-ios-apps
1004958
- e2e-smoke-tests-android
1005959
- e2e-smoke-tests-ios
1006-
env:
1007-
SKIPPED: ${{ needs.get_requirements.outputs.skip_everything == 'true' }}
1008960
steps:
1009-
- name: Block merge while pr-not-ready-for-e2e label is applied
1010-
if: ${{ needs.get_requirements.outputs.block_merge_for_e2e_readiness == 'true' }}
1011-
run: |
1012-
echo "::error::The 'pr-not-ready-for-e2e' label is still applied. Remove it to trigger E2E tests before merging."
1013-
exit 1
1014-
- run: |
1015-
# If the merge queue was skipped, consider all jobs as passed
1016-
if [[ "$SKIPPED" == "true" ]]; then
1017-
echo "Merge queue skipped, considering all jobs as passed"
1018-
exit 0
1019-
fi
1020-
1021-
# Check if all non-E2E jobs passed
1022-
if [[ "${{ needs.all-jobs-pass.outputs.ALL_JOBS_PASSED }}" != "true" ]]; then
1023-
echo "Non-E2E jobs failed"
1024-
exit 1
1025-
fi
1026-
1027-
# Check E2E build + smoke results only if E2E should have run.
1028-
# 'skipped' is acceptable — covers merge_group, fork PRs, ignorable-only changes,
1029-
# platform-only PRs, and AI selection returning zero tags.
1030-
# 'failure'/'cancelled' on any of build or smoke must block merge.
1031-
if [[ "${{ needs.get_requirements.outputs.skip_e2e }}" != "true" ]]; then
1032-
for entry in \
1033-
"build-android-apks:${{ needs.build-android-apks.result }}" \
1034-
"e2e-smoke-tests-android:${{ needs.e2e-smoke-tests-android.result }}" \
1035-
"build-ios-apps:${{ needs.build-ios-apps.result }}" \
1036-
"e2e-smoke-tests-ios:${{ needs.e2e-smoke-tests-ios.result }}"; do
1037-
name="${entry%%:*}"
1038-
result="${entry#*:}"
1039-
if [[ "$result" == "failure" ]] || [[ "$result" == "cancelled" ]]; then
1040-
echo "::error::Required E2E job '$name' did not succeed (result: $result)"
1041-
exit 1
1042-
fi
1043-
done
1044-
fi
961+
- uses: actions/checkout@v6
962+
with:
963+
fetch-depth: 1
964+
sparse-checkout: |
965+
.github/actions/ci-status-gate
1045966
1046-
echo "All required jobs passed"
967+
- name: Evaluate CI status
968+
uses: ./.github/actions/ci-status-gate
969+
with:
970+
needs-json: ${{ toJSON(needs) }}
971+
requirement-context-json: ${{ toJSON(needs.get_requirements.outputs) }}
972+
e2e-job-regex: '^(build-android-apks|build-ios-apps|e2e-smoke-tests-android|e2e-smoke-tests-ios)$'
973+
event-name: ${{ github.event_name }}
974+
is-fork: ${{ github.event.pull_request.head.repo.fork == true }}
1047975

1048976
log-merge-group-failure:
1049977
name: Log merge group failure

0 commit comments

Comments
 (0)